@gjermundgaraba/effect-actions 0.19.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (326) hide show
  1. package/README.md +19 -41
  2. package/dist/Action.d.mts +140 -48
  3. package/dist/Action.mjs +88 -44
  4. package/dist/ActionCli.d.mts +70 -35
  5. package/dist/ActionCli.mjs +53 -34
  6. package/dist/ActionHttp.d.mts +70 -38
  7. package/dist/ActionHttp.mjs +76 -25
  8. package/dist/ActionMcp.d.mts +56 -24
  9. package/dist/ActionMcp.mjs +104 -61
  10. package/dist/ActionRpc.d.mts +222 -57
  11. package/dist/ActionRpc.mjs +301 -49
  12. package/dist/ActionToolkit.d.mts +22 -7
  13. package/dist/ActionToolkit.mjs +37 -14
  14. package/dist/Authentication.d.mts +42 -12
  15. package/dist/Authentication.mjs +38 -32
  16. package/dist/Event.d.mts +370 -0
  17. package/dist/Event.mjs +103 -0
  18. package/dist/Testing.d.mts +1 -3
  19. package/dist/Testing.mjs +2 -3
  20. package/dist/Webhook.d.mts +146 -0
  21. package/dist/Webhook.mjs +72 -0
  22. package/dist/authentication/provider.d.mts +35 -12
  23. package/dist/authentication/provider.mjs +2 -3
  24. package/dist/authentication/refusal.mjs +3 -4
  25. package/dist/cli/commands.d.mts +9 -7
  26. package/dist/cli/commands.mjs +85 -35
  27. package/dist/contract/ast.mjs +5 -3
  28. package/dist/contract/authorizer.d.mts +8 -0
  29. package/dist/contract/authorizer.mjs +20 -0
  30. package/dist/contract/call.d.mts +9 -8
  31. package/dist/contract/call.mjs +49 -8
  32. package/dist/contract/callback.d.mts +7 -0
  33. package/dist/contract/callback.mjs +27 -0
  34. package/dist/contract/caller.d.mts +12 -3
  35. package/dist/contract/caller.mjs +23 -5
  36. package/dist/contract/canonical.mjs +10 -0
  37. package/dist/contract/codec.d.mts +9 -0
  38. package/dist/contract/codec.mjs +9 -0
  39. package/dist/contract/codes.mjs +81 -0
  40. package/dist/contract/declared.d.mts +0 -2
  41. package/dist/contract/declared.mjs +26 -11
  42. package/dist/contract/digest.mjs +6 -0
  43. package/dist/contract/errors.d.mts +9 -5
  44. package/dist/contract/errors.mjs +2 -3
  45. package/dist/contract/feed.d.mts +127 -0
  46. package/dist/contract/feed.mjs +249 -0
  47. package/dist/contract/hints.mjs +1 -2
  48. package/dist/contract/hub.d.mts +94 -0
  49. package/dist/contract/hub.mjs +133 -0
  50. package/dist/contract/idempotency.d.mts +47 -0
  51. package/dist/contract/idempotency.mjs +99 -0
  52. package/dist/contract/implementation.d.mts +73 -51
  53. package/dist/contract/implementation.mjs +94 -60
  54. package/dist/contract/input-check.d.mts +15 -0
  55. package/dist/contract/input-check.mjs +16 -0
  56. package/dist/contract/lease.mjs +28 -0
  57. package/dist/contract/listed.d.mts +4 -0
  58. package/dist/contract/listed.mjs +22 -0
  59. package/dist/contract/log.d.mts +41 -0
  60. package/dist/contract/log.mjs +144 -0
  61. package/dist/contract/media.d.mts +1 -3
  62. package/dist/contract/media.mjs +1 -2
  63. package/dist/contract/names.mjs +15 -6
  64. package/dist/contract/occurrence.d.mts +29 -0
  65. package/dist/contract/occurrence.mjs +45 -0
  66. package/dist/contract/pages.d.mts +13 -0
  67. package/dist/contract/pages.mjs +39 -0
  68. package/dist/contract/progress.d.mts +26 -0
  69. package/dist/contract/progress.mjs +72 -0
  70. package/dist/contract/push.d.mts +20 -0
  71. package/dist/contract/push.mjs +96 -0
  72. package/dist/contract/receiving.d.mts +29 -0
  73. package/dist/contract/receiving.mjs +42 -0
  74. package/dist/contract/recent.mjs +12 -0
  75. package/dist/contract/reported.mjs +16 -0
  76. package/dist/contract/serving.mjs +63 -0
  77. package/dist/contract/status.mjs +15 -7
  78. package/dist/contract/stream.d.mts +34 -0
  79. package/dist/contract/stream.mjs +31 -0
  80. package/dist/contract/subscribe.d.mts +45 -0
  81. package/dist/contract/subscribe.mjs +50 -0
  82. package/dist/contract/transient.mjs +28 -0
  83. package/dist/contract/watch.d.mts +18 -0
  84. package/dist/contract/watch.mjs +105 -0
  85. package/dist/contract/webhooks.d.mts +42 -0
  86. package/dist/contract/webhooks.mjs +53 -0
  87. package/dist/http/client.d.mts +37 -14
  88. package/dist/http/client.mjs +52 -14
  89. package/dist/http/events.d.mts +56 -0
  90. package/dist/http/events.mjs +102 -0
  91. package/dist/http/placed.d.mts +36 -0
  92. package/dist/http/placed.mjs +71 -0
  93. package/dist/http/schema-errors.mjs +2 -3
  94. package/dist/http/streamed.d.mts +21 -0
  95. package/dist/http/streamed.mjs +94 -0
  96. package/dist/http/watch.mjs +50 -0
  97. package/dist/mcp/bridge.mjs +283 -0
  98. package/dist/mcp/client.d.mts +30 -99
  99. package/dist/mcp/client.mjs +60 -134
  100. package/dist/mcp/events.mjs +142 -0
  101. package/dist/mcp/progress.mjs +34 -0
  102. package/dist/mcp/protocol.mjs +40 -15
  103. package/dist/mcp/reply.d.mts +79 -0
  104. package/dist/mcp/reply.mjs +99 -0
  105. package/dist/mcp/request.d.mts +40 -0
  106. package/dist/mcp/request.mjs +51 -0
  107. package/dist/mcp/watch.mjs +115 -0
  108. package/dist/rpc/watch.mjs +39 -0
  109. package/dist/stdio/console.mjs +14 -7
  110. package/dist/testing/memory.d.mts +1 -3
  111. package/dist/testing/memory.mjs +4 -5
  112. package/dist/webhook/address.mjs +47 -0
  113. package/dist/webhook/attempt.mjs +65 -0
  114. package/dist/webhook/receiver.mjs +67 -0
  115. package/dist/webhook/signing.mjs +28 -0
  116. package/dist/webhook/stored.mjs +57 -0
  117. package/dist/webhook/subscriptions.mjs +342 -0
  118. package/docs/Action-failures.md +2 -2
  119. package/docs/Action-streams.md +115 -0
  120. package/docs/Action.md +58 -124
  121. package/docs/ActionCli-failures.md +1 -2
  122. package/docs/ActionCli.md +83 -88
  123. package/docs/ActionHttp-failures.md +3 -3
  124. package/docs/ActionHttp.md +90 -70
  125. package/docs/ActionMcp-failures.md +2 -1
  126. package/docs/ActionMcp.md +120 -92
  127. package/docs/ActionRpc-events.md +126 -0
  128. package/docs/ActionRpc-failures.md +1 -2
  129. package/docs/ActionRpc.md +108 -79
  130. package/docs/ActionToolkit-failures.md +1 -2
  131. package/docs/ActionToolkit.md +33 -90
  132. package/docs/Authentication-advanced.md +57 -61
  133. package/docs/Authentication-failures.md +5 -4
  134. package/docs/Authentication.md +23 -58
  135. package/docs/Event-failures.md +15 -0
  136. package/docs/Event.md +281 -0
  137. package/docs/README.md +51 -82
  138. package/docs/Testing-failures.md +1 -5
  139. package/docs/Testing.md +126 -86
  140. package/docs/Webhook-failures.md +8 -0
  141. package/docs/Webhook.md +319 -0
  142. package/docs/setup-failures.md +2 -3
  143. package/docs/setup.md +32 -53
  144. package/examples/README.md +12 -37
  145. package/examples/assistant/README.md +21 -26
  146. package/examples/assistant/src/agent.ts +66 -24
  147. package/examples/assistant/src/cli.ts +7 -4
  148. package/examples/assistant/src/commands.ts +9 -4
  149. package/examples/assistant/src/identity/Identity.ts +5 -4
  150. package/examples/assistant/src/identity/authorize.ts +13 -5
  151. package/examples/assistant/src/identity/handlers.ts +3 -2
  152. package/examples/assistant/src/identity/permissions.ts +9 -3
  153. package/examples/assistant/src/platform/tasks-file.ts +15 -11
  154. package/examples/assistant/src/scripted.ts +7 -4
  155. package/examples/assistant/src/tasks/Tasks.ts +20 -6
  156. package/examples/assistant/src/tasks/handlers.ts +9 -5
  157. package/examples/assistant/src/tasks/store.ts +38 -11
  158. package/examples/assistant/src/toolkit.ts +11 -7
  159. package/examples/assistant/test/commands.test.ts +53 -27
  160. package/examples/assistant/test/identity/identity.test.ts +27 -11
  161. package/examples/assistant/test/platform/tasks-file.test.ts +39 -23
  162. package/examples/assistant/test/tasks/tasks.test.ts +62 -34
  163. package/examples/assistant/test/toolkit.test.ts +104 -75
  164. package/examples/build-events/README.md +83 -0
  165. package/examples/build-events/src/Api.ts +27 -0
  166. package/examples/build-events/src/Rpc.ts +26 -0
  167. package/examples/build-events/src/builds/Builds.ts +108 -0
  168. package/examples/build-events/src/builds/events.ts +54 -0
  169. package/examples/build-events/src/builds/handlers.ts +58 -0
  170. package/examples/build-events/src/builds/log.ts +30 -0
  171. package/examples/build-events/src/builds/status.ts +28 -0
  172. package/examples/build-events/src/builds/store.ts +130 -0
  173. package/examples/build-events/src/cli.ts +18 -0
  174. package/examples/build-events/src/commands.ts +32 -0
  175. package/examples/build-events/src/deploys/Deploys.ts +44 -0
  176. package/examples/build-events/src/deploys/approvers.ts +55 -0
  177. package/examples/build-events/src/deploys/events.ts +32 -0
  178. package/examples/build-events/src/deploys/handlers.ts +38 -0
  179. package/examples/build-events/src/identity/Identity.ts +29 -0
  180. package/examples/build-events/src/identity/members.ts +7 -0
  181. package/examples/build-events/src/identity/verifier.ts +22 -0
  182. package/examples/build-events/src/notifier/Notices.ts +26 -0
  183. package/examples/build-events/src/notifier/routes.ts +71 -0
  184. package/examples/build-events/src/notifier.ts +48 -0
  185. package/examples/build-events/src/routes.ts +71 -0
  186. package/examples/build-events/src/server.ts +62 -0
  187. package/examples/build-events/test/builds/events.test.ts +68 -0
  188. package/examples/build-events/test/builds/log.test.ts +71 -0
  189. package/examples/build-events/test/builds/progress.test.ts +57 -0
  190. package/examples/build-events/test/builds/status.test.ts +42 -0
  191. package/examples/build-events/test/commands.test.ts +103 -0
  192. package/examples/build-events/test/deploys/events.test.ts +52 -0
  193. package/examples/build-events/test/http.test.ts +95 -0
  194. package/examples/build-events/test/notifier.test.ts +80 -0
  195. package/examples/build-events/test/routes.test.ts +96 -0
  196. package/examples/build-events/test/rpc.test.ts +84 -0
  197. package/examples/build-events/test/support/as.ts +8 -0
  198. package/examples/build-events/test/support/memory.ts +43 -0
  199. package/examples/build-events/test/webhooks.test.ts +143 -0
  200. package/examples/kitchen-sink/README.md +178 -166
  201. package/examples/kitchen-sink/src/Actions.ts +11 -3
  202. package/examples/kitchen-sink/src/Api.ts +12 -6
  203. package/examples/kitchen-sink/src/Rpc.ts +7 -5
  204. package/examples/kitchen-sink/src/admin/Admin.ts +21 -10
  205. package/examples/kitchen-sink/src/admin/authenticate.ts +8 -4
  206. package/examples/kitchen-sink/src/admin/authorize.ts +13 -5
  207. package/examples/kitchen-sink/src/admin/grants.ts +5 -3
  208. package/examples/kitchen-sink/src/admin/handlers.ts +9 -5
  209. package/examples/kitchen-sink/src/agent.ts +14 -6
  210. package/examples/kitchen-sink/src/assistant/Assistant.ts +21 -15
  211. package/examples/kitchen-sink/src/assistant/conversations.ts +14 -9
  212. package/examples/kitchen-sink/src/assistant/demo-model.ts +2 -2
  213. package/examples/kitchen-sink/src/assistant/handlers.ts +42 -22
  214. package/examples/kitchen-sink/src/attachments/Attachments.ts +6 -6
  215. package/examples/kitchen-sink/src/attachments/download.ts +7 -6
  216. package/examples/kitchen-sink/src/attachments/handlers.ts +9 -4
  217. package/examples/kitchen-sink/src/attachments/store.ts +25 -18
  218. package/examples/kitchen-sink/src/browser.ts +10 -4
  219. package/examples/kitchen-sink/src/cli.ts +28 -12
  220. package/examples/kitchen-sink/src/config.ts +1 -1
  221. package/examples/kitchen-sink/src/identity/Identity.ts +24 -10
  222. package/examples/kitchen-sink/src/identity/authenticate.ts +19 -11
  223. package/examples/kitchen-sink/src/identity/authorize.ts +14 -4
  224. package/examples/kitchen-sink/src/identity/directory.ts +16 -7
  225. package/examples/kitchen-sink/src/identity/handlers.ts +1 -0
  226. package/examples/kitchen-sink/src/identity/members.ts +7 -2
  227. package/examples/kitchen-sink/src/identity/quota.ts +23 -20
  228. package/examples/kitchen-sink/src/issues/Issues.ts +91 -15
  229. package/examples/kitchen-sink/src/issues/assistant.ts +4 -2
  230. package/examples/kitchen-sink/src/issues/events.ts +46 -0
  231. package/examples/kitchen-sink/src/issues/export.ts +35 -0
  232. package/examples/kitchen-sink/src/issues/guide.ts +4 -3
  233. package/examples/kitchen-sink/src/issues/handlers.ts +20 -7
  234. package/examples/kitchen-sink/src/issues/store.ts +156 -67
  235. package/examples/kitchen-sink/src/issues/write-limit.ts +44 -18
  236. package/examples/kitchen-sink/src/memory.ts +11 -2
  237. package/examples/kitchen-sink/src/ops.ts +10 -5
  238. package/examples/kitchen-sink/src/request-policy.ts +25 -9
  239. package/examples/kitchen-sink/src/routes.ts +67 -27
  240. package/examples/kitchen-sink/src/sdk.ts +22 -12
  241. package/examples/kitchen-sink/src/seed.ts +15 -9
  242. package/examples/kitchen-sink/src/server.ts +24 -10
  243. package/examples/kitchen-sink/src/stdio.ts +9 -3
  244. package/examples/kitchen-sink/src/workspaces/Workspaces.ts +10 -7
  245. package/examples/kitchen-sink/src/workspaces/handlers.ts +3 -2
  246. package/examples/kitchen-sink/src/workspaces/registry.ts +16 -10
  247. package/examples/kitchen-sink/src/workspaces/tenant.ts +12 -5
  248. package/examples/kitchen-sink/test/admin/admin.test.ts +38 -23
  249. package/examples/kitchen-sink/test/assistant/assistant.test.ts +471 -299
  250. package/examples/kitchen-sink/test/attachments/attachments.test.ts +135 -94
  251. package/examples/kitchen-sink/test/identity/identity.test.ts +110 -82
  252. package/examples/kitchen-sink/test/identity/session.test.ts +65 -43
  253. package/examples/kitchen-sink/test/issues/assistant.test.ts +50 -31
  254. package/examples/kitchen-sink/test/issues/events.test.ts +273 -0
  255. package/examples/kitchen-sink/test/issues/export.test.ts +120 -0
  256. package/examples/kitchen-sink/test/issues/issues.test.ts +105 -75
  257. package/examples/kitchen-sink/test/request-policy.test.ts +130 -33
  258. package/examples/kitchen-sink/test/routes.test.ts +102 -57
  259. package/examples/kitchen-sink/test/support/as.ts +9 -0
  260. package/examples/kitchen-sink/test/support/scripted.ts +6 -5
  261. package/examples/kitchen-sink/test/support/services.ts +17 -0
  262. package/examples/live-rpc/README.md +33 -35
  263. package/examples/live-rpc/src/Rpc.ts +9 -5
  264. package/examples/live-rpc/src/browser.ts +39 -11
  265. package/examples/live-rpc/src/chat/Chat.ts +30 -16
  266. package/examples/live-rpc/src/chat/authorize.ts +2 -1
  267. package/examples/live-rpc/src/chat/events.ts +74 -0
  268. package/examples/live-rpc/src/chat/handlers.ts +13 -5
  269. package/examples/live-rpc/src/chat/room.ts +9 -5
  270. package/examples/live-rpc/src/chat/store.ts +51 -14
  271. package/examples/live-rpc/src/client.ts +22 -5
  272. package/examples/live-rpc/src/identity/Identity.ts +3 -3
  273. package/examples/live-rpc/src/identity/verifier.ts +8 -4
  274. package/examples/live-rpc/src/page.ts +58 -12
  275. package/examples/live-rpc/src/routes.ts +14 -10
  276. package/examples/live-rpc/src/server.ts +15 -5
  277. package/examples/live-rpc/test/browser.test.ts +127 -50
  278. package/examples/live-rpc/test/chat/chat.test.ts +215 -23
  279. package/examples/live-rpc/test/routes.test.ts +130 -46
  280. package/examples/live-rpc/test/server.test.ts +94 -33
  281. package/examples/mcp-tools/README.md +24 -38
  282. package/examples/mcp-tools/src/platform/fake-farm.ts +63 -24
  283. package/examples/mcp-tools/src/platform/print-farm.ts +59 -29
  284. package/examples/mcp-tools/src/printers/Printers.ts +18 -7
  285. package/examples/mcp-tools/src/printers/farm.ts +15 -5
  286. package/examples/mcp-tools/src/printers/handlers.ts +12 -6
  287. package/examples/mcp-tools/src/routes.ts +8 -6
  288. package/examples/mcp-tools/src/server.ts +12 -5
  289. package/examples/mcp-tools/src/stdio.ts +3 -2
  290. package/examples/mcp-tools/test/printers/printers.test.ts +132 -89
  291. package/examples/mcp-tools/test/routes.test.ts +140 -81
  292. package/examples/notes-service/README.md +30 -45
  293. package/examples/notes-service/src/Api.ts +1 -0
  294. package/examples/notes-service/src/browser.ts +15 -14
  295. package/examples/notes-service/src/cli.ts +8 -3
  296. package/examples/notes-service/src/commands.ts +46 -20
  297. package/examples/notes-service/src/identity/Identity.ts +2 -2
  298. package/examples/notes-service/src/identity/authorize.ts +6 -2
  299. package/examples/notes-service/src/identity/handlers.ts +3 -2
  300. package/examples/notes-service/src/identity/users.ts +2 -1
  301. package/examples/notes-service/src/identity/verifier.ts +12 -6
  302. package/examples/notes-service/src/notes/Notes.ts +46 -22
  303. package/examples/notes-service/src/notes/handlers.ts +18 -9
  304. package/examples/notes-service/src/notes/store.ts +70 -43
  305. package/examples/notes-service/src/routes.ts +7 -5
  306. package/examples/notes-service/src/server.ts +12 -5
  307. package/examples/notes-service/src/status/Status.ts +1 -1
  308. package/examples/notes-service/src/status/handlers.ts +5 -3
  309. package/examples/notes-service/src/stdio.ts +7 -3
  310. package/examples/notes-service/src/worker.ts +6 -3
  311. package/examples/notes-service/test/commands.test.ts +164 -91
  312. package/examples/notes-service/test/notes/as.ts +3 -1
  313. package/examples/notes-service/test/notes/idempotency.test.ts +97 -67
  314. package/examples/notes-service/test/notes/in-process.test.ts +26 -15
  315. package/examples/notes-service/test/notes/notes.test.ts +49 -31
  316. package/examples/notes-service/test/routes.test.ts +106 -71
  317. package/examples/notes-service/test/worker.test.ts +21 -13
  318. package/examples/quickstart/README.md +2 -6
  319. package/examples/quickstart/src/Api.ts +1 -0
  320. package/examples/quickstart/src/client.ts +6 -3
  321. package/examples/quickstart/src/greeting/Greeting.ts +1 -1
  322. package/examples/quickstart/src/greeting/handlers.ts +5 -2
  323. package/examples/quickstart/src/routes.ts +3 -2
  324. package/examples/quickstart/src/server.ts +6 -2
  325. package/examples/quickstart/test/routes.test.ts +3 -2
  326. package/package.json +19 -17
