@gjermundgaraba/effect-actions 0.19.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (326) hide show
  1. package/README.md +19 -41
  2. package/dist/Action.d.mts +140 -48
  3. package/dist/Action.mjs +88 -44
  4. package/dist/ActionCli.d.mts +70 -35
  5. package/dist/ActionCli.mjs +53 -34
  6. package/dist/ActionHttp.d.mts +70 -38
  7. package/dist/ActionHttp.mjs +76 -25
  8. package/dist/ActionMcp.d.mts +56 -24
  9. package/dist/ActionMcp.mjs +104 -61
  10. package/dist/ActionRpc.d.mts +222 -57
  11. package/dist/ActionRpc.mjs +301 -49
  12. package/dist/ActionToolkit.d.mts +22 -7
  13. package/dist/ActionToolkit.mjs +37 -14
  14. package/dist/Authentication.d.mts +42 -12
  15. package/dist/Authentication.mjs +38 -32
  16. package/dist/Event.d.mts +370 -0
  17. package/dist/Event.mjs +103 -0
  18. package/dist/Testing.d.mts +1 -3
  19. package/dist/Testing.mjs +2 -3
  20. package/dist/Webhook.d.mts +146 -0
  21. package/dist/Webhook.mjs +72 -0
  22. package/dist/authentication/provider.d.mts +35 -12
  23. package/dist/authentication/provider.mjs +2 -3
  24. package/dist/authentication/refusal.mjs +3 -4
  25. package/dist/cli/commands.d.mts +9 -7
  26. package/dist/cli/commands.mjs +85 -35
  27. package/dist/contract/ast.mjs +5 -3
  28. package/dist/contract/authorizer.d.mts +8 -0
  29. package/dist/contract/authorizer.mjs +20 -0
  30. package/dist/contract/call.d.mts +9 -8
  31. package/dist/contract/call.mjs +49 -8
  32. package/dist/contract/callback.d.mts +7 -0
  33. package/dist/contract/callback.mjs +27 -0
  34. package/dist/contract/caller.d.mts +12 -3
  35. package/dist/contract/caller.mjs +23 -5
  36. package/dist/contract/canonical.mjs +10 -0
  37. package/dist/contract/codec.d.mts +9 -0
  38. package/dist/contract/codec.mjs +9 -0
  39. package/dist/contract/codes.mjs +81 -0
  40. package/dist/contract/declared.d.mts +0 -2
  41. package/dist/contract/declared.mjs +26 -11
  42. package/dist/contract/digest.mjs +6 -0
  43. package/dist/contract/errors.d.mts +9 -5
  44. package/dist/contract/errors.mjs +2 -3
  45. package/dist/contract/feed.d.mts +127 -0
  46. package/dist/contract/feed.mjs +249 -0
  47. package/dist/contract/hints.mjs +1 -2
  48. package/dist/contract/hub.d.mts +94 -0
  49. package/dist/contract/hub.mjs +133 -0
  50. package/dist/contract/idempotency.d.mts +47 -0
  51. package/dist/contract/idempotency.mjs +99 -0
  52. package/dist/contract/implementation.d.mts +73 -51
  53. package/dist/contract/implementation.mjs +94 -60
  54. package/dist/contract/input-check.d.mts +15 -0
  55. package/dist/contract/input-check.mjs +16 -0
  56. package/dist/contract/lease.mjs +28 -0
  57. package/dist/contract/listed.d.mts +4 -0
  58. package/dist/contract/listed.mjs +22 -0
  59. package/dist/contract/log.d.mts +41 -0
  60. package/dist/contract/log.mjs +144 -0
  61. package/dist/contract/media.d.mts +1 -3
  62. package/dist/contract/media.mjs +1 -2
  63. package/dist/contract/names.mjs +15 -6
  64. package/dist/contract/occurrence.d.mts +29 -0
  65. package/dist/contract/occurrence.mjs +45 -0
  66. package/dist/contract/pages.d.mts +13 -0
  67. package/dist/contract/pages.mjs +39 -0
  68. package/dist/contract/progress.d.mts +26 -0
  69. package/dist/contract/progress.mjs +72 -0
  70. package/dist/contract/push.d.mts +20 -0
  71. package/dist/contract/push.mjs +96 -0
  72. package/dist/contract/receiving.d.mts +29 -0
  73. package/dist/contract/receiving.mjs +42 -0
  74. package/dist/contract/recent.mjs +12 -0
  75. package/dist/contract/reported.mjs +16 -0
  76. package/dist/contract/serving.mjs +63 -0
  77. package/dist/contract/status.mjs +15 -7
  78. package/dist/contract/stream.d.mts +34 -0
  79. package/dist/contract/stream.mjs +31 -0
  80. package/dist/contract/subscribe.d.mts +45 -0
  81. package/dist/contract/subscribe.mjs +50 -0
  82. package/dist/contract/transient.mjs +28 -0
  83. package/dist/contract/watch.d.mts +18 -0
  84. package/dist/contract/watch.mjs +105 -0
  85. package/dist/contract/webhooks.d.mts +42 -0
  86. package/dist/contract/webhooks.mjs +53 -0
  87. package/dist/http/client.d.mts +37 -14
  88. package/dist/http/client.mjs +52 -14
  89. package/dist/http/events.d.mts +56 -0
  90. package/dist/http/events.mjs +102 -0
  91. package/dist/http/placed.d.mts +36 -0
  92. package/dist/http/placed.mjs +71 -0
  93. package/dist/http/schema-errors.mjs +2 -3
  94. package/dist/http/streamed.d.mts +21 -0
  95. package/dist/http/streamed.mjs +94 -0
  96. package/dist/http/watch.mjs +50 -0
  97. package/dist/mcp/bridge.mjs +283 -0
  98. package/dist/mcp/client.d.mts +30 -99
  99. package/dist/mcp/client.mjs +60 -134
  100. package/dist/mcp/events.mjs +142 -0
  101. package/dist/mcp/progress.mjs +34 -0
  102. package/dist/mcp/protocol.mjs +40 -15
  103. package/dist/mcp/reply.d.mts +79 -0
  104. package/dist/mcp/reply.mjs +99 -0
  105. package/dist/mcp/request.d.mts +40 -0
  106. package/dist/mcp/request.mjs +51 -0
  107. package/dist/mcp/watch.mjs +115 -0
  108. package/dist/rpc/watch.mjs +39 -0
  109. package/dist/stdio/console.mjs +14 -7
  110. package/dist/testing/memory.d.mts +1 -3
  111. package/dist/testing/memory.mjs +4 -5
  112. package/dist/webhook/address.mjs +47 -0
  113. package/dist/webhook/attempt.mjs +65 -0
  114. package/dist/webhook/receiver.mjs +67 -0
  115. package/dist/webhook/signing.mjs +28 -0
  116. package/dist/webhook/stored.mjs +57 -0
  117. package/dist/webhook/subscriptions.mjs +342 -0
  118. package/docs/Action-failures.md +2 -2
  119. package/docs/Action-streams.md +115 -0
  120. package/docs/Action.md +58 -124
  121. package/docs/ActionCli-failures.md +1 -2
  122. package/docs/ActionCli.md +83 -88
  123. package/docs/ActionHttp-failures.md +3 -3
  124. package/docs/ActionHttp.md +90 -70
  125. package/docs/ActionMcp-failures.md +2 -1
  126. package/docs/ActionMcp.md +120 -92
  127. package/docs/ActionRpc-events.md +126 -0
  128. package/docs/ActionRpc-failures.md +1 -2
  129. package/docs/ActionRpc.md +108 -79
  130. package/docs/ActionToolkit-failures.md +1 -2
  131. package/docs/ActionToolkit.md +33 -90
  132. package/docs/Authentication-advanced.md +57 -61
  133. package/docs/Authentication-failures.md +5 -4
  134. package/docs/Authentication.md +23 -58
  135. package/docs/Event-failures.md +15 -0
  136. package/docs/Event.md +281 -0
  137. package/docs/README.md +51 -82
  138. package/docs/Testing-failures.md +1 -5
  139. package/docs/Testing.md +126 -86
  140. package/docs/Webhook-failures.md +8 -0
  141. package/docs/Webhook.md +319 -0
  142. package/docs/setup-failures.md +2 -3
  143. package/docs/setup.md +32 -53
  144. package/examples/README.md +12 -37
  145. package/examples/assistant/README.md +21 -26
  146. package/examples/assistant/src/agent.ts +66 -24
  147. package/examples/assistant/src/cli.ts +7 -4
  148. package/examples/assistant/src/commands.ts +9 -4
  149. package/examples/assistant/src/identity/Identity.ts +5 -4
  150. package/examples/assistant/src/identity/authorize.ts +13 -5
  151. package/examples/assistant/src/identity/handlers.ts +3 -2
  152. package/examples/assistant/src/identity/permissions.ts +9 -3
  153. package/examples/assistant/src/platform/tasks-file.ts +15 -11
  154. package/examples/assistant/src/scripted.ts +7 -4
  155. package/examples/assistant/src/tasks/Tasks.ts +20 -6
  156. package/examples/assistant/src/tasks/handlers.ts +9 -5
  157. package/examples/assistant/src/tasks/store.ts +38 -11
  158. package/examples/assistant/src/toolkit.ts +11 -7
  159. package/examples/assistant/test/commands.test.ts +53 -27
  160. package/examples/assistant/test/identity/identity.test.ts +27 -11
  161. package/examples/assistant/test/platform/tasks-file.test.ts +39 -23
  162. package/examples/assistant/test/tasks/tasks.test.ts +62 -34
  163. package/examples/assistant/test/toolkit.test.ts +104 -75
  164. package/examples/build-events/README.md +83 -0
  165. package/examples/build-events/src/Api.ts +27 -0
  166. package/examples/build-events/src/Rpc.ts +26 -0
  167. package/examples/build-events/src/builds/Builds.ts +108 -0
  168. package/examples/build-events/src/builds/events.ts +54 -0
  169. package/examples/build-events/src/builds/handlers.ts +58 -0
  170. package/examples/build-events/src/builds/log.ts +30 -0
  171. package/examples/build-events/src/builds/status.ts +28 -0
  172. package/examples/build-events/src/builds/store.ts +130 -0
  173. package/examples/build-events/src/cli.ts +18 -0
  174. package/examples/build-events/src/commands.ts +32 -0
  175. package/examples/build-events/src/deploys/Deploys.ts +44 -0
  176. package/examples/build-events/src/deploys/approvers.ts +55 -0
  177. package/examples/build-events/src/deploys/events.ts +32 -0
  178. package/examples/build-events/src/deploys/handlers.ts +38 -0
  179. package/examples/build-events/src/identity/Identity.ts +29 -0
  180. package/examples/build-events/src/identity/members.ts +7 -0
  181. package/examples/build-events/src/identity/verifier.ts +22 -0
  182. package/examples/build-events/src/notifier/Notices.ts +26 -0
  183. package/examples/build-events/src/notifier/routes.ts +71 -0
  184. package/examples/build-events/src/notifier.ts +48 -0
  185. package/examples/build-events/src/routes.ts +71 -0
  186. package/examples/build-events/src/server.ts +62 -0
  187. package/examples/build-events/test/builds/events.test.ts +68 -0
  188. package/examples/build-events/test/builds/log.test.ts +71 -0
  189. package/examples/build-events/test/builds/progress.test.ts +57 -0
  190. package/examples/build-events/test/builds/status.test.ts +42 -0
  191. package/examples/build-events/test/commands.test.ts +103 -0
  192. package/examples/build-events/test/deploys/events.test.ts +52 -0
  193. package/examples/build-events/test/http.test.ts +95 -0
  194. package/examples/build-events/test/notifier.test.ts +80 -0
  195. package/examples/build-events/test/routes.test.ts +96 -0
  196. package/examples/build-events/test/rpc.test.ts +84 -0
  197. package/examples/build-events/test/support/as.ts +8 -0
  198. package/examples/build-events/test/support/memory.ts +43 -0
  199. package/examples/build-events/test/webhooks.test.ts +143 -0
  200. package/examples/kitchen-sink/README.md +178 -166
  201. package/examples/kitchen-sink/src/Actions.ts +11 -3
  202. package/examples/kitchen-sink/src/Api.ts +12 -6
  203. package/examples/kitchen-sink/src/Rpc.ts +7 -5
  204. package/examples/kitchen-sink/src/admin/Admin.ts +21 -10
  205. package/examples/kitchen-sink/src/admin/authenticate.ts +8 -4
  206. package/examples/kitchen-sink/src/admin/authorize.ts +13 -5
  207. package/examples/kitchen-sink/src/admin/grants.ts +5 -3
  208. package/examples/kitchen-sink/src/admin/handlers.ts +9 -5
  209. package/examples/kitchen-sink/src/agent.ts +14 -6
  210. package/examples/kitchen-sink/src/assistant/Assistant.ts +21 -15
  211. package/examples/kitchen-sink/src/assistant/conversations.ts +14 -9
  212. package/examples/kitchen-sink/src/assistant/demo-model.ts +2 -2
  213. package/examples/kitchen-sink/src/assistant/handlers.ts +42 -22
  214. package/examples/kitchen-sink/src/attachments/Attachments.ts +6 -6
  215. package/examples/kitchen-sink/src/attachments/download.ts +7 -6
  216. package/examples/kitchen-sink/src/attachments/handlers.ts +9 -4
  217. package/examples/kitchen-sink/src/attachments/store.ts +25 -18
  218. package/examples/kitchen-sink/src/browser.ts +10 -4
  219. package/examples/kitchen-sink/src/cli.ts +28 -12
  220. package/examples/kitchen-sink/src/config.ts +1 -1
  221. package/examples/kitchen-sink/src/identity/Identity.ts +24 -10
  222. package/examples/kitchen-sink/src/identity/authenticate.ts +19 -11
  223. package/examples/kitchen-sink/src/identity/authorize.ts +14 -4
  224. package/examples/kitchen-sink/src/identity/directory.ts +16 -7
  225. package/examples/kitchen-sink/src/identity/handlers.ts +1 -0
  226. package/examples/kitchen-sink/src/identity/members.ts +7 -2
  227. package/examples/kitchen-sink/src/identity/quota.ts +23 -20
  228. package/examples/kitchen-sink/src/issues/Issues.ts +91 -15
  229. package/examples/kitchen-sink/src/issues/assistant.ts +4 -2
  230. package/examples/kitchen-sink/src/issues/events.ts +46 -0
  231. package/examples/kitchen-sink/src/issues/export.ts +35 -0
  232. package/examples/kitchen-sink/src/issues/guide.ts +4 -3
  233. package/examples/kitchen-sink/src/issues/handlers.ts +20 -7
  234. package/examples/kitchen-sink/src/issues/store.ts +156 -67
  235. package/examples/kitchen-sink/src/issues/write-limit.ts +44 -18
  236. package/examples/kitchen-sink/src/memory.ts +11 -2
  237. package/examples/kitchen-sink/src/ops.ts +10 -5
  238. package/examples/kitchen-sink/src/request-policy.ts +25 -9
  239. package/examples/kitchen-sink/src/routes.ts +67 -27
  240. package/examples/kitchen-sink/src/sdk.ts +22 -12
  241. package/examples/kitchen-sink/src/seed.ts +15 -9
  242. package/examples/kitchen-sink/src/server.ts +24 -10
  243. package/examples/kitchen-sink/src/stdio.ts +9 -3
  244. package/examples/kitchen-sink/src/workspaces/Workspaces.ts +10 -7
  245. package/examples/kitchen-sink/src/workspaces/handlers.ts +3 -2
  246. package/examples/kitchen-sink/src/workspaces/registry.ts +16 -10
  247. package/examples/kitchen-sink/src/workspaces/tenant.ts +12 -5
  248. package/examples/kitchen-sink/test/admin/admin.test.ts +38 -23
  249. package/examples/kitchen-sink/test/assistant/assistant.test.ts +471 -299
  250. package/examples/kitchen-sink/test/attachments/attachments.test.ts +135 -94
  251. package/examples/kitchen-sink/test/identity/identity.test.ts +110 -82
  252. package/examples/kitchen-sink/test/identity/session.test.ts +65 -43
  253. package/examples/kitchen-sink/test/issues/assistant.test.ts +50 -31
  254. package/examples/kitchen-sink/test/issues/events.test.ts +273 -0
  255. package/examples/kitchen-sink/test/issues/export.test.ts +120 -0
  256. package/examples/kitchen-sink/test/issues/issues.test.ts +105 -75
  257. package/examples/kitchen-sink/test/request-policy.test.ts +130 -33
  258. package/examples/kitchen-sink/test/routes.test.ts +102 -57
  259. package/examples/kitchen-sink/test/support/as.ts +9 -0
  260. package/examples/kitchen-sink/test/support/scripted.ts +6 -5
  261. package/examples/kitchen-sink/test/support/services.ts +17 -0
  262. package/examples/live-rpc/README.md +33 -35
  263. package/examples/live-rpc/src/Rpc.ts +9 -5
  264. package/examples/live-rpc/src/browser.ts +39 -11
  265. package/examples/live-rpc/src/chat/Chat.ts +30 -16
  266. package/examples/live-rpc/src/chat/authorize.ts +2 -1
  267. package/examples/live-rpc/src/chat/events.ts +74 -0
  268. package/examples/live-rpc/src/chat/handlers.ts +13 -5
  269. package/examples/live-rpc/src/chat/room.ts +9 -5
  270. package/examples/live-rpc/src/chat/store.ts +51 -14
  271. package/examples/live-rpc/src/client.ts +22 -5
  272. package/examples/live-rpc/src/identity/Identity.ts +3 -3
  273. package/examples/live-rpc/src/identity/verifier.ts +8 -4
  274. package/examples/live-rpc/src/page.ts +58 -12
  275. package/examples/live-rpc/src/routes.ts +14 -10
  276. package/examples/live-rpc/src/server.ts +15 -5
  277. package/examples/live-rpc/test/browser.test.ts +127 -50
  278. package/examples/live-rpc/test/chat/chat.test.ts +215 -23
  279. package/examples/live-rpc/test/routes.test.ts +130 -46
  280. package/examples/live-rpc/test/server.test.ts +94 -33
  281. package/examples/mcp-tools/README.md +24 -38
  282. package/examples/mcp-tools/src/platform/fake-farm.ts +63 -24
  283. package/examples/mcp-tools/src/platform/print-farm.ts +59 -29
  284. package/examples/mcp-tools/src/printers/Printers.ts +18 -7
  285. package/examples/mcp-tools/src/printers/farm.ts +15 -5
  286. package/examples/mcp-tools/src/printers/handlers.ts +12 -6
  287. package/examples/mcp-tools/src/routes.ts +8 -6
  288. package/examples/mcp-tools/src/server.ts +12 -5
  289. package/examples/mcp-tools/src/stdio.ts +3 -2
  290. package/examples/mcp-tools/test/printers/printers.test.ts +132 -89
  291. package/examples/mcp-tools/test/routes.test.ts +140 -81
  292. package/examples/notes-service/README.md +30 -45
  293. package/examples/notes-service/src/Api.ts +1 -0
  294. package/examples/notes-service/src/browser.ts +15 -14
  295. package/examples/notes-service/src/cli.ts +8 -3
  296. package/examples/notes-service/src/commands.ts +46 -20
  297. package/examples/notes-service/src/identity/Identity.ts +2 -2
  298. package/examples/notes-service/src/identity/authorize.ts +6 -2
  299. package/examples/notes-service/src/identity/handlers.ts +3 -2
  300. package/examples/notes-service/src/identity/users.ts +2 -1
  301. package/examples/notes-service/src/identity/verifier.ts +12 -6
  302. package/examples/notes-service/src/notes/Notes.ts +46 -22
  303. package/examples/notes-service/src/notes/handlers.ts +18 -9
  304. package/examples/notes-service/src/notes/store.ts +70 -43
  305. package/examples/notes-service/src/routes.ts +7 -5
  306. package/examples/notes-service/src/server.ts +12 -5
  307. package/examples/notes-service/src/status/Status.ts +1 -1
  308. package/examples/notes-service/src/status/handlers.ts +5 -3
  309. package/examples/notes-service/src/stdio.ts +7 -3
  310. package/examples/notes-service/src/worker.ts +6 -3
  311. package/examples/notes-service/test/commands.test.ts +164 -91
  312. package/examples/notes-service/test/notes/as.ts +3 -1
  313. package/examples/notes-service/test/notes/idempotency.test.ts +97 -67
  314. package/examples/notes-service/test/notes/in-process.test.ts +26 -15
  315. package/examples/notes-service/test/notes/notes.test.ts +49 -31
  316. package/examples/notes-service/test/routes.test.ts +106 -71
  317. package/examples/notes-service/test/worker.test.ts +21 -13
  318. package/examples/quickstart/README.md +2 -6
  319. package/examples/quickstart/src/Api.ts +1 -0
  320. package/examples/quickstart/src/client.ts +6 -3
  321. package/examples/quickstart/src/greeting/Greeting.ts +1 -1
  322. package/examples/quickstart/src/greeting/handlers.ts +5 -2
  323. package/examples/quickstart/src/routes.ts +3 -2
  324. package/examples/quickstart/src/server.ts +6 -2
  325. package/examples/quickstart/test/routes.test.ts +3 -2
  326. package/package.json +19 -17
