@gjermundgaraba/effect-actions 0.20.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 (321) hide show
  1. package/README.md +19 -41
  2. package/dist/Action.d.mts +140 -46
  3. package/dist/Action.mjs +87 -42
  4. package/dist/ActionCli.d.mts +69 -33
  5. package/dist/ActionCli.mjs +51 -32
  6. package/dist/ActionHttp.d.mts +69 -35
  7. package/dist/ActionHttp.mjs +75 -23
  8. package/dist/ActionMcp.d.mts +56 -22
  9. package/dist/ActionMcp.mjs +94 -69
  10. package/dist/ActionRpc.d.mts +222 -55
  11. package/dist/ActionRpc.mjs +299 -46
  12. package/dist/ActionToolkit.d.mts +22 -5
  13. package/dist/ActionToolkit.mjs +36 -12
  14. package/dist/Authentication.d.mts +42 -10
  15. package/dist/Authentication.mjs +37 -30
  16. package/dist/Event.d.mts +370 -0
  17. package/dist/Event.mjs +103 -0
  18. package/dist/Testing.mjs +1 -1
  19. package/dist/Webhook.d.mts +146 -0
  20. package/dist/Webhook.mjs +72 -0
  21. package/dist/authentication/provider.d.mts +35 -10
  22. package/dist/authentication/provider.mjs +1 -1
  23. package/dist/authentication/refusal.mjs +2 -2
  24. package/dist/cli/commands.d.mts +9 -5
  25. package/dist/cli/commands.mjs +84 -33
  26. package/dist/contract/ast.mjs +4 -1
  27. package/dist/contract/authorizer.d.mts +8 -0
  28. package/dist/contract/authorizer.mjs +20 -0
  29. package/dist/contract/call.d.mts +9 -6
  30. package/dist/contract/call.mjs +48 -6
  31. package/dist/contract/callback.d.mts +7 -0
  32. package/dist/contract/callback.mjs +27 -0
  33. package/dist/contract/caller.d.mts +13 -2
  34. package/dist/contract/caller.mjs +22 -3
  35. package/dist/contract/canonical.mjs +10 -0
  36. package/dist/contract/codec.d.mts +9 -0
  37. package/dist/contract/codec.mjs +9 -0
  38. package/dist/contract/codes.mjs +81 -0
  39. package/dist/contract/declared.mjs +25 -9
  40. package/dist/contract/digest.mjs +6 -0
  41. package/dist/contract/errors.d.mts +9 -3
  42. package/dist/contract/errors.mjs +1 -1
  43. package/dist/contract/feed.d.mts +127 -0
  44. package/dist/contract/feed.mjs +249 -0
  45. package/dist/contract/hub.d.mts +94 -0
  46. package/dist/contract/hub.mjs +133 -0
  47. package/dist/contract/idempotency.d.mts +47 -0
  48. package/dist/contract/idempotency.mjs +99 -0
  49. package/dist/contract/implementation.d.mts +73 -47
  50. package/dist/contract/implementation.mjs +92 -58
  51. package/dist/contract/input-check.d.mts +15 -0
  52. package/dist/contract/input-check.mjs +16 -0
  53. package/dist/contract/lease.mjs +28 -0
  54. package/dist/contract/listed.d.mts +4 -0
  55. package/dist/contract/listed.mjs +22 -0
  56. package/dist/contract/log.d.mts +41 -0
  57. package/dist/contract/log.mjs +144 -0
  58. package/dist/contract/names.mjs +14 -4
  59. package/dist/contract/occurrence.d.mts +29 -0
  60. package/dist/contract/occurrence.mjs +45 -0
  61. package/dist/contract/pages.d.mts +13 -0
  62. package/dist/contract/pages.mjs +39 -0
  63. package/dist/contract/progress.d.mts +26 -0
  64. package/dist/contract/progress.mjs +72 -0
  65. package/dist/contract/push.d.mts +20 -0
  66. package/dist/contract/push.mjs +96 -0
  67. package/dist/contract/receiving.d.mts +29 -0
  68. package/dist/contract/receiving.mjs +42 -0
  69. package/dist/contract/recent.mjs +12 -0
  70. package/dist/contract/reported.mjs +16 -0
  71. package/dist/contract/serving.mjs +63 -0
  72. package/dist/contract/status.mjs +14 -5
  73. package/dist/contract/stream.d.mts +34 -0
  74. package/dist/contract/stream.mjs +31 -0
  75. package/dist/contract/subscribe.d.mts +45 -0
  76. package/dist/contract/subscribe.mjs +50 -0
  77. package/dist/contract/transient.mjs +28 -0
  78. package/dist/contract/watch.d.mts +18 -0
  79. package/dist/contract/watch.mjs +105 -0
  80. package/dist/contract/webhooks.d.mts +42 -0
  81. package/dist/contract/webhooks.mjs +53 -0
  82. package/dist/http/client.d.mts +37 -12
  83. package/dist/http/client.mjs +51 -12
  84. package/dist/http/events.d.mts +56 -0
  85. package/dist/http/events.mjs +102 -0
  86. package/dist/http/placed.d.mts +36 -0
  87. package/dist/http/placed.mjs +71 -0
  88. package/dist/http/schema-errors.mjs +1 -1
  89. package/dist/http/streamed.d.mts +21 -0
  90. package/dist/http/streamed.mjs +94 -0
  91. package/dist/http/watch.mjs +50 -0
  92. package/dist/mcp/bridge.mjs +283 -0
  93. package/dist/mcp/client.d.mts +30 -97
  94. package/dist/mcp/client.mjs +59 -132
  95. package/dist/mcp/events.mjs +142 -0
  96. package/dist/mcp/progress.mjs +34 -0
  97. package/dist/mcp/protocol.mjs +39 -13
  98. package/dist/mcp/reply.d.mts +79 -0
  99. package/dist/mcp/reply.mjs +99 -0
  100. package/dist/mcp/request.d.mts +40 -0
  101. package/dist/mcp/request.mjs +51 -0
  102. package/dist/mcp/watch.mjs +115 -0
  103. package/dist/rpc/watch.mjs +39 -0
  104. package/dist/stdio/console.mjs +13 -5
  105. package/dist/testing/memory.d.mts +1 -1
  106. package/dist/testing/memory.mjs +3 -3
  107. package/dist/webhook/address.mjs +47 -0
  108. package/dist/webhook/attempt.mjs +65 -0
  109. package/dist/webhook/receiver.mjs +67 -0
  110. package/dist/webhook/signing.mjs +28 -0
  111. package/dist/webhook/stored.mjs +57 -0
  112. package/dist/webhook/subscriptions.mjs +342 -0
  113. package/docs/Action-failures.md +2 -2
  114. package/docs/Action-streams.md +115 -0
  115. package/docs/Action.md +58 -124
  116. package/docs/ActionCli-failures.md +1 -2
  117. package/docs/ActionCli.md +83 -88
  118. package/docs/ActionHttp-failures.md +3 -3
  119. package/docs/ActionHttp.md +90 -70
  120. package/docs/ActionMcp-failures.md +2 -1
  121. package/docs/ActionMcp.md +120 -92
  122. package/docs/ActionRpc-events.md +126 -0
  123. package/docs/ActionRpc-failures.md +1 -2
  124. package/docs/ActionRpc.md +108 -79
  125. package/docs/ActionToolkit-failures.md +1 -2
  126. package/docs/ActionToolkit.md +33 -90
  127. package/docs/Authentication-advanced.md +52 -60
  128. package/docs/Authentication-failures.md +5 -4
  129. package/docs/Authentication.md +23 -58
  130. package/docs/Event-failures.md +15 -0
  131. package/docs/Event.md +281 -0
  132. package/docs/README.md +51 -82
  133. package/docs/Testing-failures.md +1 -5
  134. package/docs/Testing.md +126 -86
  135. package/docs/Webhook-failures.md +8 -0
  136. package/docs/Webhook.md +319 -0
  137. package/docs/setup-failures.md +2 -3
  138. package/docs/setup.md +32 -52
  139. package/examples/README.md +12 -37
  140. package/examples/assistant/README.md +21 -26
  141. package/examples/assistant/src/agent.ts +66 -24
  142. package/examples/assistant/src/cli.ts +7 -4
  143. package/examples/assistant/src/commands.ts +9 -4
  144. package/examples/assistant/src/identity/Identity.ts +5 -4
  145. package/examples/assistant/src/identity/authorize.ts +13 -5
  146. package/examples/assistant/src/identity/handlers.ts +3 -2
  147. package/examples/assistant/src/identity/permissions.ts +9 -3
  148. package/examples/assistant/src/platform/tasks-file.ts +15 -11
  149. package/examples/assistant/src/scripted.ts +7 -4
  150. package/examples/assistant/src/tasks/Tasks.ts +20 -6
  151. package/examples/assistant/src/tasks/handlers.ts +9 -5
  152. package/examples/assistant/src/tasks/store.ts +38 -11
  153. package/examples/assistant/src/toolkit.ts +11 -7
  154. package/examples/assistant/test/commands.test.ts +53 -27
  155. package/examples/assistant/test/identity/identity.test.ts +27 -11
  156. package/examples/assistant/test/platform/tasks-file.test.ts +39 -23
  157. package/examples/assistant/test/tasks/tasks.test.ts +62 -34
  158. package/examples/assistant/test/toolkit.test.ts +104 -75
  159. package/examples/build-events/README.md +83 -0
  160. package/examples/build-events/src/Api.ts +27 -0
  161. package/examples/build-events/src/Rpc.ts +26 -0
  162. package/examples/build-events/src/builds/Builds.ts +108 -0
  163. package/examples/build-events/src/builds/events.ts +54 -0
  164. package/examples/build-events/src/builds/handlers.ts +58 -0
  165. package/examples/build-events/src/builds/log.ts +30 -0
  166. package/examples/build-events/src/builds/status.ts +28 -0
  167. package/examples/build-events/src/builds/store.ts +130 -0
  168. package/examples/build-events/src/cli.ts +18 -0
  169. package/examples/build-events/src/commands.ts +32 -0
  170. package/examples/build-events/src/deploys/Deploys.ts +44 -0
  171. package/examples/build-events/src/deploys/approvers.ts +55 -0
  172. package/examples/build-events/src/deploys/events.ts +32 -0
  173. package/examples/build-events/src/deploys/handlers.ts +38 -0
  174. package/examples/build-events/src/identity/Identity.ts +29 -0
  175. package/examples/build-events/src/identity/members.ts +7 -0
  176. package/examples/build-events/src/identity/verifier.ts +22 -0
  177. package/examples/build-events/src/notifier/Notices.ts +26 -0
  178. package/examples/build-events/src/notifier/routes.ts +71 -0
  179. package/examples/build-events/src/notifier.ts +48 -0
  180. package/examples/build-events/src/routes.ts +71 -0
  181. package/examples/build-events/src/server.ts +62 -0
  182. package/examples/build-events/test/builds/events.test.ts +68 -0
  183. package/examples/build-events/test/builds/log.test.ts +71 -0
  184. package/examples/build-events/test/builds/progress.test.ts +57 -0
  185. package/examples/build-events/test/builds/status.test.ts +42 -0
  186. package/examples/build-events/test/commands.test.ts +103 -0
  187. package/examples/build-events/test/deploys/events.test.ts +52 -0
  188. package/examples/build-events/test/http.test.ts +95 -0
  189. package/examples/build-events/test/notifier.test.ts +80 -0
  190. package/examples/build-events/test/routes.test.ts +96 -0
  191. package/examples/build-events/test/rpc.test.ts +84 -0
  192. package/examples/build-events/test/support/as.ts +8 -0
  193. package/examples/build-events/test/support/memory.ts +43 -0
  194. package/examples/build-events/test/webhooks.test.ts +143 -0
  195. package/examples/kitchen-sink/README.md +178 -166
  196. package/examples/kitchen-sink/src/Actions.ts +11 -3
  197. package/examples/kitchen-sink/src/Api.ts +12 -6
  198. package/examples/kitchen-sink/src/Rpc.ts +7 -5
  199. package/examples/kitchen-sink/src/admin/Admin.ts +21 -10
  200. package/examples/kitchen-sink/src/admin/authenticate.ts +8 -4
  201. package/examples/kitchen-sink/src/admin/authorize.ts +13 -5
  202. package/examples/kitchen-sink/src/admin/grants.ts +5 -3
  203. package/examples/kitchen-sink/src/admin/handlers.ts +9 -5
  204. package/examples/kitchen-sink/src/agent.ts +14 -6
  205. package/examples/kitchen-sink/src/assistant/Assistant.ts +21 -15
  206. package/examples/kitchen-sink/src/assistant/conversations.ts +14 -9
  207. package/examples/kitchen-sink/src/assistant/demo-model.ts +2 -2
  208. package/examples/kitchen-sink/src/assistant/handlers.ts +42 -22
  209. package/examples/kitchen-sink/src/attachments/Attachments.ts +6 -6
  210. package/examples/kitchen-sink/src/attachments/download.ts +7 -6
  211. package/examples/kitchen-sink/src/attachments/handlers.ts +9 -4
  212. package/examples/kitchen-sink/src/attachments/store.ts +25 -18
  213. package/examples/kitchen-sink/src/browser.ts +10 -4
  214. package/examples/kitchen-sink/src/cli.ts +28 -12
  215. package/examples/kitchen-sink/src/config.ts +1 -1
  216. package/examples/kitchen-sink/src/identity/Identity.ts +24 -10
  217. package/examples/kitchen-sink/src/identity/authenticate.ts +19 -11
  218. package/examples/kitchen-sink/src/identity/authorize.ts +14 -4
  219. package/examples/kitchen-sink/src/identity/directory.ts +16 -7
  220. package/examples/kitchen-sink/src/identity/handlers.ts +1 -0
  221. package/examples/kitchen-sink/src/identity/members.ts +7 -2
  222. package/examples/kitchen-sink/src/identity/quota.ts +23 -20
  223. package/examples/kitchen-sink/src/issues/Issues.ts +91 -15
  224. package/examples/kitchen-sink/src/issues/assistant.ts +4 -2
  225. package/examples/kitchen-sink/src/issues/events.ts +46 -0
  226. package/examples/kitchen-sink/src/issues/export.ts +35 -0
  227. package/examples/kitchen-sink/src/issues/guide.ts +4 -3
  228. package/examples/kitchen-sink/src/issues/handlers.ts +20 -7
  229. package/examples/kitchen-sink/src/issues/store.ts +156 -67
  230. package/examples/kitchen-sink/src/issues/write-limit.ts +44 -18
  231. package/examples/kitchen-sink/src/memory.ts +11 -2
  232. package/examples/kitchen-sink/src/ops.ts +10 -5
  233. package/examples/kitchen-sink/src/request-policy.ts +21 -9
  234. package/examples/kitchen-sink/src/routes.ts +67 -27
  235. package/examples/kitchen-sink/src/sdk.ts +22 -12
  236. package/examples/kitchen-sink/src/seed.ts +15 -9
  237. package/examples/kitchen-sink/src/server.ts +24 -10
  238. package/examples/kitchen-sink/src/stdio.ts +9 -3
  239. package/examples/kitchen-sink/src/workspaces/Workspaces.ts +10 -7
  240. package/examples/kitchen-sink/src/workspaces/handlers.ts +3 -2
  241. package/examples/kitchen-sink/src/workspaces/registry.ts +16 -10
  242. package/examples/kitchen-sink/src/workspaces/tenant.ts +12 -5
  243. package/examples/kitchen-sink/test/admin/admin.test.ts +38 -23
  244. package/examples/kitchen-sink/test/assistant/assistant.test.ts +471 -299
  245. package/examples/kitchen-sink/test/attachments/attachments.test.ts +135 -94
  246. package/examples/kitchen-sink/test/identity/identity.test.ts +110 -82
  247. package/examples/kitchen-sink/test/identity/session.test.ts +65 -43
  248. package/examples/kitchen-sink/test/issues/assistant.test.ts +50 -31
  249. package/examples/kitchen-sink/test/issues/events.test.ts +273 -0
  250. package/examples/kitchen-sink/test/issues/export.test.ts +120 -0
  251. package/examples/kitchen-sink/test/issues/issues.test.ts +105 -75
  252. package/examples/kitchen-sink/test/request-policy.test.ts +108 -47
  253. package/examples/kitchen-sink/test/routes.test.ts +102 -57
  254. package/examples/kitchen-sink/test/support/as.ts +9 -0
  255. package/examples/kitchen-sink/test/support/scripted.ts +6 -5
  256. package/examples/kitchen-sink/test/support/services.ts +17 -0
  257. package/examples/live-rpc/README.md +33 -35
  258. package/examples/live-rpc/src/Rpc.ts +9 -5
  259. package/examples/live-rpc/src/browser.ts +39 -11
  260. package/examples/live-rpc/src/chat/Chat.ts +30 -16
  261. package/examples/live-rpc/src/chat/authorize.ts +2 -1
  262. package/examples/live-rpc/src/chat/events.ts +74 -0
  263. package/examples/live-rpc/src/chat/handlers.ts +13 -5
  264. package/examples/live-rpc/src/chat/room.ts +9 -5
  265. package/examples/live-rpc/src/chat/store.ts +51 -14
  266. package/examples/live-rpc/src/client.ts +22 -5
  267. package/examples/live-rpc/src/identity/Identity.ts +3 -3
  268. package/examples/live-rpc/src/identity/verifier.ts +8 -4
  269. package/examples/live-rpc/src/page.ts +58 -12
  270. package/examples/live-rpc/src/routes.ts +14 -10
  271. package/examples/live-rpc/src/server.ts +15 -5
  272. package/examples/live-rpc/test/browser.test.ts +127 -50
  273. package/examples/live-rpc/test/chat/chat.test.ts +215 -23
  274. package/examples/live-rpc/test/routes.test.ts +130 -46
  275. package/examples/live-rpc/test/server.test.ts +94 -33
  276. package/examples/mcp-tools/README.md +24 -38
  277. package/examples/mcp-tools/src/platform/fake-farm.ts +63 -24
  278. package/examples/mcp-tools/src/platform/print-farm.ts +59 -29
  279. package/examples/mcp-tools/src/printers/Printers.ts +18 -7
  280. package/examples/mcp-tools/src/printers/farm.ts +15 -5
  281. package/examples/mcp-tools/src/printers/handlers.ts +12 -6
  282. package/examples/mcp-tools/src/routes.ts +8 -6
  283. package/examples/mcp-tools/src/server.ts +12 -5
  284. package/examples/mcp-tools/src/stdio.ts +3 -2
  285. package/examples/mcp-tools/test/printers/printers.test.ts +132 -89
  286. package/examples/mcp-tools/test/routes.test.ts +140 -81
  287. package/examples/notes-service/README.md +30 -45
  288. package/examples/notes-service/src/Api.ts +1 -0
  289. package/examples/notes-service/src/browser.ts +15 -14
  290. package/examples/notes-service/src/cli.ts +8 -3
  291. package/examples/notes-service/src/commands.ts +46 -20
  292. package/examples/notes-service/src/identity/Identity.ts +2 -2
  293. package/examples/notes-service/src/identity/authorize.ts +6 -2
  294. package/examples/notes-service/src/identity/handlers.ts +3 -2
  295. package/examples/notes-service/src/identity/users.ts +2 -1
  296. package/examples/notes-service/src/identity/verifier.ts +12 -6
  297. package/examples/notes-service/src/notes/Notes.ts +46 -22
  298. package/examples/notes-service/src/notes/handlers.ts +18 -9
  299. package/examples/notes-service/src/notes/store.ts +70 -43
  300. package/examples/notes-service/src/routes.ts +7 -5
  301. package/examples/notes-service/src/server.ts +12 -5
  302. package/examples/notes-service/src/status/Status.ts +1 -1
  303. package/examples/notes-service/src/status/handlers.ts +5 -3
  304. package/examples/notes-service/src/stdio.ts +7 -3
  305. package/examples/notes-service/src/worker.ts +6 -3
  306. package/examples/notes-service/test/commands.test.ts +164 -91
  307. package/examples/notes-service/test/notes/as.ts +3 -1
  308. package/examples/notes-service/test/notes/idempotency.test.ts +97 -67
  309. package/examples/notes-service/test/notes/in-process.test.ts +26 -15
  310. package/examples/notes-service/test/notes/notes.test.ts +49 -31
  311. package/examples/notes-service/test/routes.test.ts +106 -71
  312. package/examples/notes-service/test/worker.test.ts +21 -13
  313. package/examples/quickstart/README.md +2 -6
  314. package/examples/quickstart/src/Api.ts +1 -0
  315. package/examples/quickstart/src/client.ts +6 -3
  316. package/examples/quickstart/src/greeting/Greeting.ts +1 -1
  317. package/examples/quickstart/src/greeting/handlers.ts +5 -2
  318. package/examples/quickstart/src/routes.ts +3 -2
  319. package/examples/quickstart/src/server.ts +6 -2
  320. package/examples/quickstart/test/routes.test.ts +3 -2
  321. package/package.json +10 -4