@@ -0,0 +1,18 @@
1
+ import "./pages.mjs";
2
+ import { Any, WatchOptions, WebhookDelivery } from "../Event.mjs";
3
+ import { Receiving } from "./receiving.mjs";
4
+ import "./subscribe.mjs";
5
+ import { Effect, Schema, Stream } from "effect";
6
+ /**
7
+ * The arguments of `watch(event, ...)` after the event: its input, which may be left out when
8
+ * `{}` is a valid encoded input, as for an action's call, and the watch's options `O`, whose
9
+ * `resume` store may fail with `SE`.
10
+ */
11
+ export type WatchArgs<E extends Any, SE, O extends WatchOptions<unknown> = WatchOptions<SE>> = {} extends E["input"]["Encoded"] ? [input?: E["input"]["Type"], options?: O & WatchOptions<SE>] : [input: E["input"]["Type"], options?: O & WatchOptions<SE>];
12
+ /**
13
+ * What a watch given the options `O` requires beyond its client's: the receiver, where its `mode`
14
+ * may be a `Webhook.delivery`.
15
+ */
16
+ export type WatchRequirement<O> = O extends {
17
+ readonly mode?: infer M;
18
+ } ? [Extract<M, WebhookDelivery>] extends [never] ? never : Receiving : never;
@@ -0,0 +1,105 @@
1
+ import { Checked, Gap } from "./occurrence.mjs";
2
+ import { paged } from "./pages.mjs";
3
+ import { SubscriptionError, Unsupported } from "../Event.mjs";
4
+ import { watchParameter } from "./receiving.mjs";
5
+ import { makeRecent } from "./recent.mjs";
6
+ import { CallbackUrl } from "./callback.mjs";
7
+ import { Cause, Clock, Duration, Effect, Predicate, Queue, Schedule, Schema, Stream } from "effect";
8
+ // src/contract/watch.ts
9
+ const reconnectDelay = Duration.seconds(1);
10
+ const maxReconnectDelay = Duration.seconds(30);
11
+ const afterFailures = (transient) => Schedule.min([Schedule.exponential(reconnectDelay), Schedule.spaced(maxReconnectDelay)]).pipe(Schedule.setInputType(), Schedule.while(({ input }) => transient(input)), Schedule.tap(({ input, duration }) => Effect.logWarning(`A watch reconnects in ${Duration.format(duration)} after a failure`, input)));
12
+ const isSubscriptionError = Schema.is(SubscriptionError);
13
+ /** Whether a watch's failure says the server serves its event by poll alone, refusing to push it. */
14
+ const pushRefused = Predicate.compose(isSubscriptionError, ({ reason }) => Predicate.isTagged(reason, "Unsupported") && reason.feature === "deliveryMode" && reason.value === "push");
15
+ const dedupeWindow = 1e3;
16
+ const capacity = 16;
17
+ const unsubscribeTimeout = Duration.seconds(5);
18
+ /**
19
+ * One webhook subscription from `cursor`, read from `received`, where its receiver puts each
20
+ * body: each grant, the subscribe's and each refresh's when half of it is left, is put there too,
21
+ * behind what was accepted before it, so its position is kept in order; a refresh's failure is
22
+ * put there as well, and ends the run where it is read.
23
+ */
24
+ const delivered = (webhook, request, received, cursor) => {
25
+ let at = cursor;
26
+ const subscribed = Effect.suspend(() => webhook.subscribe({
27
+ ...request,
28
+ cursor: at
29
+ })).pipe(Effect.tap((grant) => Queue.offer(received, Effect.succeed([grant.truncated ? Gap.make({ cursor: grant.cursor }) : Checked.make({ cursor: grant.cursor })]))));
30
+ const refreshed = (refreshBefore) => refreshBefore === null ? Effect.never : Effect.flatMap(Clock.currentTimeMillis, (now) => Effect.andThen(Effect.sleep(Duration.millis(Math.max(0, (Date.parse(refreshBefore) - now) / 2))), Effect.flatMap(subscribed, (grant) => refreshed(grant.refreshBefore))));
31
+ return Stream.unwrap(Effect.gen(function* () {
32
+ const grant = yield* subscribed;
33
+ yield* Effect.forkScoped(Effect.catchCause(refreshed(grant.refreshBefore), (cause) => Cause.hasInterruptsOnly(cause) ? Effect.void : Queue.offer(received, Effect.failCause(cause))));
34
+ return Stream.fromEffectRepeat(Effect.flatten(Queue.take(received))).pipe(Stream.flattenIterable, Stream.tap((fed) => Effect.sync(() => {
35
+ at = fed.cursor ?? at;
36
+ })));
37
+ }));
38
+ };
39
+ /**
40
+ * The occurrences and gaps of one subscription, as every client's `watch` answers them. Each run
41
+ * starts from `options`: what one run saw, the cursor and the ids it deduplicates by, ends with
42
+ * it, and only `resume` carries a cursor to the next.
43
+ */
44
+ const watching = (transport, { cursor: given, mode, resume } = {}) => {
45
+ const reconnecting = (open, cursor, polled) => {
46
+ const { transient } = transport;
47
+ let at = cursor;
48
+ const once = Stream.suspend(() => open(at)).pipe(Stream.tap((fed) => Effect.sync(() => {
49
+ at = fed.cursor ?? at;
50
+ })));
51
+ const reopened = transient === void 0 ? once : once.pipe(Stream.repeat(Schedule.spaced(reconnectDelay)), Stream.retry(afterFailures(transient)));
52
+ return polled === void 0 ? reopened : reopened.pipe(Stream.catchIf(pushRefused, () => Stream.suspend(() => polled(at))));
53
+ };
54
+ const polled = (cursor) => reconnecting((at) => paged(transport.poll, at), cursor);
55
+ const webhooked = ({ baseUrl, ttl }, cursor) => Stream.unwrap(Effect.gen(function* () {
56
+ const { webhook } = transport;
57
+ if (webhook === void 0) return yield* SubscriptionError.of(transport.name, new Unsupported({
58
+ feature: "deliveryMode",
59
+ value: "webhook"
60
+ }));
61
+ const base = yield* Schema.decodeEffect(CallbackUrl)(baseUrl);
62
+ const receiving = yield* webhook.receiver;
63
+ const received = yield* Queue.unbounded();
64
+ const { id, secret } = yield* receiving.register((body) => Queue.sizeUnsafe(received) < capacity && Queue.offerUnsafe(received, webhook.read(body)));
65
+ const url = new URL(`${base.pathname.replace(/\/$/u, "")}${receiving.path}`, base);
66
+ url.searchParams.set(watchParameter, id);
67
+ const request = {
68
+ delivery: {
69
+ url,
70
+ secret
71
+ },
72
+ ttlMs: ttl === void 0 ? void 0 : Duration.toMillis(Duration.fromInputUnsafe(ttl))
73
+ };
74
+ yield* Effect.addFinalizer(() => webhook.unsubscribe({ delivery: { url } }).pipe(Effect.timeout(unsubscribeTimeout), Effect.ignore));
75
+ return reconnecting((at) => delivered(webhook, request, received, at), cursor);
76
+ }));
77
+ return Stream.suspend(() => {
78
+ const seen = makeRecent(dedupeWindow);
79
+ let pending = null;
80
+ const keep = (cursor) => cursor === null || resume === void 0 ? Effect.void : resume.store.set(resume.key, cursor);
81
+ const fresh = (eventId) => {
82
+ if (seen.has(eventId)) return false;
83
+ seen.add(eventId);
84
+ return true;
85
+ };
86
+ const handled = Effect.fnUntraced(function* (fed) {
87
+ yield* keep(pending);
88
+ pending = null;
89
+ if (Predicate.isTagged(fed, "Checked")) {
90
+ yield* keep(fed.cursor);
91
+ return [];
92
+ }
93
+ if (Predicate.isTagged(fed, "Gap")) return [fed];
94
+ pending = fed.cursor;
95
+ return fresh(fed.eventId) ? [fed] : [];
96
+ });
97
+ const start = given !== void 0 || resume === void 0 ? Effect.succeed(given ?? null) : Effect.map(resume.store.get(resume.key), (kept) => kept ?? null);
98
+ return Stream.unwrap(Effect.map(start, (cursor) => {
99
+ if (Predicate.isTagged(mode, "WebhookDelivery")) return webhooked(mode, cursor);
100
+ if (mode === "poll") return polled(cursor);
101
+ return reconnecting(transport.stream, cursor, mode === "stream" ? void 0 : polled);
102
+ })).pipe(Stream.rechunk(1), Stream.mapEffect(handled), Stream.catchCause((cause) => Cause.hasInterruptsOnly(cause) ? Stream.failCause(cause) : Stream.concat(Stream.fromEffectDrain(keep(pending)), Stream.failCause(cause))), Stream.flattenIterable);
103
+ });
104
+ };
105
+ export { watching };
@@ -0,0 +1,42 @@
1
+ import { Anyone, Named } from "./caller.mjs";
2
+ import { Feed } from "./feed.mjs";
3
+ import { Any } from "../Event.mjs";
4
+ import { AnyWebhookCancellation, AnyWebhookSubscription, Subscribed } from "./subscribe.mjs";
5
+ import { Context, Effect, Option } from "effect";
6
+ /** An event type a surface may offer by webhook: a protected one, its caller an identity. */
7
+ export type Protected = Any & {
8
+ readonly caller: Exclude<Any["caller"], typeof Anyone>;
9
+ };
10
+ /** A feed a surface offers by webhook: one of a protected event type. */
11
+ export interface WebhookFeed extends Feed {
12
+ readonly event: Protected;
13
+ }
14
+ /** Which webhook subscription a request names: the subscriber, the event, its input, the URL. */
15
+ export interface Subscription extends AnyWebhookCancellation {
16
+ readonly feed: WebhookFeed;
17
+ readonly principal: string;
18
+ }
19
+ /**
20
+ * A request to start or refresh a webhook subscription, as its surface decoded it, `keyed` the
21
+ * surface's authentication, whose `schema` keeps the subscriber's identity.
22
+ */
23
+ export interface Subscribing extends AnyWebhookSubscription {
24
+ readonly feed: WebhookFeed;
25
+ readonly principal: string;
26
+ readonly keyed: Named;
27
+ }
28
+ declare const Webhooks_base: Context.ServiceClass<Webhooks, "effect-actions/Webhooks", {
29
+ readonly serve: (feeds: readonly WebhookFeed[], keyed: Named) => Effect.Effect<void, never, unknown>;
30
+ readonly subscribe: (subscribing: Subscribing) => Effect.Effect<Subscribed, unknown, unknown>;
31
+ readonly unsubscribe: (subscription: Subscription) => Effect.Effect<void, unknown, unknown>;
32
+ }>;
33
+ /**
34
+ * Where a surface's webhook subscriptions are held and delivered from: `Webhook.layer` provides
35
+ * it, and a surface offers webhook delivery only where it is. A surface `serve`s the feeds it
36
+ * offers by webhook as it builds, with its authentication, which restores their kept
37
+ * subscriptions, and dies where they cannot be kept. `subscribe` runs as the subscriber, whose
38
+ * context each delivery runs in; it fails as the feed's admission does, or with
39
+ * `Event.SubscriptionError`.
40
+ */
41
+ export declare class Webhooks extends Webhooks_base {}
42
+ export {};
@@ -0,0 +1,53 @@
1
+ import { Anyone, isNamed } from "./caller.mjs";
2
+ import { SubscriptionError, Unsupported } from "../Event.mjs";
3
+ import { Context, Effect, Option } from "effect";
4
+ // src/contract/webhooks.ts
5
+ /**
6
+ * Where a surface's webhook subscriptions are held and delivered from: `Webhook.layer` provides
7
+ * it, and a surface offers webhook delivery only where it is. A surface `serve`s the feeds it
8
+ * offers by webhook as it builds, with its authentication, which restores their kept
9
+ * subscriptions, and dies where they cannot be kept. `subscribe` runs as the subscriber, whose
10
+ * context each delivery runs in; it fails as the feed's admission does, or with
11
+ * `Event.SubscriptionError`.
12
+ */
13
+ var Webhooks = class extends Context.Service()("effect-actions/Webhooks") {};
14
+ const isProtected = (event) => event.caller !== Anyone;
15
+ /** Whether a surface offers `event` by webhook: a protected event, its subscribers `keyed`. */
16
+ const offersWebhook = (event, keyed) => isProtected(event) && isNamed(keyed);
17
+ /**
18
+ * Serves the feeds among `feeds` a surface offers by webhook, its subscribers `keyed`, to the
19
+ * `Webhooks` it is given, as it builds: restoring their kept subscriptions, and dying where they
20
+ * cannot be kept. Answers the service, where given, and those feeds.
21
+ */
22
+ const servedWebhooks = (feeds, keyed) => Effect.gen(function* () {
23
+ const webhooks = yield* Effect.serviceOption(Webhooks);
24
+ const offered = feeds.filter((feed) => offersWebhook(feed.event, keyed));
25
+ if (Option.isSome(webhooks) && isNamed(keyed)) yield* webhooks.value.serve(offered, keyed);
26
+ return {
27
+ webhooks,
28
+ offered
29
+ };
30
+ });
31
+ /**
32
+ * The webhook subscriber calling a subscribe or unsubscribe of `feed`'s event on a surface given
33
+ * `webhooks`, its subscribers `keyed`; `Unsupported` where the surface does not offer it by
34
+ * webhook.
35
+ */
36
+ const webhookSubscriber = (feed, keyed, webhooks) => {
37
+ const { event } = feed;
38
+ if (Option.isNone(webhooks) || !isNamed(keyed) || !isProtected(event)) return Effect.fail(SubscriptionError.of(event.name, new Unsupported({
39
+ feature: "deliveryMode",
40
+ value: "webhook"
41
+ })));
42
+ const service = webhooks.value;
43
+ return Effect.map(Effect.service(event.caller), (identity) => ({
44
+ webhooks: service,
45
+ feed: {
46
+ ...feed,
47
+ event
48
+ },
49
+ keyed,
50
+ principal: keyed.principal(identity)
51
+ }));
52
+ };
53
+ export { Webhooks, offersWebhook, servedWebhooks, webhookSubscriber };
@@ -1,11 +1,17 @@
1
+ import { Codec } from "../contract/codec.mjs";
1
2
  import { BuiltIns } from "../contract/errors.mjs";
