@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
@@ -1,16 +1,19 @@
1
1
  import { describe, expect, it } from "@effect/vitest";
2
- import { Effect, Layer } from "effect";
3
2
  import * as Action from "@gjermundgaraba/effect-actions/Action";
4
- import { CurrentCaller, type Member, type Operator } from "../../src/identity/Identity.js";
3
+ import { Effect, Layer } from "effect";
4
+
5
+ import { CurrentCaller } from "../../src/identity/Identity.js";
6
+ import type { Member, Operator } from "../../src/identity/Identity.js";
5
7
  import { members } from "../../src/identity/members.js";
6
8
  import { Permissions } from "../../src/identity/permissions.js";
7
9
  import { tasks } from "../../src/tasks/handlers.js";
8
- import { TaskNotFound } from "../../src/tasks/Tasks.js";
9
10
  import { TaskStore } from "../../src/tasks/store.js";
11
+ import { TaskNotFound } from "../../src/tasks/Tasks.js";
10
12
 
11
13
  // What the implementation does, with no model and no command line: `Action.client` runs the
12
14
  // same decoding, `authorize` and handler every surface runs. Each call names its caller.
13
- const as = (caller: Member | Operator) => Effect.provideService(CurrentCaller, caller);
15
+ const as = (caller: Member | Operator) =>
16
+ Effect.provideService(CurrentCaller, caller);
14
17
 
15
18
  const operator: Operator = { kind: "operator", id: "root" };
16
19
 
