@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,39 @@
1
+ import { Checked, Gap } from "./occurrence.mjs";
2
+ import { Duration, Effect, Option, Stream } from "effect";
3
+ // src/contract/pages.ts
4
+ /** The shortest wait between polls, whatever a source or a server asks for. */
5
+ const pollFloor = Duration.seconds(1);
6
+ /** What a feed's stream yields of one read: its gap, its items, and where it ends. */
7
+ const fedOf = ({ items, truncated, from, to }) => [
8
+ ...truncated ? [Gap.make({ cursor: items.length === 0 ? to : from })] : [],
9
+ ...items,
10
+ Checked.make({ cursor: to })
11
+ ];
12
+ /**
13
+ * Reads from `cursor` again and again, as long as the stream is read: what each page yields, then
14
+ * a wait of its `nextPoll`, at least `pollFloor`, cut short by `woken`, or none where it has more;
15
+ * where `polls` is `false`, a wait for `woken` alone.
16
+ */
17
+ const pagedFrom = (read, cursor, woken, polls = true) => Stream.paginate({
18
+ cursor,
19
+ delay: Duration.zero
20
+ }, (state) => {
21
+ const next = Effect.map(Effect.suspend(() => read(state.cursor)), (page) => [page.fed, Option.some({
22
+ cursor: page.cursor ?? state.cursor,
23
+ delay: page.hasMore ? Duration.zero : Duration.max(page.nextPoll, pollFloor)
24
+ })]);
25
+ const sleep = Effect.sleep(state.delay);
26
+ if (woken === void 0 || Duration.isZero(state.delay)) return Effect.andThen(sleep, next);
27
+ return Effect.andThen(polls ? Effect.race(sleep, woken) : woken, next);
28
+ });
29
+ /** Polls from `cursor` as `pagedFrom` reads: each page as its gap, its occurrences, its end. */
30
+ const paged = (poll, cursor) => pagedFrom((at) => Effect.map(poll(at), (page) => ({
31
+ ...page,
32
+ fed: fedOf({
33
+ items: page.events,
34
+ truncated: page.truncated,
35
+ from: at,
36
+ to: page.cursor ?? at
37
+ })
38
+ })), cursor);
39
+ export { fedOf, paged, pagedFrom };
@@ -0,0 +1,26 @@
1
+ import { Codec } from "./codec.mjs";
2
+ import { ErasedValue } from "./implementation.mjs";
3
+ import { Data, Effect, Scope, Stream } from "effect";
4
+ /** Where a call's progress goes: each value reported, checked by its schema, is given to `progress`. */
5
+ export interface Reporting {
6
+ readonly progress: (value: ErasedValue) => Effect.Effect<void>;
7
+ }
8
+ /** The second argument of a handler of `A`, where `A` declares `progress`: what it reports by. */
9
+ export type ReportingOf<A extends {
10
+ readonly progress: Codec | undefined;
11
+ }> = A["progress"] extends undefined ? [] : [reporting: {
12
+ /** Reports `value` to the caller, never waiting for it: see `Action.Options.progress`. */
13
+ readonly progress: (value: NonNullable<A["progress"]>["Type"]) => Effect.Effect<void>;
14
+ }];
15
+ /** What a call of an action reporting progress `P` takes after its input. */
16
+ export interface CallOptions<P> {
17
+ /**
18
+ * Run with each progress value the handler reports, in order, before the call answers; which
19
+ * values reach it, and what its failure does, are on `Action.Options.progress`.
20
+ */
21
+ readonly onProgress?: ((progress: P) => Effect.Effect<void>) | undefined;
22
+ }
23
+ /** What a call of `A` takes after its input, where `A` declares `progress`. */
24
+ export type ProgressArgs<A extends {
25
+ readonly progress: Codec | undefined;
26
+ }> = A["progress"] extends undefined ? [] : [options?: CallOptions<NonNullable<A["progress"]>["Type"]>];
@@ -0,0 +1,72 @@
1
+ import { Cause, Data, Effect, Exit, Fiber, Option, Queue, Schema, Stream } from "effect";
2
+ // src/contract/progress.ts
3
+ const capacity = 16;
4
+ /**
5
+ * What a handler reports by, where `progress` is declared: each value checked by the schema, one
6
+ * breaking it a defect, and handed to `sink` from a buffer dropping the oldest, so the handler
7
+ * never waits. Without a `sink`, values are checked and dropped.
8
+ */
9
+ const buffering = (progress, sink) => {
10
+ const check = Schema.decodeUnknownEffect(Schema.toType(progress));
11
+ const checked = (value) => Effect.orDie(check(value));
12
+ if (sink === void 0) return Effect.succeed({
13
+ reporting: { progress: (value) => Effect.asVoid(checked(value)) },
14
+ drained: Effect.void
15
+ });
16
+ return Effect.gen(function* () {
17
+ const buffered = yield* Queue.sliding(capacity);
18
+ const forwarding = yield* Effect.forkScoped(Stream.runForEach(Stream.fromQueue(buffered), sink.progress));
19
+ return {
20
+ reporting: { progress: (value) => Effect.flatMap(checked(value), (each) => Effect.asVoid(Queue.offer(buffered, each))) },
21
+ drained: Effect.andThen(Queue.end(buffered), Fiber.join(forwarding))
22
+ };
23
+ });
24
+ };
25
+ /** Whether what is buffered still reaches the caller after `cause` ends a call. */
26
+ const drainsAfter = (cause) => !Cause.hasInterruptsOnly(cause);
27
+ /**
28
+ * Runs `handle` with what it reports by, as `buffering` makes it; what is buffered when it
29
+ * succeeds or fails reaches `sink` before its answer.
30
+ */
31
+ const reportedTo = (progress, sink, handle) => {
32
+ if (progress === void 0) return handle(void 0);
33
+ return Effect.gen(function* () {
34
+ const { reporting, drained } = yield* buffering(progress, sink);
35
+ const exit = yield* Effect.exit(handle(reporting));
36
+ if (Exit.isSuccess(exit) || drainsAfter(exit.cause)) yield* drained;
37
+ return yield* exit;
38
+ });
39
+ };
40
+ /**
41
+ * Runs the stream `handle` gives with what it reports by, as `buffering` makes it, in the
42
+ * stream's scope; what is buffered when it ends or fails reaches `sink` before its end.
43
+ */
44
+ const streamReportedTo = (progress, sink, handle) => {
45
+ if (progress === void 0) return handle(void 0);
46
+ return Stream.unwrap(Effect.map(buffering(progress, sink), ({ reporting, drained }) => handle(reporting).pipe(Stream.onEnd(drained), Stream.catchCause((cause) => drainsAfter(cause) ? Stream.unwrap(Effect.as(drained, Stream.failCause(cause))) : Stream.failCause(cause)))));
47
+ };
48
+ const shown = (onProgress) => (value) => Effect.catchCauseIf(Effect.suspend(() => onProgress(value)), (cause) => !Cause.hasInterruptsOnly(cause), (cause) => Effect.logWarning("onProgress failed", cause));
49
+ /** What a caller in process hands a call for `onProgress`, as `Action.client` and the toolkit do. */
50
+ const progressSink = (onProgress) => onProgress === void 0 ? void 0 : { progress: shown(onProgress) };
51
+ const Progressed = Data.taggedEnum();
52
+ const endedEarly = Effect.die(/* @__PURE__ */ new Error("An answer streamed with its progress ended without its final element"));
53
+ /**
54
+ * A client's answer from a call's `stream`: each `Progress` value handed to `onProgress`, where
55
+ * the call gave one, then the `Final` value. A stream ending without one is a defect.
56
+ */
57
+ const answerFrom = (stream, onProgress) => {
58
+ const report = progressSink(onProgress)?.progress ?? (() => Effect.void);
59
+ return Effect.flatMap(Stream.runFoldEffect(stream, () => Option.none(), (final, element) => Progressed.$is("Final")(element) ? Effect.succeedSome(element.value) : Effect.as(report(element.value), final)), Option.match({
60
+ onNone: () => endedEarly,
61
+ onSome: Effect.succeed
62
+ }));
63
+ };
64
+ /**
65
+ * A client's elements from a call's `stream` of an action answering a stream: each `Progress`
66
+ * value handed to `onProgress`, where the call gave one, and each `Element` value streamed.
67
+ */
68
+ const elementsFrom = (stream, onProgress) => {
69
+ const report = progressSink(onProgress)?.progress ?? (() => Effect.void);
70
+ return Stream.flatMap(stream, (sent) => Progressed.$is("Progress")(sent) ? Stream.fromEffectDrain(report(sent.value)) : Stream.make(sent.value));
71
+ };
72
+ export { Progressed, answerFrom, elementsFrom, progressSink, reportedTo, streamReportedTo };
@@ -0,0 +1,20 @@
1
+ import { Codec } from "./codec.mjs";
2
+ import "./errors.mjs";
3
+ import { OccurrenceSchema } from "./occurrence.mjs";
4
+ import "./pages.mjs";
5
+ import { Any } from "../Event.mjs";
6
+ import { Cause, Effect, Queue, Schema, Stream } from "effect";
7
+ /** What a push subscription of `event` sends, as each surface's stream encodes it. */
8
+ export declare const PushedOf: <E extends Any>(event: E) => Schema.Union<readonly [OccurrenceSchema<E>, Schema.TaggedStruct<"Gap", {
9
+ readonly cursor: Schema.NullOr<Schema.String>;
10
+ }>, Schema.TaggedStruct<"Checked", {
11
+ readonly cursor: Schema.NullOr<Schema.String>;
12
+ }>, Schema.TaggedStruct<"Failed", {}>]>;
13
+ /**
14
+ * What a subscription's request carries: the event's input, and the cursor it resumes from,
15
+ * `null` where it is omitted.
16
+ */
17
+ export declare const SubscriptionOf: <E extends Any>(event: E) => Schema.Struct<{
18
+ readonly input: Codec;
19
+ readonly cursor: Schema.withDecodingDefaultKey<Schema.NullOr<Schema.String>, never>;
20
+ }>;
@@ -0,0 +1,96 @@
1
+ import { Refusal } from "./errors.mjs";
2
+ import { reported } from "./reported.mjs";
3
+ import { Checked, Gap, OccurrenceOf } from "./occurrence.mjs";
4
+ import { defaultNextPoll } from "./feed.mjs";
5
+ import { Cause, Duration, Effect, Latch, Match, Queue, Schema, Stream } from "effect";
6
+ // src/contract/push.ts
7
+ /**
8
+ * A push subscription read on through a failure no refusal explains, such as an upstream's
9
+ * outage: its cause is logged and reported, and reading resumes from the position reached.
10
+ */
11
+ const Failed = Schema.TaggedStruct("Failed", {});
12
+ /** What a push subscription of `event` sends, as each surface's stream encodes it. */
13
+ const PushedOf = (event) => Schema.Union([
14
+ OccurrenceOf(event),
15
+ Gap,
16
+ Checked,
17
+ Failed
18
+ ]);
19
+ const subscriptionWith = (input) => Schema.Struct({
20
+ input,
21
+ cursor: Schema.NullOr(Schema.String).annotate({ description: "Where to resume: a cursor an occurrence or a page carried; null or omitted, from now." }).pipe(Schema.withDecodingDefaultKey(Effect.succeed(null)))
22
+ });
23
+ /**
24
+ * What a subscription's request carries: the event's input, and the cursor it resumes from,
25
+ * `null` where it is omitted.
26
+ */
27
+ const SubscriptionOf = (event) => subscriptionWith(event.input);
28
+ /** A subscription's request, its input left as JSON for its event's schema to decode. */
29
+ const Subscription = subscriptionWith(Schema.Json);
30
+ const queued = 16;
31
+ const heartbeatEvery = Duration.seconds(30);
32
+ const isFailed = Schema.is(Failed);
33
+ /** Each element of a push stream of `surface`'s event `name` but `Failed`, which is logged. */
34
+ const readOn = (surface, name) => (sent) => sent.pipe(Stream.mapEffect((element) => isFailed(element) ? Effect.as(Effect.logWarning(`${surface} event "${name}" failed to read, and resumes`), []) : Effect.succeed([element])), Stream.flattenIterable);
35
+ const isRefused = Schema.is(Refusal);
36
+ /** The refusal that `cause` fails with, unless it also holds a defect. */
37
+ const refusalIn = (cause) => Cause.hasDies(cause) ? void 0 : cause.reasons.flatMap((reason) => Cause.isFailReason(reason) && isRefused(reason.error) ? [reason.error] : [])[0];
38
+ /**
39
+ * A poll of `name` that answers a failure no refusal explains, reported, as a page of none from
40
+ * the cursor asked, read again after the poll interval: where no network carries a 500 for a
41
+ * client to retry, as in process, it reads on as `pushed` does.
42
+ */
43
+ const polledOn = (surface, name) => (poll) => (cursor) => Effect.catchCauseIf(poll(cursor), (cause) => !Cause.hasInterruptsOnly(cause) && refusalIn(cause) === void 0, (cause) => Effect.as(Effect.andThen(reported(cause), Effect.logWarning(`${surface} event "${name}" failed to read, and resumes`)), {
44
+ events: [],
45
+ cursor,
46
+ truncated: false,
47
+ hasMore: false,
48
+ nextPoll: defaultNextPoll
49
+ }));
50
+ /**
51
+ * One push subscription of a feed `open` reads from a cursor, starting at `cursor`, each thing it
52
+ * sends given to `deliver`. A refusal, an interruption, or, unless `resumesUnconfirmed`, a failure
53
+ * before it is confirmed ends it; any other failure of a read or of its delivery is reported,
54
+ * sent as `Failed`, and reading resumes from the position delivered after the poll interval.
55
+ */
56
+ const pushed = (open, cursor, deliver, { resumesUnconfirmed = false, inOrder = false } = {}) => Effect.gen(function* () {
57
+ const confirmed = yield* Latch.make();
58
+ let reached = cursor;
59
+ let active = false;
60
+ let wait = Duration.zero;
61
+ const movedTo = (at) => Effect.suspend(() => {
62
+ active = true;
63
+ reached = at;
64
+ return confirmed.open;
65
+ });
66
+ const sent = (element, at) => Effect.andThen(deliver(element), movedTo(at));
67
+ const delivered = (fed) => Match.valueTags(fed, {
68
+ Gap: (gap) => sent(gap, gap.cursor),
69
+ Checked: (checked) => {
70
+ if (!active) return sent(checked, checked.cursor);
71
+ if (checked.cursor === null || checked.cursor === reached) return Effect.void;
72
+ return inOrder ? sent(checked, checked.cursor) : movedTo(checked.cursor);
73
+ },
74
+ Occurrence: (occurrence) => Effect.andThen(active ? Effect.void : sent(Checked.make({ cursor: reached }), reached), Effect.suspend(() => sent(occurrence, occurrence.cursor ?? reached)))
75
+ });
76
+ const resumes = (cause) => (active || resumesUnconfirmed) && !Cause.hasInterruptsOnly(cause) && refusalIn(cause) === void 0;
77
+ const waiting = Effect.sync(() => {
78
+ wait = defaultNextPoll;
79
+ });
80
+ const once = Stream.suspend(() => Duration.isZero(wait) ? open(reached) : Stream.unwrap(Effect.as(Effect.sleep(wait), open(reached)))).pipe(Stream.runForEach(delivered), Effect.catchCauseIf(resumes, (cause) => Effect.andThen(reported(cause), deliver(Failed.make({})))), Effect.andThen(waiting));
81
+ const heartbeat = Effect.andThen(confirmed.await, Effect.forever(Effect.andThen(Effect.sleep(heartbeatEvery), () => Effect.suspend(() => deliver(Checked.make({ cursor: reached }))))));
82
+ return yield* inOrder ? Effect.forever(once) : Effect.raceFirst(Effect.forever(once), heartbeat);
83
+ });
84
+ /**
85
+ * A push subscription for a surface's streamed response: read into a bounded queue, each element
86
+ * as `encode` gives it, forked in the current scope.
87
+ */
88
+ const queuedPush = (open, cursor, encode, { ending = (run) => run, inOrder = false, capacity = queued } = {}) => Effect.gen(function* () {
89
+ const queue = yield* Queue.bounded(capacity);
90
+ yield* pushed(open, cursor, (sent) => Effect.flatMap(encode(sent), (element) => Queue.offer(queue, element)), {
91
+ resumesUnconfirmed: true,
92
+ inOrder
93
+ }).pipe(ending, Queue.into(queue), Effect.forkScoped({ startImmediately: true }));
94
+ return queue;
95
+ });
96
+ export { Failed, PushedOf, Subscription, SubscriptionOf, polledOn, pushed, queuedPush, readOn, refusalIn };
@@ -0,0 +1,29 @@
1
+ import "./errors.mjs";
2
+ import "./pages.mjs";
3
+ import "../Event.mjs";
4
+ import { SigningSecret } from "./callback.mjs";
5
+ import { Context, Effect, Schema, Scope } from "effect";
6
+ /** A POST a receiver verified, its JSON body: an occurrence, or an envelope. */
7
+ export interface Delivered {
8
+ readonly [key: string]: Schema.Json;
9
+ }
10
+ /**
11
+ * One watch's place at a receiver: the id its delivery URL names in `watchParameter`, and the
12
+ * secret to subscribe with.
13
+ */
14
+ export interface Registered {
15
+ readonly id: string;
16
+ readonly secret: SigningSecret;
17
+ }
18
+ declare const Receiving_base: Context.ServiceClass<Receiving, "effect-actions/Receiving", {
19
+ readonly path: `/${string}`;
20
+ readonly register: (deliver: (body: Delivered) => boolean) => Effect.Effect<Registered, never, Scope.Scope>;
21
+ }>;
22
+ /**
23
+ * Where a watch by webhook receives its deliveries in this process: `Webhook.layerReceiver`
24
+ * provides it beside its route at `path`, and `register` keeps a watch's place for its scope,
25
+ * handing each verified body to `deliver`, which answers `false` while the watch has no room
26
+ * for it.
27
+ */
28
+ export declare class Receiving extends Receiving_base {}
29
+ export {};
@@ -0,0 +1,42 @@
1
+ import { Forbidden } from "./errors.mjs";
2
+ import { Gap, untaggedOccurrenceOf } from "./occurrence.mjs";
3
+ import { reasonOf, refusalOf } from "./codes.mjs";
4
+ import { Context, Effect, Option, Schema, Struct } from "effect";
5
+ // src/contract/receiving.ts
6
+ /** The query parameter of a watch's delivery URL naming its place at the receiver. */
7
+ const watchParameter = "watch";
8
+ /**
9
+ * Where a watch by webhook receives its deliveries in this process: `Webhook.layerReceiver`
10
+ * provides it beside its route at `path`, and `register` keeps a watch's place for its scope,
11
+ * handing each verified body to `deliver`, which answers `false` while the watch has no room
12
+ * for it.
13
+ */
14
+ var Receiving = class extends Context.Service()("effect-actions/Receiving") {};
15
+ const Terminated = Schema.Struct({ error: Schema.Struct({
16
+ code: Schema.optional(Schema.Int),
17
+ message: Schema.String,
18
+ data: Schema.optional(Schema.Json)
19
+ }) });
20
+ const decodeTerminated = Schema.decodeUnknownOption(Terminated);
21
+ const decodeGap = Schema.decodeUnknownEffect(Schema.toCodecJson(Gap.mapFields(Struct.omit(["_tag"]))));
22
+ const ended = new Forbidden({ message: "The subscription was ended." });
23
+ /**
24
+ * What a watch of `event` reads from one delivered body: its occurrence, or the gap an envelope
25
+ * reports; a `terminated` envelope fails with the reason its error's code names, its refusal for
26
+ * any other, an envelope of another type is skipped, and a body that does not decode fails with a
27
+ * `SchemaError`.
28
+ */
29
+ const webhookFed = (event) => {
30
+ const occurrenceOf = untaggedOccurrenceOf(event);
31
+ return (body) => {
32
+ const { type } = body;
33
+ if (type === void 0) return Effect.map(occurrenceOf(body), (occurrence) => [occurrence]);
34
+ if (type === "gap") return Effect.map(decodeGap(body), (fields) => [Gap.make(fields)]);
35
+ if (type === "terminated") return Effect.fail(Option.match(decodeTerminated(body), {
36
+ onNone: () => ended,
37
+ onSome: ({ error }) => reasonOf(event.name, error) ?? refusalOf(error)
38
+ }));
39
+ return Effect.succeed([]);
40
+ };
41
+ };
42
+ export { Receiving, watchParameter, webhookFed };
@@ -0,0 +1,12 @@
1
+ // src/contract/recent.ts
2
+ const makeRecent = (size) => {
3
+ const ids = /* @__PURE__ */ new Set();
4
+ return {
5
+ has: (id) => ids.has(id),
6
+ add: (id) => {
7
+ ids.add(id);
8
+ if (ids.size > size) ids.delete(ids.values().next().value ?? id);
9
+ }
10
+ };
11
+ };
12
+ export { makeRecent };
@@ -0,0 +1,16 @@
1
+ import { Effect, ErrorReporter } from "effect";
2
+ // src/contract/reported.ts
3
+ /** Logs `cause` and gives it to the request's `ErrorReporter`s, as a failed call's is. */
4
+ const reported = (cause) => Effect.andThen(Effect.logError(cause), ErrorReporter.report(cause));
5
+ var InternalError = class extends Error {
6
+ [ErrorReporter.ignore] = true;
7
+ constructor() {
8
+ super("Internal server error");
9
+ }
10
+ };
11
+ /**
12
+ * `cause` `reported`, then a defect that says no more than "Internal server error", reported no
13
+ * further: what a surface sends where it would otherwise encode a defect's details.
14
+ */
15
+ const concealed = (cause) => Effect.andThen(reported(cause), Effect.die(new InternalError()));
16
+ export { concealed, reported };
@@ -0,0 +1,63 @@
1
+ import { Anyone, assertAuthentication } from "./caller.mjs";
2
+ import { assertDistinct, assertOnce, heldOnce } from "./names.mjs";
3
+ import { Implementation, assertHeld, markingOthers, notHeld, provideBuilders, select, servedBy, toList } from "./implementation.mjs";
4
+ import { eventsOf, feedBuilders, selectEvents } from "./feed.mjs";
5
+ import { selection } from "./listed.mjs";
6
+ // src/contract/serving.ts
7
+ /**
8
+ * What a surface without a binding serves of `served`: the actions and event types `actions`
9
+ * lists, each implementation holding those alone, or every one where it lists none.
10
+ */
11
+ const selectFrom = (served, actions) => {
12
+ const listed = selection(actions);
13
+ return {
14
+ apps: select(toList(served), listed.actions),
15
+ events: selectEvents(eventsOf(served), listed.events)
16
+ };
17
+ };
18
+ /**
19
+ * The event implementations among `served` a binding of `bound` serves, each holding the served
20
+ * event types alone: those `listed` lists, or else every bound one, refusing implementations none
21
+ * of whose event types it binds, as `servedBy` refuses such action implementations.
22
+ */
23
+ const eventsServedBy = (what, bound, served, listed) => {
24
+ const apps = eventsOf(served);
25
+ if (listed !== void 0) assertHeld(notHeld.byBinding, listed, bound);
26
+ const unbound = apps.filter((app) => !app.events.some((event) => bound.includes(event)));
27
+ if (listed === void 0 && unbound.length > 0 && unbound.length === apps.length) {
28
+ const names = new Set(bound.map(({ name }) => name));
29
+ throw new Error(`No event of these implementations is in this ${what}: ${markingOthers(unbound.flatMap((app) => app.events), names)} (pass an implementation of a bound event, or bind one of these)`);
30
+ }
31
+ const held = new Set(apps.flatMap((app) => app.events));
32
+ const selected = selectEvents(apps, listed ?? bound.filter((event) => held.has(event)));
33
+ assertOnce("served event", selected.flatMap((app) => app.events), heldOnce);
34
+ return selected;
35
+ };
36
+ /**
37
+ * What `ActionHttp.layer` and `ActionRpc.layer` serve, with the checks they share: the actions
38
+ * and event types `options.actions` lists, or every one of the binding's the implementations
39
+ * hold; `authentication` is the binding's where a served contract is protected.
40
+ */
41
+ const servingOf = (what, binding, served, options) => {
42
+ const listed = selection(options.actions);
43
+ const apps = servedBy(what, binding.actions, toList(served), listed.actions);
44
+ const actions = apps.flatMap((app) => app.actions);
45
+ const events = eventsServedBy(what, binding.events, served, listed.events);
46
+ const eventTypes = events.flatMap((app) => app.events);
47
+ assertAuthentication(actions, binding.authentication);
48
+ assertAuthentication(eventTypes, binding.authentication, "event");
49
+ assertDistinct("middleware", options.middleware ?? [], (key) => key.key, { fix: "list each middleware once" });
50
+ return {
51
+ apps,
52
+ actions,
53
+ events,
54
+ eventTypes,
55
+ authentication: [...actions, ...eventTypes].some((contract) => contract.caller !== Anyone) ? binding.authentication : void 0
56
+ };
57
+ };
58
+ /**
59
+ * Provide the builders of what `servingOf` selects, the action implementations' and the event
60
+ * implementations', to a surface's layer.
61
+ */
62
+ const provideServed = (served) => provideBuilders([...served.apps.map((app) => Implementation.layerOf(app)), ...feedBuilders(served.events)]);
63
+ export { provideServed, selectFrom, servingOf };
@@ -1,11 +1,19 @@
1
1
  import { SchemaAST } from "effect";