3
+ import { Any, Received, SubscriptionError, WatchOptions } from "../Event.mjs";
4
+ import "../contract/implementation.mjs";
2
5
  import { Call } from "../contract/call.mjs";
3
- import { Any, Errors } from "../Action.mjs";
4
- import { Any as Any$1, VerifierError } from "../authentication/provider.mjs";
5
- import { Effect, Schema } from "effect";
6
+ import { Answer, StreamOf } from "../contract/stream.mjs";
7
+ import "../contract/receiving.mjs";
8
+ import { WatchArgs, WatchRequirement } from "../contract/watch.mjs";
9
+ import { Any as Any$1, Errors } from "../Action.mjs";
10
+ import { Any as Any$2, VerifierError } from "../authentication/provider.mjs";
11
+ import { sentOf } from "./events.mjs";
12
+ import { Effect, Schema, Stream } from "effect";
6
13
  import { HttpClient, HttpClientError } from "effect/http";
7
14
  import { HttpApi, HttpApiClient } from "effect/http-api";
8
- //#region src/http/client.d.ts
9
15
  /**
10
16
  * The native `HttpApiClient.make` options except `transformResponse`, which may change a call's
11
17
  * success, failure or services the method types cannot follow: `baseUrl`, and `transformClient`,
@@ -32,31 +38,48 @@ export interface Options<E = HttpClientError.HttpClientError, R = never> {
32
38
  readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient.With<E, R>) | undefined;
33
39
  }
34
40
  type BindingError = Errors[number];
41
+ type SseFailure = Extract<Stream.Error<HttpApiClient.SuccessType<ReturnType<typeof sentOf<Any>>>>, {
42
+ readonly _tag: "SseError" | "Retry";
43
+ }>;
44
+ type SseError = Extract<SseFailure, {
45
+ readonly _tag: "SseError";
46
+ }>;
35
47
  /**
36
48
  * What one call of `A` fails with: a declared error value (the action's own, one `E` of the
37
49
  * binding's, or a built-in `InvalidInput`, `Unauthenticated` or `Forbidden`), a native
38
50
  * `HttpClientError` for a failed request or an undeclared answer, or a `SchemaError` when
39
- * the success does not decode.
51
+ * the success does not decode. A call reading an event stream, of an action answering a stream or
52
+ * declaring `progress`, also fails with Effect's `Sse.SseError` or `Sse.Retry`.
40
53
  */