@@ -0,0 +1,28 @@
1
+ import { Context, Duration, Effect, Equal, Hash, RcMap, Scope } from "effect";
2
+ // src/contract/lease.ts
3
+ /** A poll subscription's key, held with the context of the poll that first acquired it. */
4
+ var Lease = class Lease {
5
+ key;
6
+ input;
7
+ context;
8
+ constructor(key, input, context) {
9
+ this.key = key;
10
+ this.input = input;
11
+ this.context = context;
12
+ }
13
+ [Equal.symbol](that) {
14
+ return that instanceof Lease && that.key === this.key;
15
+ }
16
+ [Hash.symbol]() {
17
+ return Hash.string(this.key);
18
+ }
19
+ };
20
+ const defaultLease = Duration.minutes(5);
21
+ const makeLeases = (subscription, ttl = defaultLease) => Effect.map(RcMap.make({
22
+ lookup: (lease) => Effect.provideContext(subscription(lease.input), Context.omit(Scope.Scope)(lease.context)),
23
+ idleTimeToLive: ttl
24
+ }), (map) => ({
25
+ hold: (key, input) => Effect.flatMap(Effect.context(), (context) => Effect.scoped(RcMap.get(map, new Lease(key, input, context)))),
26
+ release: (key) => RcMap.invalidate(map, new Lease(key, void 0, Context.empty()))
27
+ }));
28
+ export { makeLeases };
@@ -0,0 +1,4 @@
1
+ import "../Event.mjs";
2
+ import "../Action.mjs";
3
+ /** The members of the list `T` that are `U`: `T` itself where every one is, `[]` where none is. */
4
+ export type Only<T extends readonly unknown[], U> = [Exclude<T[number], U>] extends [never] ? Extract<T, readonly U[]> : [Extract<T[number], U>] extends [never] ? readonly [] : ReadonlyArray<Extract<T[number], U>>;
@@ -0,0 +1,22 @@
1
+ import { assertOnce } from "./names.mjs";
2
+ import { isEvent } from "../Event.mjs";
3
+ // src/contract/listed.ts
4
+ /** A list split into its actions and its event types. */
5
+ const split = (contracts) => ({
6
+ actions: contracts.flatMap((contract) => isEvent(contract) ? [] : [contract]),
7
+ events: contracts.filter(isEvent)
8
+ });
9
+ /** A binding's list split into its actions and its event types, each name used once. */
10
+ const listed = (contracts) => {
11
+ assertOnce("action or event", contracts);
12
+ return split(contracts);
13
+ };
14
+ /**
15
+ * A surface's `actions` option split into the actions and the event types it lists, both absent
16
+ * where it is: a surface given the option serves exactly what it lists.
17
+ */
18
+ const selection = (contracts) => contracts === void 0 ? {
19
+ actions: void 0,
20
+ events: void 0
21
+ } : split(contracts);
22
+ export { listed, selection, split };
@@ -0,0 +1,41 @@
1
+ import { Duration, Effect, Stream } from "effect";
2
+ /** One entry of a log: what was published, and the key it was published to, if any. */
3
+ export interface Logged<A> {
4
+ readonly value: A;
5
+ readonly key: string | undefined;
6
+ }
7
+ /** Where a log stands: the epoch its cursors name, and the last sequence number it gave. */
8
+ export interface Position {
9
+ readonly epoch: string;
10
+ readonly last: number;
11
+ }
12
+ /** One entry as a log's storage keeps it: when it was appended, and what was published. */
13
+ export interface Stored<A> {
14
+ readonly at: number;
15
+ readonly logged: Logged<A>;
16
+ }
17
+ /**
18
+ * One entry as a log's storage lists it: as it was kept, or with `logged` `undefined` where the
19
+ * storage could not read it back, such as data an earlier deployment wrote in another shape,
20
+ * which a subscriber receives as a gap at its cursor.
21
+ */
22
+ export interface Listed<A> {
23
+ readonly at: number;
24
+ readonly logged: Logged<A> | undefined;
25
+ }
26
+ /**
27
+ * Where a log keeps its position and its entries, by sequence number, as `Event.makeEvents` is
28
+ * given it: storage alone, the log's cursors, bounds, truncation and expiry being the core's.
29
+ */
30
+ export interface LogStorage<A> {
31
+ /** The position `put` last kept, or `undefined` for a storage never written or emptied. */
32
+ readonly position: Effect.Effect<Position | undefined>;
33
+ /** Keeps `position`, and `entry` with it where given, at once. */
34
+ readonly put: (position: Position, entry?: {
35
+ readonly seq: number;
36
+ readonly stored: Stored<A>;
37
+ }) => Effect.Effect<void>;
38
+ /** The entries kept from sequence number `from` on, at most `limit`, in order. */
39
+ readonly list: (from: number, limit: number) => Effect.Effect<ReadonlyArray<readonly [seq: number, listed: Listed<A>]>>;
40
+ readonly delete: (seqs: readonly number[]) => Effect.Effect<void>;
41
+ }
@@ -0,0 +1,144 @@
1
+ import { uniqueKey } from "./implementation.mjs";
2
+ import { reported } from "./reported.mjs";
3
+ import { Cause, Clock, Duration, Effect, Option, Queue, Semaphore, Stream } from "effect";
4
+ // src/contract/log.ts
5
+ const defaultBounds = {
6
+ maxAge: Duration.minutes(10),
7
+ maxEvents: 1e4
8
+ };
9
+ /** A log storage in memory, for the process that makes it. */
10
+ const memoryStorage = () => {
11
+ let position;
12
+ let newest = 0;
13
+ const entries = /* @__PURE__ */ new Map();
14
+ return {
15
+ position: Effect.sync(() => position),
16
+ put: (next, entry) => Effect.sync(() => {
17
+ position = next;
18
+ if (entry === void 0) return;
19
+ entries.set(entry.seq, entry.stored);
20
+ newest = Math.max(newest, entry.seq);
21
+ }),
22
+ list: (from, limit) => Effect.sync(() => {
23
+ const listed = [];
24
+ const oldest = entries.keys().next().value;
25
+ if (oldest === void 0) return listed;
26
+ for (let seq = Math.max(from, oldest); seq <= newest && listed.length < limit; seq += 1) {
27
+ const stored = entries.get(seq);
28
+ if (stored !== void 0) listed.push([seq, stored]);
29
+ }
30
+ return listed;
31
+ }),
32
+ delete: (seqs) => Effect.sync(() => {
33
+ for (const seq of seqs) entries.delete(seq);
34
+ })
35
+ };
36
+ };
37
+ const separator = ":";
38
+ const cursorAt = (epoch, seq) => `${epoch}${separator}${seq}`;
39
+ const seqIn = (epoch, cursor) => {
40
+ const [named, seq, ...rest] = cursor.split(separator);
41
+ const parsed = Number(seq);
42
+ return named === epoch && rest.length === 0 && Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : void 0;
43
+ };
44
+ /**
45
+ * A log's entries over `storage`, read by cursor: a cursor is the storage's epoch and the
46
+ * sequence number of the last entry it has seen, so one minted by another epoch, such as a
47
+ * memory log's before a restart or a storage's before it was emptied, reads as truncated, as
48
+ * does one whose next entries were dropped. An epoch is minted where the storage holds none.
49
+ */
50
+ const makeLogStore = (storage, { maxAge = defaultBounds.maxAge, maxEvents = defaultBounds.maxEvents } = {}) => {
51
+ if (!Number.isSafeInteger(maxEvents) || maxEvents < 1) throw new Error(`Invalid log maxEvents: ${maxEvents} (give a positive integer)`);
52
+ const age = Duration.toMillis(Duration.fromInputUnsafe(maxAge));
53
+ const turn = Semaphore.makeUnsafe(1);
54
+ const positioned = Effect.flatMap(storage.position, (found) => {
55
+ if (found !== void 0) return Effect.succeed(found);
56
+ const minted = {
57
+ epoch: uniqueKey(),
58
+ last: 0
59
+ };
60
+ return Effect.as(storage.put(minted), minted);
61
+ });
62
+ const oldestKept = (last, now) => Effect.gen(function* () {
63
+ for (let batch = 1;; batch = 64) {
64
+ const listed = yield* storage.list(0, batch);
65
+ const young = listed.findIndex(([, { at }]) => at >= now - age);
66
+ const dropped = young === -1 ? listed : listed.slice(0, young);
67
+ if (dropped.length > 0) yield* storage.delete(dropped.map(([seq]) => seq));
68
+ const first = listed[dropped.length];
69
+ if (first !== void 0) return Math.max(first[0], last - maxEvents + 1);
70
+ if (listed.length < batch) return last + 1;
71
+ }
72
+ });
73
+ return {
74
+ append: (logged) => turn.withPermit(Effect.gen(function* () {
75
+ const { epoch, last } = yield* positioned;
76
+ const seq = last + 1;
77
+ const at = yield* Clock.currentTimeMillis;
78
+ yield* storage.put({
79
+ epoch,
80
+ last: seq
81
+ }, {
82
+ seq,
83
+ stored: {
84
+ at,
85
+ logged
86
+ }
87
+ });
88
+ if (seq > maxEvents) yield* storage.delete([seq - maxEvents]);
89
+ })),
90
+ read: (cursor, limit) => turn.withPermit(Effect.gen(function* () {
91
+ const { epoch, last } = yield* positioned;
92
+ const oldest = yield* oldestKept(last, yield* Clock.currentTimeMillis);
93
+ const seq = cursor === null ? last : seqIn(epoch, cursor);
94
+ const { after, truncated } = seq === void 0 || seq > last || seq + 1 < oldest ? {
95
+ after: oldest - 1,
96
+ truncated: true
97
+ } : {
98
+ after: seq,
99
+ truncated: false
100
+ };
101
+ const pending = yield* storage.list(after + 1, limit + 1);
102
+ const page = pending.slice(0, limit);
103
+ return {
104
+ entries: page.map(([at, stored]) => ({
105
+ seq: at,
106
+ cursor: cursorAt(epoch, at),
107
+ at: stored.at,
108
+ value: stored.logged
109
+ })),
110
+ after: cursorAt(epoch, after),
111
+ cursor: cursorAt(epoch, page.at(-1)?.[0] ?? after),
112
+ truncated,
113
+ hasMore: pending.length > limit
114
+ };
115
+ }))
116
+ };
117
+ };
118
+ /** A log over `store`, waking its followers as each entry is appended. */
119
+ const makeLog = (store) => {
120
+ const followers = /* @__PURE__ */ new Set();
121
+ const append = (value, key) => Effect.andThen(store.append({
122
+ value,
123
+ key
124
+ }), Effect.sync(() => {
125
+ for (const woken of followers) Queue.offerUnsafe(woken, void 0);
126
+ }));
127
+ const following = Effect.acquireRelease(Effect.tap(Queue.sliding(1), (woken) => Effect.sync(() => followers.add(woken))), (woken) => Effect.sync(() => followers.delete(woken)));
128
+ return {
129
+ append,
130
+ feed: (emit) => emit.pipe(Stream.runForEach(({ value, key }) => append(value, key)), Effect.matchCauseEffect({
131
+ onFailure: (cause) => Cause.hasInterruptsOnly(cause) ? Effect.void : reported(cause),
132
+ onSuccess: () => Effect.logWarning("A log source's emit stream ended: it appends nothing more")
133
+ })),
134
+ read: store.read,
135
+ follow: (cursor, limit) => Stream.unwrap(Effect.map(following, (woken) => Stream.paginate({
136
+ cursor,
137
+ wait: false
138
+ }, (state) => Effect.map(Effect.andThen(state.wait ? Queue.take(woken) : Effect.void, store.read(state.cursor, limit)), (answered) => [[answered], Option.some({
139
+ cursor: answered.cursor,
140
+ wait: !answered.hasMore
141
+ })]))))
142
+ };
143
+ };
144
+ export { makeLog, makeLogStore, memoryStorage };
@@ -1,8 +1,16 @@
1
1
  // src/contract/names.ts