2
- //#region src/contract/status.ts
2
+ // src/contract/status.ts
3
3
  const httpApiStatusOf = SchemaAST.resolveAt("httpApiStatus");
4
+ const transientStatuses = /* @__PURE__ */ new Set([
5
+ 408,
6
+ 429,
7
+ 500,
8
+ 502,
9
+ 503,
10
+ 504
11
+ ]);
12
+ /** Whether a server answering `status` may answer otherwise once it recovers. */
13
+ const isTransientStatus = (status) => transientStatuses.has(status);
4
14
  const statusThroughSuspensions = (ast) => httpApiStatusOf(ast) ?? (SchemaAST.isSuspend(ast) ? statusThroughSuspensions(ast.thunk()) : void 0);
15
+ /** The status an endpoint answers `error` with: its own, through suspensions, 422 without one. */
16
+ const declaredStatus = (error) => statusThroughSuspensions(error.ast) ?? 422;
5
17
  /** One error schema, from `errorSchemas`, as an endpoint declares it: with its status, 422 without one. */
6
- const declared = (error) => {
7
- const status = statusThroughSuspensions(error.ast);
8
- return status === void 0 ? error.annotate({ httpApiStatus: 422 }) : httpApiStatusOf(error.ast) === void 0 ? error.annotate({ httpApiStatus: status }) : error;
9
- };
10
- //#endregion
11
- export { declared, httpApiStatusOf };
18
+ const declared = (error) => httpApiStatusOf(error.ast) === void 0 ? error.annotate({ httpApiStatus: declaredStatus(error) }) : error;
19
+ export { declared, declaredStatus, httpApiStatusOf, isTransientStatus };
@@ -0,0 +1,34 @@
1
+ import { Codec, CodecOf, Fields } from "./codec.mjs";
2
+ import { Effect, Schema, Stream } from "effect";
3
+ declare const TypeId = "~effect-actions/Stream";
4
+ /** The success of an action answering a stream: a schema of `Stream`s, holding its element's. */
5
+ export interface StreamOf<Element extends Codec> extends Schema.declare<Stream.Stream<Element["Type"]>> {
6
+ readonly Rebuild: StreamOf<Element>;
7
+ readonly [TypeId]: typeof TypeId;
8
+ /** The schema of each element. */
9
+ readonly element: Element;
10
+ }
11
+ /**
12
+ * A success that is a stream of `element`, a schema or struct fields: the handler returns a
13
+ * `Stream` of them, which may fail with the action's errors, and every client's call returns
14
+ * one. How each surface carries it is on `ActionHttp.layer`, `ActionRpc.make`,
15
+ * `ActionMcp.layerHttp`, `ActionToolkit.make` and `ActionCli.command`.
16
+ * Guide: `docs/Action-streams.md` in this package; in an application: build-events' `tailLog`.
17
+ *
18
+ * **Gotchas**
19
+ *
20
+ * - A tool answers one result, so MCP and a toolkit collect the whole stream before they answer, its elements as one array: a stream that never ends never answers there.
21
+ * - An element breaking its schema is the call's defect, as an answer breaking `success` is.
22
+ * - With `progress`, values and elements are each in their order, not against each other: a value may reach `onProgress` after an element yielded after it.
23
+ */
24
+ declare const Stream$1: <const E extends Codec | Fields>(element: E) => StreamOf<CodecOf<E>>;
25
+ /** What a handler or a call of an action with success `S` answers: a stream, or one value. */
26
+ export type Answer<S extends Codec, E, R> = [S] extends [never] ? Effect.Effect<never, E, R> : [S] extends [StreamOf<infer Element>] ? Stream.Stream<Element["Type"], E, R> : Effect.Effect<S["Type"], E, R>;
27
+ /**
28
+ * The success a tool answers for an action with success `S`: an array of the elements of one
29
+ * answering a stream, collected whole, or `S`.
30
+ */
31
+ export type ToolSuccess<S extends Codec> = [S] extends [StreamOf<infer Element>] ? Schema.$Array<Element> : S;
32
+ /** What a call of an action with success `S` gives one at a time: an element, or its answer. */
33
+ export type Each<S extends Codec> = [S] extends [StreamOf<infer Element>] ? Element["Type"] : S["Type"];
34
+ export { Stream$1 as Stream };
@@ -0,0 +1,31 @@
1
+ import { codecOf } from "./codec.mjs";
2
+ import { Predicate, Schema, Stream } from "effect";
3
+ // src/contract/stream.ts
4
+ const TypeId = "~effect-actions/Stream";
5
+ const streams = Schema.declare(Stream.isStream);
6
+ /**
7
+ * A success that is a stream of `element`, a schema or struct fields: the handler returns a
8
+ * `Stream` of them, which may fail with the action's errors, and every client's call returns
9
+ * one. How each surface carries it is on `ActionHttp.layer`, `ActionRpc.make`,
10
+ * `ActionMcp.layerHttp`, `ActionToolkit.make` and `ActionCli.command`.
11
+ * Guide: `docs/Action-streams.md` in this package; in an application: build-events' `tailLog`.
12
+ *
13
+ * **Gotchas**
14
+ *
15
+ * - A tool answers one result, so MCP and a toolkit collect the whole stream before they answer, its elements as one array: a stream that never ends never answers there.
16
+ * - An element breaking its schema is the call's defect, as an answer breaking `success` is.
17
+ * - With `progress`, values and elements are each in their order, not against each other: a value may reach `onProgress` after an element yielded after it.
18
+ */
19
+ const Stream$1 = (element) => Schema.make(streams.ast, {
20
+ [TypeId]: TypeId,
21
+ element: codecOf(element)
22
+ });
23
+ const isStreamOf = (success) => Predicate.hasProperty(success, TypeId);
24
+ /** The element schema of an action answering a stream, `undefined` for any other action. */
25
+ const elementOf = (success) => isStreamOf(success) ? success.element : void 0;
26
+ /** The success a tool answers for an action with `success`, as `ToolSuccess` types it. */
27
+ const toolSuccessOf = (success) => {
28
+ const element = elementOf(success);
29
+ return element === void 0 ? success : Schema.Array(element);
30
+ };
31
+ export { Stream$1 as Stream, elementOf, toolSuccessOf };
@@ -0,0 +1,45 @@
1
+ import { Codec } from "./codec.mjs";
2
+ import { Any } from "../Event.mjs";
3
+ import { Schema } from "effect";
4
+ /** What a webhook subscribe to `event` takes: a subscription's input and cursor, and its fields. */
5
+ export declare const WebhookSubscriptionOf: <E extends Any>(event: E) => Schema.Struct<{
6
+ readonly input: Codec;
7
+ readonly cursor: Schema.withDecodingDefaultKey<Schema.NullOr<Schema.String>, never>;
8
+ delivery: Schema.Struct<{
9
+ readonly url: Schema.decodeTo<Schema.URL, Schema.String, never, never>;
10
+ readonly secret: Schema.decodeTo<Schema.Struct<{
11
+ readonly text: Schema.String;
12
+ readonly key: Schema.Uint8Array;
13
+ }>, Schema.String, never, never>;
14
+ }>;
15
+ ttlMs: Schema.optional<Schema.NullOr<Schema.Finite>>;
16
+ }>;
17
+ /** A webhook subscribe's request, its input decoded, as a surface reads it whatever its event. */
18
+ export type AnyWebhookSubscription = ReturnType<typeof WebhookSubscriptionOf<Any>>["Type"];
19
+ /** What a webhook unsubscribe from `event` takes: the input and callback it subscribed with. */
20
+ export declare const WebhookCancellationOf: <E extends Any>(event: E) => Schema.Struct<{
21
+ readonly input: Codec;
22
+ readonly delivery: Schema.Struct<{
23
+ readonly url: Schema.decodeTo<Schema.URL, Schema.String, never, never>;
24
+ }>;
25
+ }>;
26
+ /** A webhook unsubscribe's request, its input decoded, whatever its event. */
27
+ export type AnyWebhookCancellation = ReturnType<typeof WebhookCancellationOf<Any>>["Type"];
28
+ /**
29
+ * A webhook subscription's grant: its id, which each delivery's `X-MCP-Subscription-Id` carries,
30
+ * when to refresh it by, the position it is safe to resume from, and whether delivery starts
31
+ * later than the cursor asked.
32
+ */
33
+ export declare const Subscribed: Schema.Struct<{
34
+ readonly id: Schema.String;
35
+ readonly refreshBefore: Schema.NullOr<Schema.String>;
36
+ readonly cursor: Schema.NullOr<Schema.String>;
37
+ readonly truncated: Schema.Boolean;
38
+ readonly deliveryStatus: Schema.optional<Schema.Struct<{
39
+ readonly active: Schema.Boolean;
40
+ readonly lastDeliveryAt: Schema.NullOr<Schema.String>;
41
+ readonly lastError: Schema.NullOr<Schema.Literals<readonly ["connection_refused", "timeout", "tls_error", "http_4xx", "http_5xx", "challenge_failed"]>>;
42
+ readonly failedSince: Schema.optional<Schema.String>;
43
+ }>>;
44
+ }>;
45
+ export type Subscribed = typeof Subscribed.Type;
@@ -0,0 +1,50 @@
1
+ import { CallbackCategory } from "../Event.mjs";
2
+ import { CallbackUrl, WebhookSecret } from "./callback.mjs";
3
+ import { SubscriptionOf } from "./push.mjs";
4
+ import { Schema, Struct } from "effect";
5
+ // src/contract/subscribe.ts
6
+ /** Where a webhook subscription delivers: its callback, and the secret signing each POST. */
7
+ const WebhookCallback = Schema.Struct({
8
+ url: CallbackUrl,
9
+ secret: WebhookSecret
10
+ });
11
+ /** The callback a webhook unsubscribe names: its URL alone. */
12
+ const WebhookTarget = WebhookCallback.mapFields(Struct.pick(["url"]));
13
+ /**
14
+ * What a webhook subscribe asks beside the subscription it names: its callback, and the lifetime
15
+ * it suggests in milliseconds, `null` asking for none.
16
+ */
17
+ const webhookFields = {
18
+ delivery: WebhookCallback,
19
+ ttlMs: Schema.optional(Schema.NullOr(Schema.Finite)).annotate({ description: "The lifetime it suggests, in milliseconds; the answer's refreshBefore is the grant." })
20
+ };
21
+ /** What a webhook subscribe to `event` takes: a subscription's input and cursor, and its fields. */
22
+ const WebhookSubscriptionOf = (event) => SubscriptionOf(event).pipe(Schema.fieldsAssign(webhookFields));
23
+ /** What a webhook unsubscribe from `event` takes: the input and callback it subscribed with. */
24
+ const WebhookCancellationOf = (event) => Schema.Struct({
25
+ input: event.input,
26
+ delivery: WebhookTarget
27
+ });
28
+ /**
29
+ * How a webhook subscription's deliveries are going, as a refresh answers it: whether it is
30
+ * delivering, when it last delivered, and the category of its last failure, since when it fails.
31
+ */
32
+ const DeliveryStatus = Schema.Struct({
33
+ active: Schema.Boolean,
34
+ lastDeliveryAt: Schema.NullOr(Schema.String),
35
+ lastError: Schema.NullOr(CallbackCategory),
36
+ failedSince: Schema.optional(Schema.String)
37
+ });
38
+ /**
39
+ * A webhook subscription's grant: its id, which each delivery's `X-MCP-Subscription-Id` carries,
40
+ * when to refresh it by, the position it is safe to resume from, and whether delivery starts
41
+ * later than the cursor asked.
42
+ */
43
+ const Subscribed = Schema.Struct({
44
+ id: Schema.String,
45
+ refreshBefore: Schema.NullOr(Schema.String),
46
+ cursor: Schema.NullOr(Schema.String),
47
+ truncated: Schema.Boolean,
48
+ deliveryStatus: Schema.optional(DeliveryStatus)
49
+ });
50
+ export { DeliveryStatus, Subscribed, WebhookCallback, WebhookCancellationOf, WebhookSubscriptionOf, WebhookTarget, webhookFields };
@@ -0,0 +1,28 @@
1
+ import { isTransientStatus } from "./status.mjs";
2
+ import { readOn } from "./push.mjs";
3
+ import { Cause, Duration, Effect, Function, Match, Predicate, Stream } from "effect";
4
+ import { HttpClientError } from "effect/http";
5
+ // src/contract/transient.ts
6
+ /** The status a server answered with, where `error` is a failure to read its answer. */
7
+ const answeredStatus = ({ reason }) => Match.value(reason).pipe(Match.tag("StatusCodeError", "DecodeError", ({ response }) => response.status), Match.orElse(Function.constUndefined));
8
+ /** Whether an HTTP client's failure, by its `reason`, is one a reconnect may outlast. */
9
+ const isTransientReason = (reason) => Match.value(reason).pipe(Match.tag("TransportError", () => true), Match.tag("DecodeError", ({ response }) => response.status < 300 || isTransientStatus(response.status)), Match.tag("StatusCodeError", ({ response }) => isTransientStatus(response.status)), Match.orElse(() => false));
10
+ /** Whether a remote watch's failure is one a reconnect may outlast. */
11
+ const isTransient = Predicate.or(Cause.isTimeoutError, Predicate.compose(HttpClientError.isHttpClientError, ({ reason }) => isTransientReason(reason)));
12
+ const silentFor = Duration.seconds(60);
13
+ /**
14
+ * `request`, failing with a `TimeoutError`, which `isTransient` admits, when it is unanswered for
15
+ * as long as a stream may be silent, such as a server that holds its status back.
16
+ */
17
+ const failingWhenUnanswered = (request) => Effect.timeout(request, silentFor);
18
+ /** `stream`, failing with a `TimeoutError`, which `isTransient` admits, once it goes silent. */
19
+ const failingWhenSilent = (stream) => stream.pipe(Stream.timeoutOrElse({
20
+ duration: silentFor,
21
+ orElse: () => Stream.fail(new Cause.TimeoutError("The event stream went silent"))
22
+ }));
23
+ /**
24
+ * What a client reads from a push stream of `surface`'s event `name`: each element but `Failed`,
25
+ * which is logged, the server having read on; failing with a `TimeoutError` once it goes silent.
26
+ */
27
+ const receivedFrom = (surface, name) => (sent) => sent.pipe(failingWhenSilent, readOn(surface, name));
28
+ export { answeredStatus, failingWhenSilent, failingWhenUnanswered, isTransient, isTransientReason, receivedFrom };