41
- export type MethodError<A extends Any, E extends BindingError = never> = A["error"][number]["Type"] | E["Type"] | BuiltIns["Type"] | HttpClientError.HttpClientError | Schema.SchemaError;
42
- type Method<A extends Any, E extends BindingError, CE, CR> = Call<A, Effect.Effect<A["success"]["Type"], MethodError<A, E> | CE, CR>>;
54
+ export type MethodError<A extends Any$1, E extends BindingError = never> = A["error"][number]["Type"] | E["Type"] | BuiltIns["Type"] | HttpClientError.HttpClientError | Schema.SchemaError | (A["progress"] extends undefined ? A["success"] extends StreamOf<Codec> ? SseFailure : never : SseFailure);
55
+ type Method<A extends Any$1, E extends BindingError, CE, CR> = Call<A, Answer<A["success"], MethodError<A, E> | CE, CR>>;
43
56
  /**
44
57
  * Any HTTP binding, with its actions erased: what `layer` and `client` read. The API is a native
45
58
  * constraint here because native groups are invariant in their endpoints; `actions` carries the
46
59
  * types.
47
60
  */
48
61
  export interface AnyHttp {
49
- readonly authentication?: Any$1 | undefined;
50
- readonly actions: ReadonlyArray<Any>;
51
- readonly error: ReadonlyArray<BindingError>;
62
+ readonly authentication?: Any$2 | undefined;
63
+ readonly actions: readonly Any$1[];
64
+ readonly events: readonly Any[];
65
+ readonly error: readonly BindingError[];
52
66
  readonly prefix: `/${string}`;
53
67
  readonly api: HttpApi.Constraint;
54
68
  }