2
2
  const namePattern = "[A-Za-z0-9_-]{1,128}";
3
- const validName = new RegExp(`^${namePattern}$`);
3
+ const validName = new RegExp(`^${namePattern}$`, "u");
4
+ const reserved = [["then", "which would make a client a thenable, awaited as a promise"], ["watch", "every client's method for events"]];
5
+ const reservedNames = reserved.map(([name]) => name);
6
+ /** The reserved names as a message lists them: `"then" or "watch"`. */
7
+ const reservedList = reservedNames.map((name) => `"${name}"`).join(" or ");
4
8
  const assertName = (name) => {
5
- if (!validName.test(name) || name === "then") throw new Error(`Invalid action name: ${name} (use ${namePattern}, other than "then")`);
9
+ if (!validName.test(name) || reservedNames.includes(name)) throw new Error(`Invalid action name: ${name} (use ${namePattern}, other than ${reserved.map(([reservedName, why]) => `"${reservedName}", ${why}`).join(", and ")})`);
10
+ };
11
+ const validEventName = new RegExp(`^[a-z0-9_-]+(\\.[a-z0-9_-]+)*$`, "u");
12
+ const assertEventName = (name) => {
13
+ if (name.length > 128 || !validEventName.test(name)) throw new Error(`Invalid event name: ${name} (use lowercase dot-separated segments of [a-z0-9_-], at most 128 characters)`);
6
14
  };
