@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,63 +1,83 @@
1
1
  import { describe, expect, it } from "@effect/vitest";
2
- import { Effect, Layer } from "effect";
3
- import { RpcClient, RpcSerialization, RpcServer } from "effect/rpc";
4
2
  import * as Action from "@gjermundgaraba/effect-actions/Action";
3
+ import * as Event from "@gjermundgaraba/effect-actions/Event";
5
4
  import * as Testing from "@gjermundgaraba/effect-actions/Testing";
5
+ import { Effect, Fiber, Layer, Stream } from "effect";
6
+ import { RpcClient, RpcSerialization, RpcServer } from "effect/rpc";
7
+ import { TestClock } from "effect/testing";
8
+
6
9
  import { openChat } from "../src/browser.js";
7
- import { chatOver } from "../src/page.js";
8
10
  import { MessageStore } from "../src/chat/store.js";
11
+ import { chatOver } from "../src/page.js";
9
12
  import { rpc } from "../src/routes.js";
10
13
 
11
- // The page's client against the real rpcs, in memory, served as in routes.test.ts. `openChat`
12
- // needs a client protocol and does not care which: a page gives it `connection(url)`, a
13
- // WebSocket, and this test Effect's HTTP protocol.
14
+ // The page's client against the real rpcs, in memory, served as in routes.test.ts, NDJSON
15
+ // included. `openChat` needs a client protocol and does not care which: a page gives it
16
+ // `connection(url)`, a WebSocket, and this test Effect's HTTP protocol.
14
17
  const served = RpcClient.layerProtocolHttp({ url: "/rpc" }).pipe(
15
- Layer.provide(RpcSerialization.layerJson),
18
+ Layer.provide(RpcSerialization.layerNdjson),
16
19
  Layer.provide(
17
20
  Testing.layer(
18
21
  rpc.pipe(
19
22
  Layer.provide(RpcServer.layerProtocolHttp({ path: "/rpc" })),
20
- Layer.provide(RpcSerialization.layerJson),
21
- ),
22
- ),
23
+ Layer.provide(RpcSerialization.layerNdjson)
24
+ )
25
+ )
23
26
  ),
24
- Layer.provide(MessageStore.layerMemory),
27
+ Layer.provide([MessageStore.layerMemory, Event.layerMemory])
25
28
  );
26
29
 