@@ -0,0 +1,126 @@
1
+ # Events over rpcs
2
+
3
+ An [event type](Event.md) in an RPC binding is a stream rpc named after it, taking `{ input, cursor }`, which pushes each occurrence as the server reads it. A client's `watch(event, input, options)` holds it open over the client's protocol, a socket or HTTP, as `Action.client`'s does, and connects again after a drop, resuming from the last occurrence it delivered. Experimental, as [events](Event.md) are.
4
+
5
+ A message the library throws says its fix; for a compiler diagnostic or Effect's own error, search [ActionRpc-failures.md](ActionRpc-failures.md). For one symbol, `grep -rn <symbol> node_modules/@gjermundgaraba/effect-actions/dist/`.
6
+
7
+ ## Bind and serve events
8
+
9
+ List the event types in `ActionRpc.make` beside the actions, and their implementations in `ActionRpc.layer`'s list ([ActionRpc.md](ActionRpc.md#serve-the-rpcs)); live-rpc's binding holds `MessagePosted` beside its actions. The same implementations serve HTTP and MCP too: [build-events/src/routes.ts](../examples/build-events/src/routes.ts) serves them all three ways, and says which serialization a watch needs over HTTP.
10
+
11
+ ```ts example=build-events/src/Rpc.ts
12
+ import * as ActionRpc from "@gjermundgaraba/effect-actions/ActionRpc";
13
+
14
+ import {
15
+ BuildFinished,
16
+ FinishBuild,
17
+ PipelineStatus,
18
+ StartBuild,
19
+ TailLog,
20
+ } from "./builds/Builds.js";
21
+ import { DeployRequested, RequestDeploy } from "./deploys/Deploys.js";
22
+ import { Login } from "./identity/Identity.js";
23
+
24
+ // The RPC binding: each action an rpc named after it, and each event a stream rpc named after
25
+ // it, which pushes each occurrence as the server reads it. Plain data, as the HTTP binding is.
26
+ export const Rpc = ActionRpc.make(
27
+ [
28
+ FinishBuild,
29
+ StartBuild,
30
+ TailLog,
31
+ BuildFinished,
32
+ PipelineStatus,
33
+ RequestDeploy,
34
+ DeployRequested,
35
+ ],
36
+ { authentication: Login }
37
+ );
38
+ ```
39
+
40
+ What a stream rpc sends is on `ActionRpc.layer`; what a watch fails with, and when it reconnects, on `ActionRpc.client`.
41
+
42
+ ## Follow what a request names
43
+
44
+ A watch runs the layer's `middleware` and its authentication as it starts, and again each time it connects, so its source reads the same request-time services an action's handler does. live-rpc's room travels in a header, which `ResolveRoom` turns into `CurrentRoom` ([ActionRpc.md](ActionRpc.md#send-request-context-with-a-call)): the event's check reads that room's messages, and its `key` is the room, so `postMessage`'s notify for one room wakes that room's pages alone.
45
+
46
+ ```ts example=live-rpc/src/chat/events.ts
47
+ import * as Event from "@gjermundgaraba/effect-actions/Event";
48
+ import { Effect, Option, Schema } from "effect";
49
+
50
+ import { authorize } from "./authorize.js";
51
+ import { MessagePosted } from "./Chat.js";
52
+ import { CurrentRoom } from "./room.js";
53
+ import { MessageStore } from "./store.js";
54
+
55
+ const idOf = Schema.decodeUnknownOption(
56
+ Schema.NumberFromString.pipe(Schema.check(Schema.isInt()))
57
+ );
58
+
59
+ // A cursor this source gives is `<epoch>:<id>`, the store's epoch and the id of the last message
60
+ // a page holds, so one from before a restart of the memory store is told apart. A page starting
61
+ // a watch gives the id alone, as `readMessages`' `after` is, which is the store's now.
62
+ const positionOf = (epoch: string, cursor: string | null) => {
63
+ const parts =
64
+ cursor?.includes(":") === true ? cursor.split(":") : [epoch, cursor];
65
+
66
+ return parts.length === 2 && parts[0] === epoch
67
+ ? idOf(parts[1])
68
+ : Option.none();
69
+ };
70
+
71
+ // The room's messages as an event, read from the store that keeps them: a check source.
72
+ // `postMessage` notifies after each post, so a page receives it at once, not at its next read
73
+ // every 5 seconds.
74
+ export const messageEvents = Event.implement(
75
+ MessagePosted,
76
+ Effect.gen(function* () {
77
+ const store = yield* MessageStore;
78
+
79
+ return {
80
+ check: (_input, cursor) =>
81
+ Effect.gen(function* () {
82
+ const room = yield* CurrentRoom;
83
+ const last = yield* store.last(room);
84
+
85
+ const after = Option.filter(
86
+ positionOf(store.epoch, cursor),
87
+ (id) => id >= 0 && id <= last
88
+ );
89
+
90
+ // No cursor starts after the room's last message. One this room never gave, such as
91
+ // one from before a restart, is answered `truncated` from there: the page learns its
92
+ // messages may be another run's, and reads the room again with `readMessages`.
93
+ if (Option.isNone(after)) {
94
+ return {
95
+ cursor: `${store.epoch}:${last}`,
96
+ events: [],
97
+ truncated: cursor !== null,
98
+ };
99
+ }
100
+
101
+ const messages = yield* store.read(room, after.value);
102
+
103
+ return {
104
+ cursor: `${store.epoch}:${messages.at(-1)?.id ?? after.value}`,
105
+ // A message's id counts within its room and the store's epoch, so the event's id
106
+ // names all three: a watch drops an id it has seen, and after a restart of the memory
107
+ // store `lobby:1` is another message.
108
+ events: messages.map((data) => ({
109
+ eventId: `${store.epoch}:${room}:${data.id}`,
110
+ data,
111
+ })),
112
+ };
113
+ }),
114
+ // A notify for one room wakes that room's pages alone: the key is the room the watch's
115
+ // header names, read as `check` reads it.
116
+ key: () => CurrentRoom,
117
+ };
118
+ }),
119
+ { authorize }
120
+ );
121
+ ```
122
+
123
+ The page sends the room and its token with the watch: `RpcClient.withHeaders` wraps an Effect, so `browser.ts`'s `messages` sets them around the stream, and so on every request it sends again after a drop ([live-rpc/src/browser.ts](../examples/live-rpc/src/browser.ts)).
124
+
125
+ - **No poll.** An rpc client holds the stream open; a client that cannot, such as a serverless function, watches over [HTTP](ActionHttp.md#serve-and-watch-events) or MCP.
126
+ - **Which deployment.** A notify wakes the watches its process holds, so the process serving the sockets is the one whose `Event.Events` the handlers notify; another process's watches read the post at their next read, 5 seconds on. A log source fed by `emit` and a `subscription` hook need a server that outlives a request, as over MCP.
@@ -1,7 +1,6 @@
1
1
  # ActionRpc failure modes
2
2
 
3
- What you see when something is wrong, and the fix. Search for a fragment of the message.
4
- The tasks are in [ActionRpc.md](ActionRpc.md).
3
+ What you see when something is wrong, and the fix. Search for a fragment of the message. The tasks are in [ActionRpc.md](ActionRpc.md).
5
4
 
6
5
  - A type error on `middleware` naming `"Layer middleware fails only with the binding's errors and needs no client"`: a middleware declares an error neither the binding nor the built-ins hold, or is `requiredForClient`. Add the error to the binding's `error`, or check in the handler, declaring the error on its actions.
7
6
  - `Type 'HttpRouter' is not assignable to type 'never'` where the server is launched: an rpc protocol was provided around `HttpRouter.serve`, whose router it adds its route to. Provide the protocol to the rpc layer.
package/docs/ActionRpc.md CHANGED
@@ -1,39 +1,39 @@
1
1
  # ActionRpc
2
2
 
3
- Actions as Effect RPC, on Effect's own `RpcServer` and `RpcClient`: one unary rpc per action.
3
+ Actions as Effect RPC, on Effect's own `RpcServer` and `RpcClient`: one rpc per action, a stream rpc per event type, and native rpcs of your own beside them.
4
4
 
5
5
  A message the library throws says its fix; for a compiler diagnostic or Effect's own error, search [ActionRpc-failures.md](ActionRpc-failures.md). For one symbol, `grep -rn <symbol> node_modules/@gjermundgaraba/effect-actions/dist/`: each declaration's JSDoc carries its rules and gotchas, and this page only what an example cannot show.
6
6
 
7
7
  ## Bind actions to rpcs
8
8
 
9
- A binding that holds a protected action names its authentication descriptor
10
- ([Authentication.md](Authentication.md#declare-how-callers-sign-in)); the rules of each option
11
- are on `ActionRpc.Options`. Call `ActionRpc.make` once and export the binding:
9
+ A binding that holds a protected action names its authentication descriptor ([Authentication.md](Authentication.md#declare-how-callers-sign-in)); the rules of each option are on `ActionRpc.Options`. Call `ActionRpc.make` once and export the binding:
12
10
 
13
11
  ```ts example=live-rpc/src/Rpc.ts
14
12
  import * as ActionRpc from "@gjermundgaraba/effect-actions/ActionRpc";
15
- import { ListRooms, Messages } from "./chat/Chat.js";
13
+
14
+ import { ListRooms, MessagePosted, Messages } from "./chat/Chat.js";
16
15
  import { Login } from "./identity/Identity.js";
17
16
 
18
- // The RPC binding: one native rpc per action, named after it, the protected ones requiring
19
- // `Login`. It is plain data and imports no server code, so the server and a browser page both
20
- // import this one value. `Rpc.group` is the native `RpcGroup`, for Effect's own `RpcClient.make`.
21
- export const Rpc = ActionRpc.make([ListRooms, ...Messages], { authentication: Login });
17
+ // The RPC binding: one native rpc per action, named after it, and a stream rpc for
18
+ // `MessagePosted`, the protected ones requiring `Login`. It is plain data and imports no server
19
+ // code, so the server and a browser page both import this one value. `Rpc.group` is the native
20
+ // `RpcGroup`, for Effect's own `RpcClient.make`.
21
+ export const Rpc = ActionRpc.make([ListRooms, ...Messages, MessagePosted], {
22
+ authentication: Login,
23
+ });
22
24
  ```
23
25
 
24
- [kitchen-sink/src/Rpc.ts](../examples/kitchen-sink/src/Rpc.ts) adds `error`, what its
25
- middleware fails with.
26
+ [kitchen-sink/src/Rpc.ts](../examples/kitchen-sink/src/Rpc.ts) adds `error`, what its middleware fails with.
26
27
 
27
28
  ## Serve the rpcs
28
29
 
29
- In the routes file, give `ActionRpc.layer` the binding and the implementations, and provide
30
- what it requires except the protocol and the startup services the entrypoint chooses: the
31
- service of each listed middleware and, where it serves a protected action, the authentication
32
- provider ([Authentication.md](Authentication.md#verify-credentials)).
30
+ In the routes file, give `ActionRpc.layer` the binding and the implementations, and provide what it requires except the protocol and the startup services the entrypoint chooses: the service of each listed middleware and, where it serves a protected action, the authentication provider ([Authentication.md](Authentication.md#verify-credentials)).
33
31
 
34
32
  ```ts example=live-rpc/src/routes.ts
35
- import { Layer } from "effect";
36
33
  import * as ActionRpc from "@gjermundgaraba/effect-actions/ActionRpc";
34
+ import { Layer } from "effect";
35
+
36
+ import { messageEvents } from "./chat/events.js";
37
37
  import { messages, rooms } from "./chat/handlers.js";
38
38
  import { ResolveRoom, resolveRoom } from "./chat/room.js";
39
39
  import { authenticate } from "./identity/verifier.js";
@@ -42,28 +42,32 @@ import { Rpc } from "./Rpc.js";
42
42
  // The binding's rpcs on Effect's own RpcServer, the protected ones behind `Login`'s verifier;
43
43
  // `listRooms` needs no token.
44
44
  //
45
- // `ResolveRoom`, listed in `middleware`, provides `CurrentRoom`, which the handlers and
46
- // `authorize` read. Only something that runs per message can, since each message names its own
47
- // room: `Layer.succeed(CurrentRoom, "lobby")` provided here would leave the layer still
48
- // requiring it, in its type.
45
+ // `ResolveRoom`, listed in `middleware`, provides `CurrentRoom`, which the handlers, the event's
46
+ // source and `authorize` read; on a watch it runs as the watch starts, and each time it connects
47
+ // again. Only something that runs per message can, since each message names its own room:
48
+ // `Layer.succeed(CurrentRoom, "lobby")` provided here would leave the layer still requiring it,
49
+ // in its type.
49
50
  //
50
- // It still requires three things, each chosen by whoever serves it. The protocol and its
51
+ // It still requires four things, each chosen by whoever serves it. The protocol and its
51
52
  // serialization: server.ts gives it a WebSocket, and a test Effect's HTTP protocol, which
52
- // answers in memory. And `MessageStore`.
53
- export const rpc = ActionRpc.layer(Rpc, [rooms, messages], { middleware: [ResolveRoom] }).pipe(
54
- Layer.provide([resolveRoom, authenticate]),
55
- );
53
+ // answers in memory. `MessageStore`, and `Event.Events`, which `postMessage` notifies and the
54
+ // watches wait on: one of each, provided around the whole server.
55
+ export const rpc = ActionRpc.layer(Rpc, [rooms, messages, messageEvents], {
56
+ middleware: [ResolveRoom],
57
+ }).pipe(Layer.provide([resolveRoom, authenticate]));
56
58
  ```
57
59
 
58
- The entrypoint serves the result with `HttpRouter.serve`; a test serves it in memory
59
- ([Testing.md](Testing.md#test-rpcs)).
60
+ The entrypoint serves the result with `HttpRouter.serve`; a test serves it in memory ([Testing.md](Testing.md#test-rpcs)).
60
61
 
61
62
  ```ts example=live-rpc/src/server.ts
62
63
  import { createServer } from "node:http";
64
+
63
65
  import { NodeHttpServer, NodeRuntime } from "@effect/platform-node";
66
+ import * as Event from "@gjermundgaraba/effect-actions/Event";
64
67
  import { Config, Effect, Layer } from "effect";
65
68
  import { HttpRouter } from "effect/http";
66
69
  import { RpcSerialization, RpcServer } from "effect/rpc";
70
+
67
71
  import { MessageStore } from "./chat/store.js";
68
72
  import { rpc } from "./routes.js";
69
73
 
@@ -76,13 +80,16 @@ import { rpc } from "./routes.js";
76
80
  // serialization.
77
81
  const socket = rpc.pipe(
78
82
  Layer.provide(RpcServer.layerProtocolWebsocket({ path: "/rpc" })),
79
- Layer.provide(RpcSerialization.layerJson),
83
+ Layer.provide(RpcSerialization.layerJson)
80
84
  );
81
85
 
82
86
  const server = Layer.unwrap(
83
87
  Effect.gen(function* () {
84
88
  // The loopback default serves this machine alone; HOST=0.0.0.0 lets others reach it.
85
- const host = yield* Config.String("HOST").pipe(Config.withDefault("127.0.0.1"));
89
+ const host = yield* Config.String("HOST").pipe(
90
+ Config.withDefault("127.0.0.1")
91
+ );
92
+
86
93
  const port = yield* Config.Port("PORT").pipe(Config.withDefault(3000));
87
94
 
88
95
  return HttpRouter.serve(socket).pipe(
@@ -94,46 +101,41 @@ const server = Layer.unwrap(
94
101
  // does not bound it: Node's WebSocket server reads one of up to 100 MiB whole unless
95
102
  // told otherwise. A chat message is short, and a larger one closes the socket (1009).
96
103
  websocket: { maxPayload: 64 * 1024 },
97
- }),
104
+ })
98
105
  ),
99
106
  // DEMO ONLY: messages live in this process's memory; a deployment provides a store that
100
107
  // lasts.
101
108
  Layer.provide(MessageStore.layerMemory),
109
+ // The pages watching a room are this process's sockets, so what wakes them lives in its
110
+ // memory too: one for the whole server. Several processes behind a balancer would each
111
+ // wake only their own pages, the others reading a post at their next read, 5 seconds on.
112
+ Layer.provide(Event.layerMemory)
102
113
  );
103
- }),
114
+ })
104
115
  );
105
116
 
106
- Layer.launch(server).pipe(NodeRuntime.runMain);
117
+ Layer.launch(server).pipe(NodeRuntime.runMain());
107
118
  ```
108
119
 
109
- Where the protocol goes, why one protocol takes one layer, and what a protocol with no router
110
- lacks are on `ActionRpc.layer`; the order of `middleware`, and which actions a middleware
111
- reading the identity may serve, on `ActionRpc.LayerOptions.middleware`.
112
- [kitchen-sink/src/identity/quota.ts](../examples/kitchen-sink/src/identity/quota.ts) is a limit
113
- in `middleware`, keyed on the caller, and
114
- [kitchen-sink/src/Rpc.ts](../examples/kitchen-sink/src/Rpc.ts) binds the protected actions
115
- alone for it.
116
-
117
- - **The same actions as JSON too.** Merge `ActionHttp.layer(Http, implementations)` beside the
118
- rpc layer, each given its descriptor's provider
119
- ([kitchen-sink/src/routes.ts](../examples/kitchen-sink/src/routes.ts) defines both, and
120
- [server.ts](../examples/kitchen-sink/src/server.ts) merges them): one provider given to both
121
- is built once. The routes need what the rpcs did not: an `httpApiStatus` on a declared error,
122
- by the rule on `Action.Options.error`, and router middleware for a service an `RpcMiddleware`
123
- provides ([ActionHttp.md](ActionHttp.md#serve-the-routes)).
120
+ Where the protocol goes, why one protocol takes one layer, and what a protocol with no router lacks are on `ActionRpc.layer`; the order of `middleware`, and which actions a middleware reading the identity may serve, on `ActionRpc.LayerOptions.middleware`. [kitchen-sink/src/identity/quota.ts](../examples/kitchen-sink/src/identity/quota.ts) is a limit in `middleware`, keyed on the caller, and [kitchen-sink/src/Rpc.ts](../examples/kitchen-sink/src/Rpc.ts) binds the protected actions alone for it.
121
+
122
+ - **On Cloudflare Workers**, serve the rpcs from a Durable Object, with `@gjermundgaraba/effect-actions-cloudflare`'s `ActionDurableObject.layerWebSocket`.
123
+ - **The same actions as JSON too.** Merge `ActionHttp.layer(Http, implementations)` beside the rpc layer, each given its descriptor's provider ([kitchen-sink/src/routes.ts](../examples/kitchen-sink/src/routes.ts) defines both, and [server.ts](../examples/kitchen-sink/src/server.ts) merges them): one provider given to both is built once. The routes need what the rpcs did not: an `httpApiStatus` on a declared error, by the rule on `Action.Options.error`, and router middleware for a service an `RpcMiddleware` provides ([ActionHttp.md](ActionHttp.md#serve-the-routes)).
124
+ - **An rpc of your own beside the actions.** Give `ActionRpc.make` native rpcs in `rpcs`, `ActionRpc.make(actions, { authentication, rpcs: RpcGroup.make(Upload) })`, each with its own schemas: under the binding's `authentication` each message authenticates as a protected action's does, and the client has their methods beside the actions'. Implement them with the binding's own `Rpc.rpcs.toLayer(handlers)`, whose handlers read the identity and authorize it themselves, and provide that to `ActionRpc.layer`, which otherwise does not compile. The rules are on `ActionRpc.Options.rpcs`.
125
+ - **An action reporting progress or answering a stream is a stream rpc.** Over a WebSocket, or HTTP under `RpcSerialization.layerNdjson`, its values and elements arrive as sent; under `layerJson` over HTTP, all at the end (`ActionRpc.layer`).
124
126
 
125
127
  ## Call the rpcs
126
128
 
127
- `ActionRpc.client(Rpc)` has the methods of `ActionHttp.client`
128
- ([ActionHttp.md](ActionHttp.md#call-the-api-from-effect)), and what one call fails with is
129
- `ActionRpc.MethodError`. A page's client module:
129
+ `ActionRpc.client(Rpc)` has the methods of `ActionHttp.client` ([ActionHttp.md](ActionHttp.md#call-the-api-from-effect)), and what one call fails with is `ActionRpc.MethodError`. A page's client module:
130
130
 
131
131
  ```ts example=live-rpc/src/browser.ts
132
- import { Effect, Layer } from "effect";
132
+ import * as ActionRpc from "@gjermundgaraba/effect-actions/ActionRpc";
133
+ import { Effect, Layer, Stream } from "effect";
134
+ import { Headers } from "effect/http";
133
135
  import { RpcClient, RpcSerialization } from "effect/rpc";
134
136
  import { Socket } from "effect/socket";
135
- import * as ActionRpc from "@gjermundgaraba/effect-actions/ActionRpc";
136
- import { roomHeader } from "./chat/Chat.js";
137
+
138
+ import { MessagePosted, roomHeader } from "./chat/Chat.js";
137
139
  import { Rpc } from "./Rpc.js";
138
140
 
139
141
  // Client module: what a web page imports. It holds the binding and nothing of the server, so a
@@ -146,7 +148,7 @@ import { Rpc } from "./Rpc.js";
146
148
  export const connection = (url: string) =>
147
149
  RpcClient.layerProtocolSocket().pipe(
148
150
  Layer.provide(Socket.layerWebSocket(url)),
149
- Layer.provide(RpcSerialization.layerJson),
151
+ Layer.provide(RpcSerialization.layerJson)
150
152
  );
151
153
 
152
154
  // The chat as a page uses it, on the connection provided around it.
@@ -161,24 +163,50 @@ export const openChat = Effect.gen(function* () {
161
163
  room: (name: string, token: string) => {
162
164
  // A browser WebSocket cannot set an `Authorization` header on its upgrade, so the token
163
165
  // travels on each message, and the room beside it. A refreshed token needs no new socket.
164
- const here = RpcClient.withHeaders({ authorization: `Bearer ${token}`, [roomHeader]: name });
166
+ const headers = { authorization: `Bearer ${token}`, [roomHeader]: name };
167
+ const here = RpcClient.withHeaders(headers);
165
168
 
166
169
  return {
167
- // Called again and again while the page is open, with the last id it holds. A token
168
- // may expire while the page stays open, so each refusal becomes what the page shows.
170
+ // What the room holds after the last id the page has. A token may expire while the
171
+ // page stays open, so each refusal becomes what the page shows.
169
172
  read: (after: number) =>
170
173
  client.readMessages({ after }).pipe(
171
174
  here,
172
- Effect.map(({ messages }) => ({ access: "member" as const, messages })),
175
+ Effect.map(({ messages }) => ({
176
+ access: "member" as const,
177
+ messages,
178
+ })),
173
179
  Effect.catchTags({
174
180
  Unauthenticated: () =>
175
181
  Effect.succeed({ access: "signed-out" as const, messages: [] }),
176
- Forbidden: () => Effect.succeed({ access: "not-a-member" as const, messages: [] }),
177
- }),
182
+ Forbidden: () =>
183
+ Effect.succeed({
184
+ access: "not-a-member" as const,
185
+ messages: [],
186
+ }),
187
+ })
178
188
  ),
179
189
 
180
190
  post: (text: string) => client.postMessage({ text }).pipe(here),
181
191
 
192
+ // Each message posted to the room after the last id the page holds: a page opening a
193
+ // room reads it, then watches from the last id it read, so a post between the two is
194
+ // not lost; 0 sends the room's history first. `watch` connects again after a drop and
195
+ // resumes from the last message it delivered, so the page never subscribes by hand; a
196
+ // `Gap` says some were missed, or are another run's, so the page reads the room again
197
+ // with `read(0)`. The headers go with the
198
+ // stream, as `here` puts them on a call, and so with every request it sends again. A
199
+ // refusal ends it, as when it connects again with a token that has expired.
200
+ messages: (after: number) =>
201
+ client
202
+ .watch(MessagePosted, {}, { cursor: String(after) })
203
+ .pipe(
204
+ Stream.updateService(
205
+ RpcClient.CurrentHeaders,
206
+ Headers.merge(Headers.fromInput(headers))
207
+ )
208
+ ),
209
+
182
210
  // A declared error arrives as the contract's class. A message someone else already
183
211
  // removed is gone, which is what the page asked for.
184
212
  remove: (id: number) =>
@@ -188,7 +216,7 @@ export const openChat = Effect.gen(function* () {
188
216
  Effect.catchTags({
189
217
  MessageNotFound: () => Effect.succeed("removed" as const),
190
218
  NotAuthor: () => Effect.succeed("not-yours" as const),
191
- }),
219
+ })
192
220
  ),
193
221
  };
194
222
  },
@@ -196,47 +224,48 @@ export const openChat = Effect.gen(function* () {
196
224
  });
197
225
  ```
198
226
 
199
- [live-rpc/src/client.ts](../examples/live-rpc/src/client.ts) runs it from Node, and
200
- [live-rpc/src/page.ts](../examples/live-rpc/src/page.ts) wraps it in promises for a page that is
201
- not one Effect program. The client's traps are on `ActionRpc.client`.
227
+ [live-rpc/src/client.ts](../examples/live-rpc/src/client.ts) runs it from Node, and [live-rpc/src/page.ts](../examples/live-rpc/src/page.ts) wraps it in promises for a page that is not one Effect program. The client's traps are on `ActionRpc.client`.
228
+
229
+ - **A server that closes a socket to refuse it.** Effect's socket client connects again after any close, backing off to every five seconds, for good. Give `RpcClient.layerProtocolSocket` the `retryPolicy` `ActionRpc.reconnectUntil(code)`, and a close with that code ends its calls instead.
230
+
231
+ ## Serve and watch events
232
+
233
+ List [event types](Event.md) in the binding beside the actions: each is a stream rpc, which a client's `watch` holds open on its socket. How, with a room's messages over a WebSocket: [ActionRpc-events.md](ActionRpc-events.md).
202
234
 
203
235
  ## Send request context with a call
204
236
 
205
- What travels beside a call's input, such as a token, a tenant or a room, goes in headers: the
206
- client wraps the call in `RpcClient.withHeaders`, as `room` does in `browser.ts` above. On the
207
- server, authentication decodes the credential from those headers. Anything else is read by
208
- native `RpcMiddleware` listed in `ActionRpc.layer`'s `middleware`:
237
+ What travels beside a call's input, such as a token, a tenant or a room, goes in headers: the client wraps the call in `RpcClient.withHeaders`, as `room` does in `browser.ts` above. On the server, authentication decodes the credential from those headers. Anything else is read by native `RpcMiddleware` listed in `ActionRpc.layer`'s `middleware`:
209
238
 
210
239
  ```ts example=live-rpc/src/chat/room.ts
211
240
  import { Context, Effect, Layer } from "effect";
212
241
  import { RpcMiddleware } from "effect/rpc";
242
+
213
243
  import { defaultRoom, roomHeader } from "./Chat.js";
214
244
 
215
245
  /**
216
246
  * The room a message is about. A request-time service: handlers and `authorize` read it per
217
247
  * call, and it has no Layer, because no one value of it is right for a whole server.
218
248
  */
219
- export class CurrentRoom extends Context.Service<CurrentRoom, string>()("live-rpc/CurrentRoom") {}
249
+ export class CurrentRoom extends Context.Service<CurrentRoom, string>()(
250
+ "live-rpc/CurrentRoom"
251
+ ) {}
220
252
 
221
253
  // Native Effect RPC middleware, declared as providing `CurrentRoom`. Listed in
222
254
  // `ActionRpc.layer`'s `middleware`, it runs once per message.
223
- export class ResolveRoom extends RpcMiddleware.Service<ResolveRoom, { provides: CurrentRoom }>()(
224
- "live-rpc/ResolveRoom",
225
- ) {}
255
+ export class ResolveRoom extends RpcMiddleware.Service<
256
+ ResolveRoom,
257
+ { provides: CurrentRoom }
258
+ >()("live-rpc/ResolveRoom") {}
226
259
 
227
260
  // The middleware's implementation. It gets the message's headers, which over a WebSocket are
228
261
  // the upgrade request's merged with the message's own, and runs the rest of the call with the
229
262
  // room they name, or the default room when they name none. It refuses nothing: whether the
230
263
  // caller may be in that room is `authorize`'s rule.
231
264
  export const resolveRoom = Layer.succeed(ResolveRoom, (call, { headers }) =>
232
- Effect.provideService(call, CurrentRoom, headers[roomHeader] ?? defaultRoom),
265
+ Effect.provideService(call, CurrentRoom, headers[roomHeader] ?? defaultRoom)
233
266
  );
234
267
  ```
235
268
 
236
- Why router middleware is no substitute over a WebSocket is on
237
- `ActionRpc.LayerOptions.middleware`.
269
+ Why router middleware is no substitute over a WebSocket is on `ActionRpc.LayerOptions.middleware`.
238
270
 
239
- - **A cookie the browser attaches to the upgrade covers every message**
240
- ([kitchen-sink/src/browser.ts](../examples/kitchen-sink/src/browser.ts)). A browser attaches
241
- it from any page, so refuse a foreign `Origin` on the upgrade, with a request policy
242
- ([Authentication-advanced.md](Authentication-advanced.md#run-a-request-policy-before-authentication)).
271
+ - **A cookie the browser attaches to the upgrade covers every message** ([kitchen-sink/src/browser.ts](../examples/kitchen-sink/src/browser.ts)). A browser attaches it from any page, so refuse a foreign `Origin` on the upgrade, with a request policy ([Authentication-advanced.md](Authentication-advanced.md#run-a-request-policy-before-authentication)).
@@ -1,7 +1,6 @@
1
1
  # ActionToolkit failure modes
2
2
 
3
- What you see when something is wrong, and the fix. Search for a fragment of the message.
4
- The tasks are in [ActionToolkit.md](ActionToolkit.md).
3
+ What you see when something is wrong, and the fix. Search for a fragment of the message. The tasks are in [ActionToolkit.md](ActionToolkit.md).
5
4
 
6
5
  - `Type 'X' is not assignable to type 'never'` where a turn or a call by hand runs, or `Service not found` in plain JavaScript: a request service, the caller first, is provided around the whole turn or call effect, never to the stream alone or to `layer`; a startup service's Layer is provided to `layer`.
7
6
  - A returned failure whose `result` is an `AiError` with reason `ToolParameterValidationError` (`failureOrigin: "parameters"`), not a declared error: the arguments did not decode, and neither `authorize` nor the handler ran.