7
15
  const assertDistinct = (what, items, nameOf, { claimantOf = nameOf, labelOf = nameOf, fix }) => {
8
16
  const seen = /* @__PURE__ */ new Map();
@@ -18,9 +26,11 @@ const assertDistinct = (what, items, nameOf, { claimantOf = nameOf, labelOf = na
18
26
  }
19
27
  };
20
28
  const heldOnce = "pass one implementation of it, or rename one";
21
- const assertOnce = (what, actions, fix = "list it once, or rename one") => assertDistinct(what, actions, (action) => action.name, { fix });
29
+ const assertOnce = (what, actions, fix = "list it once, or rename one") => {
30
+ assertDistinct(what, actions, (action) => action.name, { fix });
31
+ };
22
32
  const assertKnown = (what, keys, names, fix) => {
23
33
  const unknown = keys.filter((key) => !names.includes(key));
24
34
  if (unknown.length > 0) throw new Error(`Unknown ${what}: ${unknown.join(", ")} (${fix})`);
25
35
  };
26
- export { assertDistinct, assertKnown, assertName, assertOnce, heldOnce };
36
+ export { assertDistinct, assertEventName, assertKnown, assertName, assertOnce, heldOnce, reservedList, reservedNames };
@@ -0,0 +1,29 @@
1
+ import { Any } from "../Event.mjs";
2
+ import { Effect, Schema } from "effect";
3
+ /**
4
+ * Occurrences were missed: the cursor resumed from was no longer served, or delivery skipped
5
+ * some. `cursor` is where delivery continues.
6
+ */
7
+ export declare const Gap: Schema.TaggedStruct<"Gap", {
8
+ readonly cursor: Schema.NullOr<Schema.String>;
9
+ }>;
10
+ /** A gap in what a subscriber received. */
11
+ export type Gap = typeof Gap.Type;
12
+ /**
13
+ * Where a feed has read up to without an occurrence to show for it, such as a quiet page or an
14
+ * occurrence a subscriber's `match` rejected: a watch keeps it as its cursor and never yields it.
15
+ */
16
+ export declare const Checked: Schema.TaggedStruct<"Checked", {
17
+ readonly cursor: Schema.NullOr<Schema.String>;
18
+ }>;
19
+ export type Checked = typeof Checked.Type;
20
+ declare const fieldsOf: <Name extends Schema.Top, Data extends Schema.Top>(name: Name, data: Data) => {
21
+ eventId: Schema.String;
22
+ name: Name;
23
+ timestamp: Schema.DateTimeUtc;
24
+ data: Data;
25
+ cursor: Schema.NullOr<Schema.String>;
26
+ };
27
+ /** The schema of an occurrence of `E`. */
28
+ export type OccurrenceSchema<E extends Any> = Schema.TaggedStruct<"Occurrence", ReturnType<typeof fieldsOf<Schema.Literal<E["name"]>, E["payload"]>>>;
29
+ export {};
@@ -0,0 +1,45 @@
1
+ import { Effect, Schema, Struct } from "effect";
2
+ // src/contract/occurrence.ts
3
+ /**
4
+ * Occurrences were missed: the cursor resumed from was no longer served, or delivery skipped
5
+ * some. `cursor` is where delivery continues.
6
+ */
7
+ const Gap = Schema.TaggedStruct("Gap", { cursor: Schema.NullOr(Schema.String) });
8
+ /**
9
+ * Where a feed has read up to without an occurrence to show for it, such as a quiet page or an
10
+ * occurrence a subscriber's `match` rejected: a watch keeps it as its cursor and never yields it.
11
+ */
12
+ const Checked = Schema.TaggedStruct("Checked", { cursor: Schema.NullOr(Schema.String) });
13
+ const fieldsOf = (name, data) => ({
14
+ eventId: Schema.String.annotate({ description: "The source's id for it, or one generated: a subscriber deduplicates by it." }),
15
+ name,
16
+ timestamp: Schema.DateTimeUtc,
17
+ data,
18
+ cursor: Schema.NullOr(Schema.String).annotate({ description: "Where to resume after it, null for an event type that cannot be resumed." })
19
+ });
20
+ /** An occurrence of `event`, as a subscriber decodes it. */
21
+ const OccurrenceOf = (event) => Schema.TaggedStruct("Occurrence", fieldsOf(Schema.Literal(event.name), event.payload));
22
+ /** Any occurrence, its data erased, as a feed makes it. */
23
+ const AnyOccurrence = Schema.TaggedStruct("Occurrence", fieldsOf(Schema.String, Schema.Unknown));
24
+ /** What a subscriber of `event` decodes: an occurrence, or a gap. */
25
+ const ReceivedOf = (event) => Schema.Union([OccurrenceOf(event), Gap]);
26
+ const untaggedOf = (event) => OccurrenceOf(event).mapFields((fields) => ({
27
+ ...Struct.omit(fields, ["_tag"]),
28
+ timestamp: Schema.DateTimeUtcFromString,
29
+ data: Schema.toCodecJson(event.payload)
30
+ }));
31
+ /**
32
+ * How a subscriber of `event` decodes an occurrence sent as JSON without its tag, as MCP's
33
+ * notifications and a webhook's body carry one.
34
+ */
35
+ const untaggedOccurrenceOf = (event) => {
36
+ const occurrence = OccurrenceOf(event);
37
+ const decode = Schema.decodeUnknownEffect(untaggedOf(event));
38
+ return (wire) => Effect.map(decode(wire), (fields) => occurrence.make(fields));
39
+ };
40
+ /** How a surface encodes an occurrence of `event` as JSON without its tag, its counterpart. */
41
+ const encodeUntaggedOf = (event) => {
42
+ const encode = Schema.encodeEffect(untaggedOf(event));
43
+ return (occurrence) => Effect.orDie(encode(occurrence));
44
+ };
45
+ export { AnyOccurrence, Checked, Gap, OccurrenceOf, ReceivedOf, encodeUntaggedOf, untaggedOccurrenceOf };
@@ -0,0 +1,13 @@
1
+ import { Checked } from "./occurrence.mjs";
2
+ import { Any, Occurrence, Received } from "../Event.mjs";
3
+ import { Duration, Effect, Stream } from "effect";
4
+ /** One poll's answer, as a surface sends it and a client reads it. */
5
+ export interface Page {
6
+ readonly events: ReadonlyArray<Occurrence<Any>>;
7
+ readonly cursor: string | null;
8
+ readonly truncated: boolean;
9
+ readonly hasMore: boolean;
10
+ readonly nextPoll: Duration.Duration;
11
+ }
12
+ /** What a feed's stream yields: what a subscriber receives, and where it has read up to. */
13
+ export type Fed = Received<Any> | Checked;
@@ -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 {};