69
+ /**
70
+ * What a watch of `W` over the binding `H` may fail with: what its route's first answer declares,
71
+ * a refusal ending it later, an `Event.SubscriptionError` where it is not served, a native
72
+ * `HttpClientError` that no reconnect outlasts, or a stream or an occurrence that does not decode.
73
+ */
74
+ export type WatchError<W extends Any, H extends AnyHttp> = SubscriptionError | BuiltIns["Type"] | H["error"][number]["Type"] | VerifierError<W, H["authentication"]>["Type"] | HttpClientError.HttpClientError | Schema.SchemaError | SseError;
75
+ type Watching<H extends AnyHttp, E, R> = [H["events"][number]] extends [never] ? unknown : {
76
+ readonly watch: <W extends H["events"][number], SE = never, const O extends WatchOptions<unknown> = {}>(event: W, ...args: WatchArgs<W, SE, O>) => Stream.Stream<Received<W>, WatchError<W, H> | E | SE, R | WatchRequirement<O>>;
77
+ };
55
78
  /**
56
79
  * Every action of the binding `H`, as `client.<action>(input)`: a protected one may also fail
57
- * with what its descriptor's verifier declares. Each call also fails with `E` and requires `R`,
58
- * what its `transformClient` adds.
80
+ * with what its descriptor's verifier declares. Where `H` lists event types, `watch(event, input,
81
+ * options)` subscribes to one. Each call and watch also fails with `E` and requires `R`, what its
82
+ * `transformClient` adds.
59
83
  */