27
30
  describe("the page's client", () => {
28
- it.effect("posts and reads in the room it names, as the member its token names", () =>
29
- Effect.gen(function* () {
30
- const chat = yield* openChat;
31
- const first = yield* chat.room("lobby", "ada").post("Deploying at noon.");
32
- const second = yield* chat.room("lobby", "grace").post("Ready.");
31
+ it.effect(
32
+ "posts and reads in the room it names, as the member its token names",
33
+ () =>
34
+ Effect.gen(function* () {
35
+ const chat = yield* openChat;
33
36
 
34
- yield* chat.room("ops", "ada").post("Disk is full.");
37
+ const first = yield* chat
38
+ .room("lobby", "ada")
39
+ .post("Deploying at noon.");
35
40
 
36
- expect(first).toEqual({ id: 1, author: "ada", text: "Deploying at noon." });
37
- expect(yield* chat.room("lobby", "grace").read(first.id)).toEqual({
38
- access: "member",
39
- messages: [second],
40
- });
41
- expect(yield* chat.rooms).toEqual([
42
- { name: "lobby", messages: 2 },
43
- { name: "ops", messages: 1 },
44
- ]);
45
- }).pipe(Effect.provide(served)),
41
+ const second = yield* chat.room("lobby", "grace").post("Ready.");
42
+
43
+ yield* chat.room("ops", "ada").post("Disk is full.");
44
+
45
+ expect(first).toStrictEqual({
46
+ id: 1,
47
+ author: "ada",
48
+ text: "Deploying at noon.",
49
+ });
50
+ expect(yield* chat.room("lobby", "grace").read(first.id)).toStrictEqual(
51
+ {
52
+ access: "member",
53
+ messages: [second],
54
+ }
55
+ );
56
+ expect(yield* chat.rooms).toStrictEqual([
57
+ { name: "lobby", messages: 2 },
58
+ { name: "ops", messages: 1 },
59
+ ]);
60
+ }).pipe(Effect.provide(served))
46
61
  );
47
62
 
48
- it.effect("turns each declared error of a removal into what the page shows", () =>
49
- Effect.gen(function* () {
50
- const chat = yield* openChat;
51
- const ada = chat.room("lobby", "ada");
52
- const grace = chat.room("lobby", "grace");
53
- const { id } = yield* ada.post("Oops.");
54
-
55
- expect(yield* grace.remove(id)).toBe("not-yours");
56
- expect(yield* grace.read(0)).toMatchObject({ messages: [{ id }] });
57
- expect(yield* ada.remove(id)).toBe("removed");
58
- expect(yield* grace.read(0)).toEqual({ access: "member", messages: [] });
59
- expect(yield* ada.remove(id)).toBe("removed");
60
- }).pipe(Effect.provide(served)),
63
+ it.effect(
64
+ "turns each declared error of a removal into what the page shows",
65
+ () =>
66
+ Effect.gen(function* () {
67
+ const chat = yield* openChat;
68
+ const ada = chat.room("lobby", "ada");
69
+ const grace = chat.room("lobby", "grace");
70
+ const { id } = yield* ada.post("Oops.");
71
+
72
+ expect(yield* grace.remove(id)).toBe("not-yours");
73
+ expect(yield* grace.read(0)).toMatchObject({ messages: [{ id }] });
74
+ expect(yield* ada.remove(id)).toBe("removed");
75
+ expect(yield* grace.read(0)).toStrictEqual({
76
+ access: "member",
77
+ messages: [],
78
+ });
79
+ expect(yield* ada.remove(id)).toBe("removed");
80
+ }).pipe(Effect.provide(served))
61
81
  );
62
82
 
63
83
  it.effect("turns each refusal of a read into the access the page shows", () =>
@@ -66,15 +86,62 @@ describe("the page's client", () => {
66
86
 
67
87
  yield* chat.room("ops", "ada").post("Disk is full.");
68
88
 
69
- expect(yield* chat.room("ops", "grace").read(0)).toEqual({
89
+ expect(yield* chat.room("ops", "grace").read(0)).toStrictEqual({
70
90
  access: "not-a-member",
71
91
  messages: [],
72
92
  });
73
- expect(yield* chat.room("ops", "expired").read(0)).toEqual({
93
+ expect(yield* chat.room("ops", "expired").read(0)).toStrictEqual({
74
94
  access: "signed-out",
75
95
  messages: [],
76
96
  });
77
- }).pipe(Effect.provide(served)),
97
+ }).pipe(Effect.provide(served))
98
+ );
99
+
100
+ it.effect(
101
+ "streams the posts after what the page read, one posted in between included",
102
+ () =>
103
+ Effect.gen(function* () {
104
+ const chat = yield* openChat;
105
+ const lobby = chat.room("lobby", "grace");
106
+
107
+ yield* chat.room("lobby", "ada").post("Deploying at noon.");
108
+
109
+ const { messages } = yield* lobby.read(0);
110
+
111
+ // Posted after the page read and before it watches: the watch from the last id read
112
+ // still sends it.
113
+ yield* chat.room("lobby", "ada").post("Done.");
114
+
115
+ const watched = yield* lobby
116
+ .messages(messages.at(-1)?.id ?? 0)
117
+ .pipe(Stream.take(1), Stream.runCollect);
118
+
119
+ expect(watched).toMatchObject([
120
+ { _tag: "Occurrence", data: { id: 2, text: "Done." } },
121
+ ]);
122
+ }).pipe(Effect.provide(served))
123
+ );
124
+
125
+ it.effect(
126
+ "sends a gap first for an id the room never gave, then the posts after its last",
127
+ () =>
128
+ Effect.gen(function* () {
129
+ const chat = yield* openChat;
130
+
131
+ // As after a restart emptied the demo's memory store: the page holds id 7.
132
+ const watching = yield* chat
133
+ .room("lobby", "grace")
134
+ .messages(7)
135
+ .pipe(Stream.take(2), Stream.runCollect, Effect.forkChild);
136
+
137
+ yield* Effect.repeat(TestClock.adjust("0 millis"), { times: 10 });
138
+ yield* chat.room("lobby", "ada").post("Hello again.");
139
+
140
+ expect(yield* Fiber.join(watching)).toMatchObject([
141
+ { _tag: "Gap" },
142
+ { _tag: "Occurrence", data: { id: 1, text: "Hello again." } },
143
+ ]);
144
+ }).pipe(Effect.provide(served))
78
145
  );
79
146
 
80
147
  // `connect(url)` is this with a WebSocket. Each test's runtime builds its own layer, and so
@@ -84,23 +151,33 @@ describe("the page's client", () => {
84
151
 
85
152
  const posted = await chat.room("lobby", "ada").post("Deploying at noon.");
86
153
 
87
- expect(posted).toEqual({ id: 1, author: "ada", text: "Deploying at noon." });
88
- expect(await chat.room("lobby", "grace").read(0)).toEqual({
154
+ expect(posted).toStrictEqual({
155
+ id: 1,
156
+ author: "ada",
157
+ text: "Deploying at noon.",
158
+ });
159
+ await expect(chat.room("lobby", "grace").read(0)).resolves.toStrictEqual({
89
160
  access: "member",
90
161
  messages: [posted],
91
162
  });
92
- expect(await chat.room("lobby", "grace").remove(posted.id)).toBe("not-yours");
93
- expect(await chat.room("ops", "expired").read(0)).toEqual({
163
+ await expect(chat.room("lobby", "grace").remove(posted.id)).resolves.toBe(
164
+ "not-yours"
165
+ );
166
+ await expect(chat.room("ops", "expired").read(0)).resolves.toStrictEqual({
94
167
  access: "signed-out",
95
168
  messages: [],
96
169
  });
97
- expect(await chat.rooms()).toEqual([{ name: "lobby", messages: 1 }]);
170
+ await expect(chat.rooms()).resolves.toStrictEqual([
171
+ { name: "lobby", messages: 1 },
172
+ ]);
98
173
 
99
174
  // A failure `openChat` does not handle rejects: here, a text the contract refuses.
100
- await expect(chat.room("lobby", "ada").post("")).rejects.toBeInstanceOf(Action.InvalidInput);
175
+ await expect(chat.room("lobby", "ada").post("")).rejects.toBeInstanceOf(
176
+ Action.InvalidInput
177
+ );
101
178
 
102
179
  await chat.close();
103
180
 
104
- await expect(chat.rooms()).rejects.toThrow();
181
+ await expect(chat.rooms()).rejects.toThrow("disposed");
105
182
  });
106
183
  });
@@ -1,7 +1,11 @@
1
1
  import { describe, expect, it } from "@effect/vitest";
2
- import { Effect, flow } from "effect";
3
2
  import * as Action from "@gjermundgaraba/effect-actions/Action";
4
- import { NotAuthor } from "../../src/chat/Chat.js";
3
+ import * as Event from "@gjermundgaraba/effect-actions/Event";
4
+ import { Effect, Fiber, flow, Stream } from "effect";
5
+ import { TestClock } from "effect/testing";
6
+
7
+ import { MessagePosted, NotAuthor } from "../../src/chat/Chat.js";
8
+ import { messageEvents } from "../../src/chat/events.js";
5
9
  import { messages } from "../../src/chat/handlers.js";
6
10
  import { CurrentRoom } from "../../src/chat/room.js";
7
11
  import { MessageStore } from "../../src/chat/store.js";
@@ -15,7 +19,19 @@ import { members } from "../../src/identity/members.js";
15
19
  const as = (member: keyof typeof members, room: string) =>
16
20
  flow(
17
21
  Effect.provideService(CurrentMember, members[member]),
18
- Effect.provideService(CurrentRoom, room),
22
+ Effect.provideService(CurrentRoom, room)
23
+ );
24
+
25
+ const posted = (text: string) =>
26
+ Effect.flatMap(Action.client([messages, messageEvents]), (client) =>
27
+ client.postMessage({ text }).pipe(as("ada", "lobby"))
28
+ );
29
+
30
+ const watchedFrom = (cursor: string) =>
31
+ Effect.flatMap(Action.client([messages, messageEvents]), (client) =>
32
+ client
33
+ .watch(MessagePosted, {}, { cursor })
34
+ .pipe(Stream.take(1), Stream.runCollect, as("grace", "lobby"))
19
35
  );
20
36
 
21
37
  describe("messages, in process", () => {
@@ -23,47 +39,223 @@ describe("messages, in process", () => {
23
39
  Effect.gen(function* () {
24
40
  const client = yield* Action.client(messages);
25
41
 
26
- yield* client.postMessage({ text: "Deploying at noon." }).pipe(as("ada", "lobby"));
27
- yield* client.postMessage({ text: "Disk is full." }).pipe(as("ada", "ops"));
42
+ yield* client
43
+ .postMessage({ text: "Deploying at noon." })
44
+ .pipe(as("ada", "lobby"));
45
+ yield* client
46
+ .postMessage({ text: "Disk is full." })
47
+ .pipe(as("ada", "ops"));
28
48
  yield* client.postMessage({ text: "Ready." }).pipe(as("grace", "lobby"));
29
49
 
30
- expect(yield* client.readMessages({ after: 1 }).pipe(as("grace", "lobby"))).toEqual({
50
+ expect(
51
+ yield* client.readMessages({ after: 1 }).pipe(as("grace", "lobby"))
52
+ ).toStrictEqual({
31
53
  messages: [{ id: 2, author: "grace", text: "Ready." }],
32
54
  });
33
- expect(yield* client.readMessages({ after: 0 }).pipe(as("ada", "ops"))).toEqual({
55
+ expect(
56
+ yield* client.readMessages({ after: 0 }).pipe(as("ada", "ops"))
57
+ ).toStrictEqual({
34
58
  messages: [{ id: 1, author: "ada", text: "Disk is full." }],
35
59
  });
36
- }).pipe(Effect.provide(MessageStore.layerMemory)),
60
+ }).pipe(Effect.provide([MessageStore.layerMemory, Event.layerMemory]))
37
61
  );
38
62
 
39
63
  it.effect("lets only its author delete a message", () =>
40
64
  Effect.gen(function* () {
41
65
  const client = yield* Action.client(messages);
42
- const { id } = yield* client.postMessage({ text: "Oops." }).pipe(as("ada", "lobby"));
43
66
 
44
- expect(yield* Effect.flip(client.deleteMessage({ id }).pipe(as("grace", "lobby")))).toEqual(
45
- new NotAuthor({ id }),
46
- );
67
+ const { id } = yield* client
68
+ .postMessage({ text: "Oops." })
69
+ .pipe(as("ada", "lobby"));
70
+
71
+ expect(
72
+ yield* Effect.flip(
73
+ client.deleteMessage({ id }).pipe(as("grace", "lobby"))
74
+ )
75
+ ).toStrictEqual(new NotAuthor({ id }));
47
76
 
48
77
  yield* client.deleteMessage({ id }).pipe(as("ada", "lobby"));
49
78
 
50
- expect(yield* client.readMessages({ after: 0 }).pipe(as("grace", "lobby"))).toEqual({
79
+ expect(
80
+ yield* client.readMessages({ after: 0 }).pipe(as("grace", "lobby"))
81
+ ).toStrictEqual({
51
82
  messages: [],
52
83
  });
53
- }).pipe(Effect.provide(MessageStore.layerMemory)),
84
+ }).pipe(Effect.provide([MessageStore.layerMemory, Event.layerMemory]))
85
+ );
86
+
87
+ it.effect(
88
+ "refuses a member a room they have not joined, before the handler runs",
89
+ () =>
90
+ Effect.gen(function* () {
91
+ const client = yield* Action.client(messages);
92
+
93
+ const post = client
94
+ .postMessage({ text: "Hello?" })
95
+ .pipe(as("grace", "ops"));
96
+
97
+ const refused = yield* Effect.flip(post);
98
+
99
+ expect(refused).toBeInstanceOf(Action.Forbidden);
100
+ expect(refused.message).toBe("Not a member of ops.");
101
+ expect(
102
+ yield* client.readMessages({ after: 0 }).pipe(as("ada", "ops"))
103
+ ).toStrictEqual({
104
+ messages: [],
105
+ });
106
+ }).pipe(Effect.provide([MessageStore.layerMemory, Event.layerMemory]))
107
+ );
108
+
109
+ it.effect(
110
+ "delivers each message posted to a watched room as it is posted, and none of another",
111
+ () =>
112
+ Effect.gen(function* () {
113
+ const client = yield* Action.client([messages, messageEvents]);
114
+ const { epoch } = yield* MessageStore;
115
+
116
+ const watching = yield* client
117
+ .watch(MessagePosted, {})
118
+ .pipe(
119
+ Stream.take(1),
120
+ Stream.runCollect,
121
+ as("grace", "lobby"),
122
+ Effect.forkChild
123
+ );
124
+
125
+ // The watch starts from now: let it take its first position before anyone posts.
126
+ yield* TestClock.adjust("0 millis");
127
+ yield* client
128
+ .postMessage({ text: "Disk is full." })
129
+ .pipe(as("ada", "ops"));
130
+ yield* client.postMessage({ text: "Ready." }).pipe(as("ada", "lobby"));
131
+
132
+ // `postMessage` notifies: the watch reads at once, with no wait for its next read.
133
+ expect(yield* Fiber.join(watching)).toMatchObject([
134
+ {
135
+ _tag: "Occurrence",
136
+ eventId: `${epoch}:lobby:1`,
137
+ data: { id: 1, author: "ada", text: "Ready." },
138
+ },
139
+ ]);
140
+ }).pipe(Effect.provide([MessageStore.layerMemory, Event.layerMemory]))
141
+ );
142
+
143
+ it.effect(
144
+ "names the first message after a restart apart from the one before it",
145
+ () =>
146
+ Effect.gen(function* () {
147
+ const firstReceived = Effect.gen(function* () {
148
+ const client = yield* Action.client([messages, messageEvents]);
149
+
150
+ const watching = yield* client
151
+ .watch(MessagePosted, {})
152
+ .pipe(
153
+ Stream.take(1),
154
+ Stream.runCollect,
155
+ as("grace", "lobby"),
156
+ Effect.forkChild
157
+ );
158
+
159
+ yield* TestClock.adjust("0 millis");
160
+ yield* client
161
+ .postMessage({ text: "Ready." })
162
+ .pipe(as("ada", "lobby"));
163
+
164
+ return yield* Fiber.join(watching);
165
+ }).pipe(Effect.provide([MessageStore.layerMemory, Event.layerMemory]));
166
+
167
+ const received = [...(yield* firstReceived), ...(yield* firstReceived)];
168
+
169
+ // Both are the room's message 1, and a watch that saw the first still reads the second.
170
+ expect(received).toMatchObject([
171
+ { data: { id: 1 } },
172
+ { data: { id: 1 } },
173
+ ]);
174
+ expect(
175
+ new Set(
176
+ received.map((each) =>
177
+ each._tag === "Occurrence" ? each.eventId : ""
178
+ )
179
+ ).size
180
+ ).toBe(2);
181
+ })
54
182
  );
55
183
 
56
- it.effect("refuses a member a room they have not joined, before the handler runs", () =>
184
+ it.effect("refuses a watch of a room the member has not joined", () =>
57
185
  Effect.gen(function* () {
58
- const client = yield* Action.client(messages);
59
- const post = client.postMessage({ text: "Hello?" }).pipe(as("grace", "ops"));
60
- const refused = yield* Effect.flip(post);
186
+ const client = yield* Action.client([messages, messageEvents]);
187
+
188
+ const refused = yield* Effect.flip(
189
+ Stream.runDrain(client.watch(MessagePosted, {})).pipe(
190
+ as("grace", "ops")
191
+ )
192
+ );
61
193
 
62
194
  expect(refused).toBeInstanceOf(Action.Forbidden);
63
- expect(refused.message).toBe("Not a member of ops.");
64
- expect(yield* client.readMessages({ after: 0 }).pipe(as("ada", "ops"))).toEqual({
65
- messages: [],
66
- });
67
- }).pipe(Effect.provide(MessageStore.layerMemory)),
195
+ }).pipe(Effect.provide([MessageStore.layerMemory, Event.layerMemory]))
196
+ );
197
+
198
+ it.effect(
199
+ "answers a cursor that names no id of the room with a gap, then reads on",
200
+ () =>
201
+ Effect.gen(function* () {
202
+ const client = yield* Action.client([messages, messageEvents]);
203
+ const { epoch } = yield* MessageStore;
204
+
205
+ yield* client.postMessage({ text: "Ready." }).pipe(as("ada", "lobby"));
206
+
207
+ // An event's id, such as `<epoch>:lobby:1`, is no cursor: the cursor is `<epoch>:<id>`.
208
+ const received = yield* client
209
+ .watch(MessagePosted, {}, { cursor: "lobby:1" })
210
+ .pipe(Stream.take(1), Stream.runCollect, as("grace", "lobby"));
211
+
212
+ expect(received).toMatchObject([{ _tag: "Gap", cursor: `${epoch}:1` }]);
213
+ }).pipe(Effect.provide([MessageStore.layerMemory, Event.layerMemory]))
214
+ );
215
+
216
+ it.effect(
217
+ "answers a cursor with more parts than `<epoch>:<id>` with a gap",
218
+ () =>
219
+ Effect.gen(function* () {
220
+ const client = yield* Action.client([messages, messageEvents]);
221
+ const { epoch } = yield* MessageStore;
222
+
223
+ yield* client
224
+ .postMessage({ text: "Disk is full." })
225
+ .pipe(as("ada", "lobby"));
226
+ yield* client.postMessage({ text: "Ready." }).pipe(as("ada", "lobby"));
227
+
228
+ const received = yield* client
229
+ .watch(MessagePosted, {}, { cursor: `${epoch}:1:lobby` })
230
+ .pipe(Stream.take(1), Stream.runCollect, as("grace", "lobby"));
231
+
232
+ expect(received).toMatchObject([{ _tag: "Gap", cursor: `${epoch}:2` }]);
233
+ }).pipe(Effect.provide([MessageStore.layerMemory, Event.layerMemory]))
234
+ );
235
+
236
+ it.effect(
237
+ "answers a cursor from before a restart with a gap, however far the store has counted",
238
+ () =>
239
+ Effect.gen(function* () {
240
+ const run = Effect.provide([
241
+ MessageStore.layerMemory,
242
+ Event.layerMemory,
243
+ ]);
244
+
245
+ const [before] = yield* Effect.andThen(
246
+ posted("Before the restart."),
247
+ watchedFrom("0")
248
+ ).pipe(run);
249
+
250
+ const after = yield* Effect.andThen(
251
+ posted("After the restart."),
252
+ watchedFrom(
253
+ before?._tag === "Occurrence" ? (before.cursor ?? "") : ""
254
+ )
255
+ ).pipe(run);
256
+
257
+ expect(before).toMatchObject({ _tag: "Occurrence", data: { id: 1 } });
258
+ expect(after).toMatchObject([{ _tag: "Gap" }]);
259
+ })
68
260
  );
69
261
  });