@@ -22,52 +25,77 @@ describe("tasks, in process", () => {
22
25
  Effect.gen(function* () {
23
26
  const client = yield* Action.client(tasks);
24
27
 
25
- yield* client.addTask({ title: "Book the venue", assignee: "ada" }).pipe(as(members.ada));
26
- yield* client.addTask({ title: "Send invitations", assignee: "grace" }).pipe(as(operator));
28
+ yield* client
29
+ .addTask({ title: "Book the venue", assignee: "ada" })
30
+ .pipe(as(members.ada));
31
+ yield* client
32
+ .addTask({ title: "Send invitations", assignee: "grace" })
33
+ .pipe(as(operator));
27
34
 
28
- const { tasks: listed } = yield* client.listTasks().pipe(as(members.grace));
35
+ const { tasks: listed } = yield* client
36
+ .listTasks()
37
+ .pipe(as(members.grace));
29
38
 
30
- expect(listed.map(({ id, addedBy }) => [id, addedBy])).toEqual([
39
+ expect(listed.map(({ id, addedBy }) => [id, addedBy])).toStrictEqual([
31
40
  ["1", "ada"],
32
41
  ["2", "root"],
33
42
  ]);
34
- expect(yield* client.listTasks({ assignee: "grace" }).pipe(as(members.grace))).toMatchObject({
43
+ expect(
44
+ yield* client.listTasks({ assignee: "grace" }).pipe(as(members.grace))
45
+ ).toMatchObject({
35
46
  tasks: [{ title: "Send invitations" }],
36
47
  });
37
- }).pipe(Effect.provide(services)),
48
+ }).pipe(Effect.provide(services))
38
49
  );
39
50
 
40
- it.effect("refuses a write from a member the store grants reading alone", () =>
41
- Effect.gen(function* () {
42
- const client = yield* Action.client(tasks);
43
- const add = client.addTask({ title: "Order lunch", assignee: "guest" });
51
+ it.effect(
52
+ "refuses a write from a member the store grants reading alone",
53
+ () =>
54
+ Effect.gen(function* () {
55
+ const client = yield* Action.client(tasks);
56
+ const add = client.addTask({ title: "Order lunch", assignee: "guest" });
44
57
 
45
- const refusal = yield* Effect.flip(add.pipe(as(members.guest)));
58
+ const refusal = yield* Effect.flip(add.pipe(as(members.guest)));
46
59
 
47
- expect(refusal).toBeInstanceOf(Action.Forbidden);
48
- expect(refusal.message).toBe("Requires tasks:write.");
49
- expect(yield* client.listTasks().pipe(as(members.guest))).toEqual({ tasks: [] });
50
- }).pipe(Effect.provide(services)),
60
+ expect(refusal).toBeInstanceOf(Action.Forbidden);
61
+ expect(refusal.message).toBe("Requires tasks:write.");
62
+ expect(yield* client.listTasks().pipe(as(members.guest))).toStrictEqual(
63
+ {
64
+ tasks: [],
65
+ }
66
+ );
67
+ }).pipe(Effect.provide(services))
51
68
  );
52
69
 
53
- it.effect("admits the operator, whom the permission store does not know", () =>
54
- Effect.gen(function* () {
55
- const client = yield* Action.client(tasks);
56
- const add = client.addTask({ title: "Rotate the keys", assignee: "ada" });
57
- const { id } = yield* add.pipe(as(operator));
70
+ it.effect(
71
+ "admits the operator, whom the permission store does not know",
72
+ () =>
73
+ Effect.gen(function* () {
74
+ const client = yield* Action.client(tasks);
75
+
76
+ const add = client.addTask({
77
+ title: "Rotate the keys",
78
+ assignee: "ada",
79
+ });
58
80
 
59
- expect(yield* client.completeTask({ id }).pipe(as(operator))).toMatchObject({ done: true });
60
- }).pipe(Effect.provide(services)),
81
+ const { id } = yield* add.pipe(as(operator));
82
+
83
+ expect(
84
+ yield* client.completeTask({ id }).pipe(as(operator))
85
+ ).toMatchObject({ done: true });
86
+ }).pipe(Effect.provide(services))
61
87
  );
62
88
 
63
- it.effect("fails with the declared error for a task that does not exist", () =>
64
- Effect.gen(function* () {
65
- const client = yield* Action.client(tasks);
66
- const annotate = client.annotateTask({ id: "9", notes: "Urgent." });
89
+ it.effect(
90
+ "fails with the declared error for a task that does not exist",
91
+ () =>
92
+ Effect.gen(function* () {
93
+ const client = yield* Action.client(tasks);
94
+ const annotate = client.annotateTask({ id: "9", notes: "Urgent." });
67
95
 
68
- expect(yield* Effect.flip(annotate.pipe(as(members.ada)))).toEqual(
69
- new TaskNotFound({ id: "9" }),
70
- );
71
- }).pipe(Effect.provide(services)),
96
+ expect(
97
+ yield* Effect.flip(annotate.pipe(as(members.ada)))
98
+ ).toStrictEqual(new TaskNotFound({ id: "9" }));
99
+ }).pipe(Effect.provide(services))
72
100
  );
73
101
  });
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, it } from "@effect/vitest";
2
2
  import { Effect, Layer, Stream } from "effect";
3
3
  import { Chat, LanguageModel } from "effect/ai";
4
+
4
5
  import type { Member } from "../src/identity/Identity.js";
5
6
  import { members } from "../src/identity/members.js";
6
7
  import { Permissions } from "../src/identity/permissions.js";
@@ -11,21 +12,43 @@ import { actingFor, layer, toolkit } from "../src/toolkit.js";
11
12
  // What the toolkit adds to the implementations: a caller per call, and a model's approvals.
12
13
  // The handlers run in this process, on a store in memory the tests can read too.
13
14
  const tools = layer.pipe(
14
- Layer.provideMerge(Layer.mergeAll(TaskStore.layerMemory, Permissions.layerMemory)),
15
+ Layer.provideMerge(
16
+ Layer.mergeAll(TaskStore.layerMemory, Permissions.layerMemory)
17
+ )
15
18
  );
16
19
 
17
20
  // A model's call of `addTask`, as a provider sends it.
18
21
  const adding = (id: string, assignee: string) =>
19
- ({ type: "tool-call", id, name: "addTask", params: { title: "Order lunch", assignee } }) as const;
22
+ ({
23
+ type: "tool-call",
24
+ id,
25
+ name: "addTask",
26
+ params: { title: "Order lunch", assignee },
27
+ }) as const;
20
28
 
21
29
  // Which calls of a turn ran, and which wait for a person.
22
- const outcome = (response: LanguageModel.GenerateTextResponse<typeof toolkit.tools, "opaque">) => ({
30
+ const outcome = (
31
+ response: LanguageModel.GenerateTextResponse<typeof toolkit.tools, "opaque">
32
+ ) => ({
23
33
  ran: response.toolResults.map(({ id }) => id),
24
34
  waiting: response.content.flatMap((part) =>
25
- part.type === "tool-approval-request" ? [part.toolCallId] : [],
35
+ part.type === "tool-approval-request" ? [part.toolCallId] : []
26
36
  ),
27
37
  });
28
38
 
39
+ // The model answers each member with the same two calls.
40
+ const plan = (member: Member) =>
41
+ LanguageModel.generateText({
42
+ prompt: "Plan the launch.",
43
+ toolkit,
44
+ }).pipe(
45
+ actingFor(member),
46
+ Effect.map(outcome),
47
+ Effect.provide(
48
+ scripted([adding("for ada", "ada"), adding("for grace", "grace")])
49
+ )
50
+ );
51
+
29
52
  describe("the toolkit", () => {
30
53
  it.effect("runs each call as the member provided around it", () =>
31
54
  Effect.gen(function* () {
@@ -33,9 +56,10 @@ describe("the toolkit", () => {
33
56
 
34
57
  // A call by hand, with the JSON arguments a model sends. The member goes around the whole
35
58
  // call, the collecting of its results included.
36
- const add = handle("addTask", { title: "Order lunch", assignee: "guest" }).pipe(
37
- Effect.flatMap(Stream.runCollect),
38
- );
59
+ const add = handle("addTask", {
60
+ title: "Order lunch",
61
+ assignee: "guest",
62
+ }).pipe(Effect.flatMap(Stream.runCollect));
39
63
 
40
64
  // A refusal is not raised: it is a result the model reads, as a declared error is.
41
65
  expect(yield* add.pipe(actingFor(members.guest))).toMatchObject([
@@ -44,77 +68,82 @@ describe("the toolkit", () => {
44
68
  expect(yield* add.pipe(actingFor(members.ada))).toMatchObject([
45
69
  { isFailure: false, result: { id: "1", addedBy: "ada" } },
46
70
  ]);
47
- }).pipe(Effect.provide(tools)),
71
+ }).pipe(Effect.provide(tools))
48
72
  );
49
73
 
50
- it.effect("asks for approval before a model adds to a list that is not its caller's", () =>
51
- Effect.gen(function* () {
52
- // The model answers each member with the same two calls.
53
- const plan = (member: Member) =>
54
- LanguageModel.generateText({ prompt: "Plan the launch.", toolkit }).pipe(
55
- actingFor(member),
56
- Effect.map(outcome),
57
- Effect.provide(scripted([adding("for ada", "ada"), adding("for grace", "grace")])),
58
- );
59
-
60
- expect(yield* plan(members.ada)).toEqual({ ran: ["for ada"], waiting: ["for grace"] });
61
- expect(yield* plan(members.grace)).toEqual({ ran: ["for grace"], waiting: ["for ada"] });
62
-
63
- // A call that waits has not run.
64
- const store = yield* TaskStore;
65
-
66
- expect((yield* store.list(undefined)).map(({ addedBy }) => addedBy)).toEqual([
67
- "ada",
68
- "grace",
69
- ]);
70
- }).pipe(Effect.provide(tools)),
74
+ it.effect(
75
+ "asks for approval before a model adds to a list that is not its caller's",
76
+ () =>
77
+ Effect.gen(function* () {
78
+ expect(yield* plan(members.ada)).toStrictEqual({
79
+ ran: ["for ada"],
80
+ waiting: ["for grace"],
81
+ });
82
+ expect(yield* plan(members.grace)).toStrictEqual({
83
+ ran: ["for grace"],
84
+ waiting: ["for ada"],
85
+ });
86
+
87
+ // A call that waits has not run.
88
+ const store = yield* TaskStore;
89
+
90
+ expect(
91
+ (yield* store.list(undefined)).map(({ addedBy }) => addedBy)
92
+ ).toStrictEqual(["ada", "grace"]);
93
+ }).pipe(Effect.provide(tools))
71
94
  );
72
95
 
73
- it.effect("authorizes an approved call too: approval lets no one do what they may not", () =>
74
- Effect.gen(function* () {
75
- // The `Chat` keeps the request in its history, which the answer is matched against.
76
- const chat = yield* Chat.empty;
77
-
78
- const asked = yield* chat
79
- .generateText({ prompt: "Add lunch to Ada's list.", toolkit })
80
- .pipe(actingFor(members.guest));
81
-
82
- expect(outcome(asked)).toEqual({ ran: [], waiting: ["for ada"] });
83
-
84
- // The approved call runs in the turn that carries the answer, as that turn's caller.
85
- const answered = yield* chat
86
- .generateText({
87
- prompt: [
88
- {
89
- role: "tool",
90
- content: asked.content.flatMap((part) =>
91
- part.type === "tool-approval-request"
92
- ? [
93
- {
94
- type: "tool-approval-response",
95
- approvalId: part.approvalId,
96
- approved: true,
97
- },
98
- ]
99
- : [],
100
- ),
101
- },
102
- ],
103
- toolkit,
104
- })
105
- .pipe(actingFor(members.guest));
106
-
107
- expect(answered.toolResults).toMatchObject([
108
- {
109
- id: "for ada",
110
- isFailure: true,
111
- result: { message: "Requires tasks:write." },
112
- },
113
- ]);
114
-
115
- const store = yield* TaskStore;
116
-
117
- expect(yield* store.list(undefined)).toEqual([]);
118
- }).pipe(Effect.provide(scripted([adding("for ada", "ada")])), Effect.provide(tools)),
96
+ it.effect(
97
+ "authorizes an approved call too: approval lets no one do what they may not",
98
+ () =>
99
+ Effect.gen(function* () {
100
+ // The `Chat` keeps the request in its history, which the answer is matched against.
101
+ const chat = yield* Chat.empty;
102
+
103
+ const asked = yield* chat
104
+ .generateText({ prompt: "Add lunch to Ada's list.", toolkit })
105
+ .pipe(actingFor(members.guest));
106
+
107
+ expect(outcome(asked)).toStrictEqual({ ran: [], waiting: ["for ada"] });
108
+
109
+ // The approved call runs in the turn that carries the answer, as that turn's caller.
110
+ const answered = yield* chat
111
+ .generateText({
112
+ prompt: [
113
+ {
114
+ role: "tool",
115
+ content: asked.content.flatMap((part) =>
116
+ part.type === "tool-approval-request"
117
+ ? [
118
+ {
119
+ type: "tool-approval-response",
120
+ approvalId: part.approvalId,
121
+ approved: true,
122
+ },
123
+ ]
124
+ : []
125
+ ),
126
+ },
127
+ ],
128
+ toolkit,
129
+ })
130
+ .pipe(actingFor(members.guest));
131
+
132
+ expect(answered.toolResults).toMatchObject([
133
+ {
134
+ id: "for ada",
135
+ isFailure: true,
136
+ result: { message: "Requires tasks:write." },
137
+ },
138
+ ]);
139
+
140
+ const store = yield* TaskStore;
141
+
142
+ expect(yield* store.list(undefined)).toStrictEqual([]);
143
+ }).pipe(
144
+ Effect.provide(
145
+ scripted([adding("for ada", "ada")]).pipe(Layer.provideMerge(tools))
146
+ )
147
+ )
119
148
  );
120
149
  });
@@ -0,0 +1,83 @@
1
+ # build-events
2
+
3
+ A CI service whose events are watched by its members: `build.finished`, read from a store that can be asked what finished since a cursor, and `deploy.requested`, published by the action that requests a deploy, with no store to ask. Each member sees only the pipelines they may. It serves both, with the actions that cause them, as an MCP endpoint, as HTTP routes and as Effect RPC, delivers them by webhook, keeping its subscriptions past a restart, and watches them in process, over each surface, and from a command line. A second service, the notifier, watches failed builds by webhook. Two actions answer more than one value: `startBuild` reports progress as it runs, and `tailLog` answers a stream of a build's log lines.
4
+
5
+ ## Run it
6
+
7
+ From the repository root, after `vp install`:
8
+
9
+ ```sh
10
+ node --import tsx examples/build-events/src/server.ts
11
+ BUILDS_TOKEN=grace node --import tsx examples/build-events/src/cli.ts watch build.finished
12
+ BUILDS_TOKEN=grace PUBLIC_URL=https://<a tunnel to port 3001> node --import tsx examples/build-events/src/notifier.ts
13
+ vp test examples/build-events
14
+ ```
15
+
16
+ ## Layout
17
+
18
+ ```text
19
+ src/
20
+ ├── identity/
21
+ │ ├── Identity.ts contract: CurrentMember and the Login descriptor, with its principal and schema
22
+ │ ├── members.ts the demo members
23
+ │ └── verifier.ts Login's verifier: a token is a member's name
24
+ ├── builds/
25
+ │ ├── Builds.ts contract: the build schema, finishBuild, startBuild, tailLog and build.finished
26
+ │ ├── store.ts BuildStore: finished builds read by cursor, its epoch, and its Layer
27
+ │ ├── handlers.ts Action.implement: finishBuild, and startBuild reporting progress, each notifying build.finished
28
+ │ ├── log.ts Action.implement: tailLog, answering a stream of log lines
29
+ │ └── events.ts Event.implement: build.finished, a check source
30
+ ├── deploys/
31
+ │ ├── Deploys.ts contract: the deploy schema, requestDeploy and deploy.requested
32
+ │ ├── approvers.ts Approvers: who stands by to approve deploys, and its Layer
33
+ │ ├── handlers.ts Action.implement: requestDeploy, publishing deploy.requested
34
+ │ └── events.ts Event.implement: deploy.requested, a log source with a subscription hook
35
+ ├── Api.ts the HTTP binding: the actions' routes and the events' routes
36
+ ├── Rpc.ts the RPC binding: the actions' rpcs and the events' stream rpcs
37
+ ├── routes.ts the MCP endpoint, the HTTP routes and the rpcs, from one list of implementations, and webhook delivery
38
+ ├── server.ts entrypoint: the stores, the events announced, a body limit, webhook delivery's client, resolver and store, and Node's HTTP server
39
+ ├── commands.ts ActionCli.remote: a command per action, and `watch` per event type
40
+ ├── cli.ts entrypoint: the commands, calling the server over HTTP
41
+ ├── notifier/
42
+ │ ├── Notices.ts the failed builds the notifier was told of, and its Layer
43
+ │ └── routes.ts the notifier: its receiver, and its watch by webhook reading from it
44
+ └── notifier.ts entrypoint of the notifier, a second service
45
+ test/
46
+ ├── support/ as: the caller of a call or subscription; inMemory: what the routes need, in memory
47
+ ├── builds/ watching build.finished in process, a progress report and a log's stream
48
+ ├── deploys/ watching deploy.requested in process
49
+ ├── routes.test.ts watching both over MCP, streamed and polled, in memory
50
+ ├── http.test.ts watching both over HTTP, as SSE and as JSON pages, in memory
51
+ ├── rpc.test.ts watching over Effect RPC, in memory
52
+ ├── webhooks.test.ts subscribing a callback, and the signed POSTs it receives, in memory
53
+ ├── notifier.test.ts the notifier told of failed builds by webhook, both services in memory
54
+ └── commands.test.ts `watch build.finished` printing lines of JSON, in memory
55
+ ```
56
+
57
+ ## Where to look
58
+
59
+ | To see | Read |
60
+ | --- | --- |
61
+ | An event type beside the actions of its domain | `src/builds/Builds.ts` |
62
+ | A check source, its cursor the store's own | `src/builds/events.ts` |
63
+ | A cursor that tells a restart apart, as `truncated` | `src/builds/store.ts` |
64
+ | An action reporting progress as it runs | `src/builds/handlers.ts` |
65
+ | An action answering a stream | `src/builds/log.ts` |
66
+ | What a member may see, checked in the data access | `src/builds/store.ts` |
67
+ | A log source, and each subscriber's `match` | `src/deploys/events.ts` |
68
+ | Publishing to a log source from an action | `src/deploys/handlers.ts` |
69
+ | Waking a check source's subscribers after a write | `src/builds/handlers.ts` |
70
+ | Something set up upstream while a subscription lasts | `src/deploys/events.ts` |
71
+ | Watching events in process, with an action causing them | `test/builds/events.test.ts` |
72
+ | Serving events over MCP, and what it needs of `Login` | `src/routes.ts` |
73
+ | Watching over MCP, by stream and by poll | `test/routes.test.ts` |
74
+ | Binding events to HTTP routes beside the actions | `src/Api.ts` |
75
+ | Watching over HTTP, by SSE and by JSON pages | `test/http.test.ts` |
76
+ | Serving events as stream rpcs, and their serialization | `src/routes.ts` |
77
+ | Watching over RPC | `test/rpc.test.ts` |
78
+ | Watching from a command line, as lines of JSON | `src/commands.ts` |
79
+ | Delivering events by webhook, and what it needs | `src/routes.ts`, `src/server.ts` |
80
+ | Keeping webhook subscriptions past a restart | `src/server.ts`, `src/identity/Identity.ts` |
81
+ | Subscribing a callback, and checking each signature | `test/webhooks.test.ts` |
82
+ | Watching by webhook from a second service | `src/notifier/routes.ts` |
83
+ | Two services in memory, each calling the other | `test/notifier.test.ts` |
@@ -0,0 +1,27 @@
1
+ import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
2
+
3
+ import {
4
+ BuildFinished,
5
+ FinishBuild,
6
+ PipelineStatus,
7
+ StartBuild,
8
+ TailLog,
9
+ } from "./builds/Builds.js";
10
+ import { DeployRequested, RequestDeploy } from "./deploys/Deploys.js";
11
+ import { Login } from "./identity/Identity.js";
12
+
13
+ // The HTTP binding: each action at `POST /api/<name>`, and each event at
14
+ // `POST /api/events/<name>`, which answers a page of JSON, or, asked for `text/event-stream`,
15
+ // holds a stream open. It imports no server code, so a client imports it with the contracts.
16
+ export const Http = ActionHttp.make(
17
+ [
18
+ FinishBuild,
19
+ StartBuild,
20
+ TailLog,
21
+ BuildFinished,
22
+ PipelineStatus,
23
+ RequestDeploy,
24
+ DeployRequested,
25
+ ],
26
+ { authentication: Login }
27
+ );
@@ -0,0 +1,26 @@
1
+ import * as ActionRpc from "@gjermundgaraba/effect-actions/ActionRpc";
2
+
3
+ import {
4
+ BuildFinished,
5
+ FinishBuild,
6
+ PipelineStatus,
7
+ StartBuild,
8
+ TailLog,
9
+ } from "./builds/Builds.js";
10
+ import { DeployRequested, RequestDeploy } from "./deploys/Deploys.js";
11
+ import { Login } from "./identity/Identity.js";
12
+
13
+ // The RPC binding: each action an rpc named after it, and each event a stream rpc named after
14
+ // it, which pushes each occurrence as the server reads it. Plain data, as the HTTP binding is.
15
+ export const Rpc = ActionRpc.make(
16
+ [
17
+ FinishBuild,
18
+ StartBuild,
19
+ TailLog,
20
+ BuildFinished,
21
+ PipelineStatus,
22
+ RequestDeploy,
23
+ DeployRequested,
24
+ ],
25
+ { authentication: Login }
26
+ );
@@ -0,0 +1,108 @@
1
+ import * as Action from "@gjermundgaraba/effect-actions/Action";
2
+ import * as Event from "@gjermundgaraba/effect-actions/Event";
3
+ import { Schema } from "effect";
4
+
5
+ import { CurrentMember } from "../identity/Identity.js";
6
+
7
+ const Pipeline = Schema.String.annotate({
8
+ description: "A pipeline's name, such as api.",
9
+ });
10
+
11
+ const Status = Schema.Literals(["passed", "failed"]).annotate({
12
+ description: "How the build ended.",
13
+ });
14
+
15
+ export const Build = Schema.Struct({
16
+ id: Schema.String,
17
+ pipeline: Pipeline,
18
+ status: Status,
19
+ });
20
+
21
+ export type Build = typeof Build.Type;
22
+
23
+ // DEMO ONLY: a CI runner reports finished builds; here a member does, so the example can
24
+ // cause the event it watches.
25
+ export const FinishBuild = Action.make("finishBuild", {
26
+ description: "Record that a build of a pipeline finished.",
27
+ input: { pipeline: Pipeline, status: Status },
28
+ success: Build,
29
+ readOnly: false,
30
+ caller: CurrentMember,
31
+ });
32
+
33
+ // A build run here, which reports each step as it starts, before it answers how the build ended.
34
+ // A caller reads the steps only if it asks, with `onProgress`; each surface carries them its
35
+ // own way. They are a snapshot to show: a caller falling behind misses some, never the answer.
36
+ // What a caller must see every time is the answer, or `build.finished`, which this build causes.
37
+ export const StartBuild = Action.make("startBuild", {
38
+ description:
39
+ "Run a build of a pipeline, reporting each step, and record how it ended.",
40
+ input: { pipeline: Pipeline },
41
+ success: Build,
42
+ progress: {
43
+ step: Schema.String.annotate({
44
+ description: "The step that started, such as test.",
45
+ }),
46
+ done: Schema.Finite.annotate({
47
+ description: "How many steps finished before it.",
48
+ }),
49
+ total: Schema.Finite.annotate({
50
+ description: "How many steps the build has.",
51
+ }),
52
+ },
53
+ readOnly: false,
54
+ caller: CurrentMember,
55
+ });
56
+
57
+ // A build this member may not see is not found, as one that never ran: its id tells nothing.
58
+ export class BuildNotFound extends Schema.TaggedError<
59
+ BuildNotFound,
60
+ { readonly brand: unique symbol }
61
+ >()("BuildNotFound", { buildId: Schema.String }, { httpApiStatus: 404 }) {}
62
+
63
+ // An action answering a stream: its handler returns a `Stream` of lines, and every client's
64
+ // call returns one, each line arriving as the server sends it. HTTP, RPC and a command carry a
65
+ // stream; an MCP tool answers one result, the lines collected once the stream ends, so the MCP
66
+ // endpoint in `routes.ts` leaves it out rather than put a whole log in a model's context.
67
+ export const TailLog = Action.make("tailLog", {
68
+ description: "Stream a build's log, line by line.",
69
+ input: {
70
+ buildId: Schema.String.annotate({
71
+ description: "The build's id, such as build-1.",
72
+ }),
73
+ },
74
+ success: Action.Stream({ line: Schema.String }),
75
+ error: BuildNotFound,
76
+ readOnly: true,
77
+ caller: CurrentMember,
78
+ });
79
+
80
+ // An event type: what a subscription takes, what each occurrence carries, and who may
81
+ // subscribe. Like an action's contract, it imports no server code.
82
+ export const BuildFinished = Event.make("build.finished", {
83
+ description: "A build of a pipeline you may see finished.",
84
+ input: {
85
+ pipeline: Schema.optional(Pipeline).annotate({
86
+ description: "Only this pipeline's builds.",
87
+ }),
88
+ status: Schema.optional(Status).annotate({
89
+ description: "Only builds that ended so.",
90
+ }),
91
+ },
92
+ payload: Build,
93
+ caller: CurrentMember,
94
+ });
95
+
96
+ // An event whose occurrence is the whole value: how a pipeline's last build ended, `null` before
97
+ // its first. A subscriber is sent it as it is, then again each time a build of it finishes.
98
+ export const PipelineStatus = Event.make("pipeline.status", {
99
+ description:
100
+ "How a pipeline's last build ended: first as it is, then each time one finishes.",
101
+ input: { pipeline: Pipeline },
102
+ payload: {
103
+ status: Schema.NullOr(Status).annotate({
104
+ description: "How it ended; null before any did.",
105
+ }),
106
+ },
107
+ caller: CurrentMember,
108
+ });
@@ -0,0 +1,54 @@
1
+ import * as Event from "@gjermundgaraba/effect-actions/Event";
2
+ import { Effect } from "effect";
3
+
4
+ import { CurrentMember } from "../identity/Identity.js";
5
+ import { BuildFinished } from "./Builds.js";
6
+ import { BuildStore } from "./store.js";
7
+
8
+ // A check source: one function answers "what finished since this cursor?" for every
9
+ // subscriber, whether it polls or holds a stream open, on every surface. The builder runs once,
10
+ // at startup; the check runs per subscriber, as that subscriber.
11
+ export const buildEvents = Event.implement(
12
+ BuildFinished,
13
+ Effect.gen(function* () {
14
+ const store = yield* BuildStore;
15
+
16
+ return {
17
+ check: ({ pipeline, status }, cursor) =>
18
+ Effect.gen(function* () {
19
+ const member = yield* CurrentMember;
20
+
21
+ // A first subscription asks with `null`: the store answers the current position, so
22
+ // the subscriber starts from now.
23
+ const page = yield* store.finishedSince(
24
+ member.pipelines,
25
+ cursor,
26
+ 100
27
+ );
28
+
29
+ return {
30
+ cursor: page.cursor,
31
+ hasMore: page.hasMore,
32
+ // A cursor the store cannot resume from: the subscriber is sent a gap first.
33
+ truncated: page.truncated,
34
+ events: page.builds
35
+ .filter(
36
+ (build) => pipeline === undefined || build.pipeline === pipeline
37
+ )
38
+ .filter(
39
+ (build) => status === undefined || build.status === status
40
+ )
41
+ // The store's own id, within its epoch: a subscriber that sees a build twice,
42
+ // after a reconnect, drops the second, and never drops a build of a restarted store
43
+ // as one it saw before.
44
+ .map((build) => ({
45
+ eventId: `${page.epoch}/${build.id}`,
46
+ data: build,
47
+ })),
48
+ };
49
+ }),
50
+ };
51
+ }),
52
+ // Every member may watch; which builds each sees is the store's check, by pipeline.
53
+ { authorize: Event.allowAll }
54
+ );