60
- export type Client<H extends AnyHttp, E = never, R = never> = { readonly [A in H["actions"][number] as A["name"]]: Method<A, H["error"][number] | VerifierError<A, H["authentication"]>, E, R>; };
61
- //#endregion
84
+ export type Client<H extends AnyHttp, E = never, R = never> = { readonly [A in H["actions"][number] as A["name"]]: Method<A, H["error"][number] | VerifierError<A, H["authentication"]>, E, R>; } & Watching<H, E, R>;
62
85
  export {};
@@ -1,19 +1,57 @@
1
- import { checked } from "../contract/call.mjs";
2
- import { Effect } from "effect";
3
- import { HttpClient } from "effect/http";
1
+ import { elementOf } from "../contract/stream.mjs";
2
+ import { checked, checkedStream, sendingValue, watchInput } from "../contract/call.mjs";
3
+ import { watching } from "../contract/watch.mjs";
4
+ import { readElements, readWithProgress } from "./streamed.mjs";
5
+ import { transportOf } from "./watch.mjs";
6
+ import { Effect, Stream } from "effect";
7
+ import { HttpClient, HttpClientRequest } from "effect/http";
4
8
  import { HttpApiClient } from "effect/http-api";
5
- //#region src/http/client.ts
6
- const methodsWith = (http, httpClient, baseUrl) => Effect.map(HttpApiClient.makeWith(http.api, {
9
+ // src/http/client.ts
10
+ const methodOf = (native, name, what) => {
11
+ const method = native[name];
12
+ if (method === void 0) throw new Error(`No client method for ${name} (use the binding \`ActionHttp.make\` returned, whose group holds this ${what})`);
13
+ return method;
14
+ };
15
+ const acceptingStreams = HttpClient.mapRequest(HttpClientRequest.setHeader("accept", "application/json, text/event-stream"));
16
+ const nativeOf = (http, httpClient, baseUrl) => HttpApiClient.makeWith(http.api, {
7
17
  baseUrl,
8
18
  httpClient
9
- }), (client) => {
10
- const native = client;
11
- return Object.fromEntries(http.actions.map((action) => {
12
- const method = native[action.name];
13
- if (method === void 0) throw new Error(`No client method for ${action.name} (use the binding \`ActionHttp.make\` returned, whose group holds this action)`);
14
- return [action.name, checked(action, (payload) => method({ payload }))];
15
- }));
19
+ });
20
+ const actionMethodOf = (action, native) => {
21
+ const method = methodOf(native, action.name, "action");
22
+ if (elementOf(action.success) !== void 0) return checkedStream(action, sendingValue, (payload, onProgress) => Stream.unwrap(Effect.map(method({ payload }), (answer) => readElements(answer, onProgress))));
23
+ if (action.progress === void 0) return checked(action, sendingValue, (payload) => method({ payload }));
24
+ return checked(action, sendingValue, (payload, onProgress) => Effect.flatMap(method({ payload }), (answer) => readWithProgress(answer, onProgress)));
25
+ };
26
+ /** The `Senders` of `http` over `httpClient`: its native clients, built once. */
27
+ const sendersWith = (http, httpClient, baseUrl) => Effect.gen(function* () {
28
+ const native = yield* nativeOf(http, acceptingStreams(httpClient), baseUrl);
29
+ const plain = yield* nativeOf(http, httpClient, baseUrl);
30
+ const polls = plain;
31
+ const streams = native;
32
+ const subscribes = plain;
33
+ const unsubscribes = plain;
34
+ const webhookOf = (name) => {
35
+ const subscribe = subscribes[`${name}/subscribe`];
36
+ const unsubscribe = unsubscribes[`${name}/unsubscribe`];
37
+ return subscribe === void 0 || unsubscribe === void 0 ? void 0 : {
38
+ subscribe,
39
+ unsubscribe
40
+ };
41
+ };
42
+ return {
43
+ method: (action) => actionMethodOf(action, native),
44
+ transport: (event, input) => transportOf(event, methodOf(polls, event.name, "event"), methodOf(streams, event.name, "event"), webhookOf(event.name), input)
45
+ };
46
+ });
47
+ const methodsWith = (http, httpClient, baseUrl) => Effect.map(sendersWith(http, httpClient, baseUrl), ({ method, transport }) => {
48
+ const actions = Object.fromEntries(http.actions.map((action) => [action.name, method(action)]));
49
+ if (http.events.length === 0) return actions;
50
+ const watch = (event, input, options) => Stream.unwrap(Effect.map(watchInput(event, input), ({ value }) => watching(transport(event, value), options)));
51
+ return {
52
+ ...actions,
53
+ watch
54
+ };
16
55
  });
17
56
  const methods = (http, { baseUrl, transformClient } = {}) => Effect.flatMap(HttpClient.HttpClient, (native) => transformClient === void 0 ? methodsWith(http, native, baseUrl) : methodsWith(http, transformClient(native), baseUrl));
18
- //#endregion
19
- export { methods };
57
+ export { methods, sendersWith };
@@ -0,0 +1,56 @@
1
+ import "../contract/codec.mjs";
2
+ import { Forbidden, Unauthenticated } from "../contract/errors.mjs";
3
+ import { OccurrenceSchema } from "../contract/occurrence.mjs";
4
+ import "../contract/pages.mjs";
5
+ import "../contract/feed.mjs";
6
+ import { Any } from "../Event.mjs";
7
+ import "../contract/subscribe.mjs";
8
+ import "../Action.mjs";
9
+ import "../authentication/provider.mjs";
10
+ import "../contract/push.mjs";
11
+ import "../contract/webhooks.mjs";
12
+ import { Effect, Option, Schema, Stream } from "effect";
13
+ import { HttpApiEndpoint, HttpApiSchema } from "effect/http-api";
14
+ /**
15
+ * One poll's answer: the occurrences since the cursor, and when to ask again, sent as
16
+ * `nextPollMs`, its milliseconds.
17
+ */
18
+ export declare const pageOf: <E extends Any>(event: E) => Schema.encodeKeys<Schema.Struct<{
19
+ readonly events: Schema.$Array<OccurrenceSchema<E>>;
20
+ readonly cursor: Schema.NullOr<Schema.String>;
21
+ readonly truncated: Schema.Boolean;
22
+ readonly hasMore: Schema.Boolean;
23
+ readonly nextPoll: Schema.DurationFromMillis;
24
+ }>, {
25
+ readonly nextPoll: "nextPollMs";
26
+ }>;
27
+ /**
28
+ * What a subscription's event stream sends, each as one SSE `message`: a `Checked` with the
29
+ * position it starts from, or a `Gap` where that was no longer served, then each occurrence, a
30
+ * `Gap` where some were missed, a `Checked` every 30 seconds, and a `Failed` where reading
31
+ * failed and resumes. A later refusal ends it, as Effect's stream failure event.
32
+ */
33
+ export declare const sentOf: <E extends Any>(event: E) => HttpApiSchema.StreamSse<HttpApiSchema.SseEventFromData<Schema.toCodecJson<Schema.Union<readonly [OccurrenceSchema<E>, Schema.TaggedStruct<"Gap", {
34
+ readonly cursor: Schema.NullOr<Schema.String>;
35
+ }>, Schema.TaggedStruct<"Checked", {
36
+ readonly cursor: Schema.NullOr<Schema.String>;
37
+ }>, Schema.TaggedStruct<"Failed", {}>]>>>, Schema.Union<readonly [typeof Unauthenticated, typeof Forbidden]>, {
38
+ readonly _tag: "Gap";
39
+ readonly cursor: string | null;
40
+ } | {
41
+ readonly _tag: "Checked";
42
+ readonly cursor: string | null;
43
+ } | {
44
+ readonly _tag: "Failed";
45
+ } | Schema.Struct.View<{
46
+ readonly _tag: Schema.tag<"Occurrence">;
47
+ eventId: Schema.String;
48
+ name: Schema.Literal<E["name"]>;
49
+ timestamp: Schema.DateTimeUtc;
50
+ data: E["payload"];
51
+ cursor: Schema.NullOr<Schema.String>;
52
+ }, "Type", E["payload"] extends {
53
+ readonly "~type.optionality": "optional";
54
+ } ? "data" : never, E["payload"] extends {
55
+ readonly "~type.mutability": "mutable";
56
+ } ? "data" : never>>;
@@ -0,0 +1,102 @@
1
+ import { principalOf } from "../contract/caller.mjs";
2
+ import { Refusal } from "../contract/errors.mjs";
3
+ import { declared } from "../contract/status.mjs";
4
+ import { assertDistinctTags } from "../contract/declared.mjs";
5
+ import { OccurrenceOf } from "../contract/occurrence.mjs";
6
+ import { SubscriptionError } from "../Event.mjs";
7
+ import { webhookSubscriber } from "../contract/webhooks.mjs";
8
+ import { PushedOf, SubscriptionOf, queuedPush } from "../contract/push.mjs";
9
+ import { Subscribed, WebhookCancellationOf, WebhookSubscriptionOf } from "../contract/subscribe.mjs";
10
+ import { Effect, Schema, Stream } from "effect";
11
+ import { HttpServerRequest } from "effect/http";
12
+ import { HttpApiEndpoint, HttpApiSchema, OpenApi } from "effect/http-api";
13
+ // src/http/events.ts
14
+ /**
15
+ * One poll's answer: the occurrences since the cursor, and when to ask again, sent as
16
+ * `nextPollMs`, its milliseconds.
17
+ */
18
+ const pageOf = (event) => Schema.Struct({
19
+ events: Schema.Array(OccurrenceOf(event)),
20
+ cursor: Schema.NullOr(Schema.String),
21
+ truncated: Schema.Boolean,
22
+ hasMore: Schema.Boolean,
23
+ nextPoll: Schema.DurationFromMillis
24
+ }).pipe(Schema.encodeKeys({ nextPoll: "nextPollMs" }));
25
+ /**
26
+ * What a subscription's event stream sends, each as one SSE `message`: a `Checked` with the
27
+ * position it starts from, or a `Gap` where that was no longer served, then each occurrence, a
28
+ * `Gap` where some were missed, a `Checked` every 30 seconds, and a `Failed` where reading
29
+ * failed and resumes. A later refusal ends it, as Effect's stream failure event.
30
+ */
31
+ const sentOf = (event) => HttpApiSchema.StreamSse({
32
+ data: Schema.toCodecJson(PushedOf(event)),
33
+ error: Refusal
34
+ });
35
+ /** What a served subscription's event stream sends: each element's JSON, as `sentOf` encodes it. */
36
+ const servedSent = HttpApiSchema.StreamSse({
37
+ data: Schema.Json,
38
+ error: Refusal
39
+ });
40
+ /**
41
+ * The route of one event type at `path`, its poll answered as JSON and its stream as SSE, as
42
+ * `sent` declares it.
43
+ */
44
+ const eventEndpointOf = (event, path, errors, sent = sentOf(event)) => HttpApiEndpoint.post(event.name, path, {
45
+ payload: SubscriptionOf(event).pipe(HttpApiSchema.asJson()),
46
+ success: [pageOf(event), sent],
47
+ error: errors.map(declared)
48
+ }).annotate(OpenApi.Description, event.description);
49
+ /**
50
+ * The webhook routes of one protected event type at `path`: `/subscribe`, which starts or
51
+ * refreshes a subscription and answers its grant, and `/unsubscribe`, which ends it.
52
+ */
53
+ const webhookEndpointsOf = (event, path, errors) => {
54
+ const withWebhooks = [...errors, SubscriptionError];
55
+ assertDistinctTags(`ActionHttp binding of event "${event.name}"'s webhook routes`, withWebhooks);
56
+ const error = withWebhooks.map(declared);
57
+ return [HttpApiEndpoint.post(`${event.name}/subscribe`, `${path}/subscribe`, {
58
+ payload: WebhookSubscriptionOf(event).pipe(HttpApiSchema.asJson()),
59
+ success: Subscribed,
60
+ error
61
+ }).annotate(OpenApi.Description, `Deliver ${event.name} to a webhook, or refresh its grant.`), HttpApiEndpoint.post(`${event.name}/unsubscribe`, `${path}/unsubscribe`, {
62
+ payload: WebhookCancellationOf(event).pipe(HttpApiSchema.asJson()),
63
+ success: HttpApiSchema.NoContent,
64
+ error
65
+ }).annotate(OpenApi.Description, `Stop delivering ${event.name} to a webhook.`)];
66
+ };
67
+ /**
68
+ * The handlers of `feed`'s webhook routes, through `webhooks` where the host provided
69
+ * `Webhook.layer`; without it, both fail with `Unsupported`.
70
+ */
71
+ const webhookAnswers = (feed, authentication, webhooks) => {
72
+ const served = webhookSubscriber(feed, authentication, webhooks);
73
+ return {
74
+ subscribe: ({ payload }) => Effect.flatMap(served, ({ webhooks: service, ...subscriber }) => service.subscribe({
75
+ ...payload,
76
+ ...subscriber
77
+ })),
78
+ unsubscribe: ({ payload }) => Effect.flatMap(served, ({ webhooks: service, feed: subscribed, principal }) => service.unsubscribe({
79
+ ...payload,
80
+ feed: subscribed,
81
+ principal
82
+ }))
83
+ };
84
+ };
85
+ /**
86
+ * Answers a subscription's request to `feed`: a page, or, where it accepts `text/event-stream`,
87
+ * the subscription's stream, each element its JSON. Both admit the subscriber first, so a refusal
88
+ * is the route's error; `authentication`'s principal names a poll's subscriber, for its
89
+ * `subscription` hook.
90
+ */
91
+ const answering = (feed, authentication) => {
92
+ const encode = Schema.encodeEffect(Schema.toCodecJson(PushedOf(feed.event)));
93
+ return ({ payload: { input, cursor } }) => Effect.gen(function* () {
94
+ if (((yield* HttpServerRequest.HttpServerRequest).headers["accept"] ?? "").includes("text/event-stream")) {
95
+ const open = yield* feed.stream(input);
96
+ return Stream.fromQueue(yield* queuedPush(open, cursor, encode));
97
+ }
98
+ const subscriber = yield* principalOf(feed.event, authentication);
99
+ return yield* feed.poll(input, cursor, { subscriber });
100
+ });
101
+ };
102
+ export { answering, eventEndpointOf, pageOf, sentOf, servedSent, webhookAnswers, webhookEndpointsOf };
@@ -0,0 +1,36 @@
1
+ import { EventImplementation } from "../contract/feed.mjs";
2
+ import { Any } from "../Event.mjs";
3
+ import { Implementation } from "../contract/implementation.mjs";
4
+ import { Any as Any$1 } from "../Action.mjs";
5
+ import { AnyHttp } from "./client.mjs";
6
+ import { HttpClient } from "effect/http";
7
+ type Contract<H extends AnyHttp> = H["actions"][number] | H["events"][number];
8
+ /** A call's or a subscription's decoded input, beside its contract, one of `C`. */
9
+ /** What the contracts `C` require per request, by name: nothing. */
10
+ type Nothing<C extends Any$1 | Any> = { readonly [K in C["name"]]: never; };
11
+ /**
12
+ * What `placed` returns: an implementation of the binding's actions where it has some, and one of
13
+ * its event types where it has some, which require nothing.
14
+ */
15
+ export type Placed<H extends AnyHttp> = ReadonlyArray<Implementation<H["actions"][number], Nothing<H["actions"][number]>, never, never> | EventImplementation<H["events"][number], Nothing<H["events"][number]>, never, never>>;
16
+ /**
17
+ * The actions and event types of `binding` served where another server serves them, as a
18
+ * surface's implementations: each call and subscription sent on over `binding` with the client
19
+ * `client` answers for its decoded input and its contract, one of the binding's, carrying the
20
+ * caller's headers, and its answer, its errors and its occurrences passed back as they are. The
21
+ * surface serving them authenticates a protected one as its own and authorizes every caller; the
22
+ * server reached authenticates and authorizes again.
23
+ *
24
+ * **Gotchas**
25
+ *
26
+ * - The caller's credential is verified twice, by the surface serving these and by the server reached, so both are given the verifier: a credential expiring between the two is refused by the second.
27
+ * - The server reached builds its own verifier: one caching signing keys in memory fetches them in each of its instances, a Durable Object's again after each eviction.
28
+ * - Every header of the caller's request is sent on but its connection's, its body's, what it accepts and an MCP request's routing headers (`mcp-*`): one that must not reach the server reached is removed per contract by the client `client` returns, `HttpClient.mapRequest(HttpClientRequest.removeHeader("cookie"))`, which runs after them.
29
+ * - A header `client`'s client sets replaces the caller's of that name, as it is set after them: a service's own `authorization` is sent in place of the caller's.
30
+ * - A call with no HTTP request, as through `Action.client` in process or an MCP stdio surface, sends no header on: a protected one reaches the server reached with no credential, which refuses it with `Unauthenticated`.
31
+ * - An error the server reached declares beyond the action's own and the built-in ones, its binding's `error` or its verifier's, is a defect where it is passed back, answered as one.
32
+ * - A server reached that does not serve one of them, its route a 404, is a defect naming it, with the fix: a declared error whose status is 404 is passed back as it is.
33
+ * - A refusal of a subscription by the server reached comes after the surface admitted it, so it ends the subscription's stream where the surface's own would have answered its 401 or 403.
34
+ */
35
+ export declare const placed: <const H extends AnyHttp>(binding: H, client: (input: Contract<H>["input"]["Type"], contract: Contract<H>) => HttpClient.HttpClient) => Placed<H>;
36
+ export {};