@gjermundgaraba/effect-actions 0.18.2 → 0.19.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 (144) hide show
  1. package/README.md +20 -47
  2. package/dist/Action.d.mts +96 -47
  3. package/dist/Action.mjs +11 -14
  4. package/dist/ActionCli.d.mts +81 -42
  5. package/dist/ActionCli.mjs +52 -33
  6. package/dist/ActionHttp.d.mts +84 -43
  7. package/dist/ActionHttp.mjs +2 -2
  8. package/dist/ActionMcp.d.mts +54 -27
  9. package/dist/ActionMcp.mjs +5 -5
  10. package/dist/ActionRpc.d.mts +48 -35
  11. package/dist/ActionRpc.mjs +1 -1
  12. package/dist/ActionToolkit.d.mts +32 -13
  13. package/dist/ActionToolkit.mjs +10 -7
  14. package/dist/Authentication.d.mts +92 -40
  15. package/dist/Authentication.mjs +37 -35
  16. package/dist/Testing.d.mts +22 -24
  17. package/dist/authentication/provider.d.mts +1 -4
  18. package/dist/authentication/provider.mjs +1 -1
  19. package/dist/authentication/refusal.mjs +2 -1
  20. package/dist/cli/commands.d.mts +34 -11
  21. package/dist/cli/commands.mjs +47 -22
  22. package/dist/contract/caller.mjs +1 -1
  23. package/dist/contract/declared.mjs +27 -14
  24. package/dist/contract/errors.d.mts +42 -17
  25. package/dist/contract/errors.mjs +42 -17
  26. package/dist/contract/hints.mjs +3 -2
  27. package/dist/contract/implementation.d.mts +5 -8
  28. package/dist/contract/implementation.mjs +29 -17
  29. package/dist/contract/media.d.mts +6 -2
  30. package/dist/contract/media.mjs +6 -2
  31. package/dist/contract/names.mjs +14 -12
  32. package/dist/contract/status.mjs +3 -10
  33. package/dist/http/client.d.mts +15 -4
  34. package/dist/http/client.mjs +1 -1
  35. package/dist/mcp/client.d.mts +21 -8
  36. package/dist/mcp/client.mjs +6 -8
  37. package/docs/Action-failures.md +6 -15
  38. package/docs/Action.md +159 -191
  39. package/docs/ActionCli-failures.md +12 -92
  40. package/docs/ActionCli.md +102 -156
  41. package/docs/ActionHttp-failures.md +14 -76
  42. package/docs/ActionHttp.md +150 -176
  43. package/docs/ActionMcp-failures.md +2 -22
  44. package/docs/ActionMcp.md +79 -170
  45. package/docs/ActionRpc-failures.md +3 -12
  46. package/docs/ActionRpc.md +183 -120
  47. package/docs/ActionToolkit-failures.md +3 -10
  48. package/docs/ActionToolkit.md +112 -109
  49. package/docs/Authentication-advanced.md +167 -108
  50. package/docs/Authentication-failures.md +11 -91
  51. package/docs/Authentication.md +114 -135
  52. package/docs/README.md +45 -30
  53. package/docs/Testing-failures.md +2 -6
  54. package/docs/Testing.md +126 -159
  55. package/docs/setup-failures.md +8 -32
  56. package/docs/setup.md +42 -37
  57. package/examples/README.md +6 -10
  58. package/examples/assistant/README.md +20 -38
  59. package/examples/assistant/src/agent.ts +2 -3
  60. package/examples/assistant/src/cli.ts +4 -9
  61. package/examples/assistant/src/commands.ts +6 -4
  62. package/examples/assistant/src/identity/authorize.ts +3 -3
  63. package/examples/assistant/src/tasks/Tasks.ts +1 -2
  64. package/examples/assistant/src/tasks/handlers.ts +3 -3
  65. package/examples/assistant/src/toolkit.ts +6 -8
  66. package/examples/assistant/test/commands.test.ts +16 -11
  67. package/examples/assistant/test/tasks/tasks.test.ts +0 -2
  68. package/examples/assistant/test/toolkit.test.ts +0 -1
  69. package/examples/kitchen-sink/README.md +30 -52
  70. package/examples/kitchen-sink/src/Actions.ts +2 -3
  71. package/examples/kitchen-sink/src/Api.ts +3 -4
  72. package/examples/kitchen-sink/src/admin/Admin.ts +1 -1
  73. package/examples/kitchen-sink/src/admin/authenticate.ts +3 -3
  74. package/examples/kitchen-sink/src/admin/authorize.ts +1 -2
  75. package/examples/kitchen-sink/src/agent.ts +1 -1
  76. package/examples/kitchen-sink/src/assistant/handlers.ts +1 -6
  77. package/examples/kitchen-sink/src/attachments/Attachments.ts +4 -8
  78. package/examples/kitchen-sink/src/attachments/download.ts +5 -8
  79. package/examples/kitchen-sink/src/attachments/handlers.ts +2 -3
  80. package/examples/kitchen-sink/src/browser.ts +3 -4
  81. package/examples/kitchen-sink/src/cli.ts +6 -7
  82. package/examples/kitchen-sink/src/config.ts +2 -0
  83. package/examples/kitchen-sink/src/identity/Identity.ts +7 -7
  84. package/examples/kitchen-sink/src/identity/authenticate.ts +3 -5
  85. package/examples/kitchen-sink/src/identity/authorize.ts +4 -3
  86. package/examples/kitchen-sink/src/identity/quota.ts +1 -1
  87. package/examples/kitchen-sink/src/issues/Issues.ts +2 -2
  88. package/examples/kitchen-sink/src/issues/assistant.ts +3 -3
  89. package/examples/kitchen-sink/src/issues/guide.ts +2 -3
  90. package/examples/kitchen-sink/src/ops.ts +3 -6
  91. package/examples/kitchen-sink/src/request-policy.ts +3 -3
  92. package/examples/kitchen-sink/src/routes.ts +4 -6
  93. package/examples/kitchen-sink/src/seed.ts +0 -1
  94. package/examples/kitchen-sink/src/server.ts +11 -12
  95. package/examples/kitchen-sink/src/stdio.ts +3 -2
  96. package/examples/kitchen-sink/src/workspaces/tenant.ts +3 -1
  97. package/examples/kitchen-sink/test/admin/admin.test.ts +0 -1
  98. package/examples/kitchen-sink/test/assistant/assistant.test.ts +1 -1
  99. package/examples/kitchen-sink/test/attachments/attachments.test.ts +1 -2
  100. package/examples/kitchen-sink/test/identity/identity.test.ts +0 -1
  101. package/examples/kitchen-sink/test/issues/issues.test.ts +0 -2
  102. package/examples/kitchen-sink/test/request-policy.test.ts +1 -1
  103. package/examples/kitchen-sink/test/routes.test.ts +3 -3
  104. package/examples/live-rpc/README.md +8 -49
  105. package/examples/live-rpc/src/browser.ts +8 -41
  106. package/examples/live-rpc/src/chat/Chat.ts +9 -3
  107. package/examples/live-rpc/src/chat/room.ts +1 -1
  108. package/examples/live-rpc/src/client.ts +2 -3
  109. package/examples/live-rpc/src/page.ts +34 -0
  110. package/examples/live-rpc/src/routes.ts +6 -7
  111. package/examples/live-rpc/src/server.ts +6 -4
  112. package/examples/live-rpc/test/browser.test.ts +6 -5
  113. package/examples/live-rpc/test/routes.test.ts +16 -15
  114. package/examples/live-rpc/test/server.test.ts +1 -1
  115. package/examples/mcp-tools/README.md +22 -45
  116. package/examples/mcp-tools/src/printers/Printers.ts +3 -7
  117. package/examples/mcp-tools/src/routes.ts +0 -7
  118. package/examples/mcp-tools/src/server.ts +1 -1
  119. package/examples/mcp-tools/test/printers/printers.test.ts +2 -3
  120. package/examples/mcp-tools/test/routes.test.ts +2 -3
  121. package/examples/notes-service/README.md +9 -43
  122. package/examples/notes-service/src/cli.ts +0 -3
  123. package/examples/notes-service/src/commands.ts +7 -17
  124. package/examples/notes-service/src/identity/authorize.ts +2 -5
  125. package/examples/notes-service/src/identity/verifier.ts +3 -4
  126. package/examples/notes-service/src/notes/Notes.ts +2 -6
  127. package/examples/notes-service/src/notes/store.ts +5 -1
  128. package/examples/notes-service/src/routes.ts +6 -8
  129. package/examples/notes-service/src/server.ts +4 -3
  130. package/examples/notes-service/src/stdio.ts +4 -3
  131. package/examples/notes-service/test/commands.test.ts +1 -1
  132. package/examples/notes-service/test/notes/as.ts +6 -0
  133. package/examples/notes-service/test/notes/idempotency.test.ts +1 -4
  134. package/examples/notes-service/test/notes/in-process.test.ts +50 -0
  135. package/examples/notes-service/test/notes/notes.test.ts +3 -42
  136. package/examples/notes-service/test/routes.test.ts +2 -2
  137. package/examples/quickstart/README.md +1 -1
  138. package/examples/quickstart/src/client.ts +1 -0
  139. package/examples/quickstart/src/server.ts +2 -4
  140. package/examples/quickstart/test/{greeting/greeting.test.ts → routes.test.ts} +3 -3
  141. package/package.json +1 -1
  142. package/docs/Action-in-process.md +0 -93
  143. package/docs/CONTEXT.md +0 -46
  144. package/docs/guarantees.md +0 -44
package/docs/ActionRpc.md CHANGED
@@ -1,179 +1,242 @@
1
1
  # ActionRpc
2
2
 
3
3
  Actions as Effect RPC, on Effect's own `RpcServer` and `RpcClient`: one unary rpc per action.
4
- Reach for it when a client keeps a connection open and calls often, such as a page on a
5
- WebSocket, or when the application already speaks Effect RPC.
6
4
 
7
- Read this page alone, in one tool call; if its output says it was truncated, read it again alone.
8
- When something fails, search [ActionRpc-failures.md](ActionRpc-failures.md). Reading order:
9
- [README.md](README.md).
10
-
11
- ## API
12
-
13
- Import `@gjermundgaraba/effect-actions/ActionRpc`.
14
-
15
- | API | Purpose |
16
- | --------------------------------------- | -------------------------------------------------------------------------- |
17
- | `make(actions, options?)` | Bind a list of actions; returns a `Binding`. |
18
- | `Rpc.actions` | The bound actions. |
19
- | `Rpc.error` | The errors every rpc declares besides its action's own. |
20
- | `Rpc.authentication` | The authentication descriptor of its protected actions, or `undefined`. |
21
- | `Rpc.group` | Native `RpcGroup`, one rpc per action, for `RpcClient.make`. |
22
- | `layer(Rpc, implementations, options?)` | Serve the rpcs of the bound actions the implementations hold. |
23
- | `client(Rpc, options?)` | An Effect of a typed client; requires the client `Protocol` and a `Scope`. |
24
-
25
- Exported types: `Binding`; `Any`, any binding; `Client<typeof Rpc>`; `MethodError`, what one call fails with; `Options` of `make`, `LayerOptions` of `layer`, `ClientOptions` of `client`.
26
-
27
- | Option | Meaning |
28
- | ------------------------ | ------------------------------------------------------------------------------------------------------ |
29
- | `make`: `error` | Errors the layer's `middleware` fails with: declared by every rpc, so clients decode them. |
30
- | `make`: `authentication` | `Authentication.make(...)` of the protected actions' identity: required when the binding holds one. |
31
- | `layer`: `actions` | The bound actions it serves: `[GetUser]`. Defaults to every one the implementations hold. |
32
- | `layer`: `middleware` | Native `RpcMiddleware` around every rpc, inside authentication, outside decoding; the first innermost. |
33
- | `client`: options | The native `RpcClient.make` options but `flatten`. |
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.
34
6
 
35
7
  ## Bind actions to rpcs
36
8
 
37
- Call `ActionRpc.make` once, in a file that imports no server code
38
- ([setup.md](setup.md#bundle-for-a-browser)), and export the binding: the server serves it and
39
- every client calls it. A binding that holds a protected action names its authentication
40
- descriptor ([Authentication.md](Authentication.md#declare-how-callers-sign-in)).
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:
41
12
 
42
- ```ts
13
+ ```ts example=live-rpc/src/Rpc.ts
14
+ import * as ActionRpc from "@gjermundgaraba/effect-actions/ActionRpc";
15
+ import { ListRooms, Messages } from "./chat/Chat.js";
16
+ import { Login } from "./identity/Identity.js";
17
+
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`.
43
21
  export const Rpc = ActionRpc.make([ListRooms, ...Messages], { authentication: Login });
44
22
  ```
45
23
 
46
- [live-rpc/src/Rpc.ts](../examples/live-rpc/src/Rpc.ts) is that binding;
47
- [kitchen-sink/src/Rpc.ts](../examples/kitchen-sink/src/Rpc.ts) adds `error`.
48
-
49
- Nothing streams. A page that follows changes calls again on its open socket, as `readMessages`
50
- does in [live-rpc/src/chat/Chat.ts](../examples/live-rpc/src/chat/Chat.ts).
24
+ [kitchen-sink/src/Rpc.ts](../examples/kitchen-sink/src/Rpc.ts) adds `error`, what its
25
+ middleware fails with.
51
26
 
52
27
  ## Serve the rpcs
53
28
 
54
29
  In the routes file, give `ActionRpc.layer` the binding and the implementations, and provide
55
- what it requires except the protocol: the service of each listed middleware and, where it
56
- serves a protected action, the authentication provider
57
- ([Authentication.md](Authentication.md#verify-credentials)). The entrypoint chooses the
58
- protocol and its serialization, then serves the result with `HttpRouter.serve`.
59
-
60
- ```ts
61
- // routes.ts
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)).
33
+
34
+ ```ts example=live-rpc/src/routes.ts
35
+ import { Layer } from "effect";
36
+ import * as ActionRpc from "@gjermundgaraba/effect-actions/ActionRpc";
37
+ import { messages, rooms } from "./chat/handlers.js";
38
+ import { ResolveRoom, resolveRoom } from "./chat/room.js";
39
+ import { authenticate } from "./identity/verifier.js";
40
+ import { Rpc } from "./Rpc.js";
41
+
42
+ // The binding's rpcs on Effect's own RpcServer, the protected ones behind `Login`'s verifier;
43
+ // `listRooms` needs no token.
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.
49
+ //
50
+ // It still requires three things, each chosen by whoever serves it. The protocol and its
51
+ // serialization: server.ts gives it a WebSocket, and a test Effect's HTTP protocol, which
52
+ // answers in memory. And `MessageStore`.
62
53
  export const rpc = ActionRpc.layer(Rpc, [rooms, messages], { middleware: [ResolveRoom] }).pipe(
63
54
  Layer.provide([resolveRoom, authenticate]),
64
55
  );
56
+ ```
65
57
 
66
- // server.ts
58
+ The entrypoint serves the result with `HttpRouter.serve`; a test serves it in memory
59
+ ([Testing.md](Testing.md#test-rpcs)).
60
+
61
+ ```ts example=live-rpc/src/server.ts
62
+ import { createServer } from "node:http";
63
+ import { NodeHttpServer, NodeRuntime } from "@effect/platform-node";
64
+ import { Config, Effect, Layer } from "effect";
65
+ import { HttpRouter } from "effect/http";
66
+ import { RpcSerialization, RpcServer } from "effect/rpc";
67
+ import { MessageStore } from "./chat/store.js";
68
+ import { rpc } from "./routes.js";
69
+
70
+ // Entrypoint: `node --import tsx examples/live-rpc/src/server.ts`.
71
+ // It chooses the protocol, the store and the port, and starts Node's HTTP server. It provides
72
+ // no caller and no room: those arrive with each message.
73
+
74
+ // The rpcs over a WebSocket at `/rpc`, the router's one route: Node's HTTP server answers the
75
+ // upgrade. JSON, because a page's client speaks it: browser.ts gives its socket the same
76
+ // serialization.
67
77
  const socket = rpc.pipe(
68
78
  Layer.provide(RpcServer.layerProtocolWebsocket({ path: "/rpc" })),
69
79
  Layer.provide(RpcSerialization.layerJson),
70
80
  );
81
+
82
+ const server = Layer.unwrap(
83
+ Effect.gen(function* () {
84
+ // 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"));
86
+ const port = yield* Config.Port("PORT").pipe(Config.withDefault(3000));
87
+
88
+ return HttpRouter.serve(socket).pipe(
89
+ Layer.provide(
90
+ NodeHttpServer.layer(createServer, {
91
+ host,
92
+ port,
93
+ // A message on a WebSocket is no request body, so `HttpServerRequest.MaxBodySize`
94
+ // does not bound it: Node's WebSocket server reads one of up to 100 MiB whole unless
95
+ // told otherwise. A chat message is short, and a larger one closes the socket (1009).
96
+ websocket: { maxPayload: 64 * 1024 },
97
+ }),
98
+ ),
99
+ // DEMO ONLY: messages live in this process's memory; a deployment provides a store that
100
+ // lasts.
101
+ Layer.provide(MessageStore.layerMemory),
102
+ );
103
+ }),
104
+ );
105
+
106
+ Layer.launch(server).pipe(NodeRuntime.runMain);
71
107
  ```
72
108
 
73
- [live-rpc/src/routes.ts](../examples/live-rpc/src/routes.ts) and
74
- [live-rpc/src/server.ts](../examples/live-rpc/src/server.ts) are the two halves.
75
-
76
- - Provide the protocol to the rpc layer, inside what `HttpRouter.serve` serves:
77
- `RpcServer.layerProtocolWebsocket` and `layerProtocolHttp` add their route to that router.
78
- - Export the rpc layer before it is given a protocol. `Testing.layer` cannot upgrade a
79
- WebSocket, so a test gives that layer the HTTP protocol ([Testing.md](Testing.md#test-rpcs)).
80
- - Provide a JSON serialization, the same one on the client: `RpcSerialization.layerJson`,
81
- `layerNdjson`, `layerJsonRpc()` or `layerNdJsonRpc()`.
82
- - Bound a WebSocket message on the server: `HttpServerRequest.MaxBodySize` does not, and Node's
83
- reads up to 100 MiB whole. `server.ts` passes `websocket: { maxPayload }` to
84
- `NodeHttpServer.layer`; a larger message closes the socket.
85
- - Authentication runs for every message of a protected rpc, not once per connection.
86
- - A limit in `middleware` may key on the caller and counts input that does not decode, as the
87
- quota of [kitchen-sink/src/identity/quota.ts](../examples/kitchen-sink/src/identity/quota.ts)
88
- does; a limit in a handler holds on every surface.
89
- - A middleware may require the identity only where every action its layer serves is
90
- protected, and one protocol takes one `ActionRpc.layer`: a second layer given the same
91
- protocol is never reached. So bind the protected actions alone, as
92
- [kitchen-sink/src/Rpc.ts](../examples/kitchen-sink/src/Rpc.ts) does, or have the middleware
93
- read the identity with `Effect.serviceOption`, which a public rpc's call lacks.
94
- - A protocol with no router, such as `RpcServer.layerProtocolSocketServer` or
95
- `layerProtocolStdio`, serves protected actions too, but has no HTTP request, which limits its
96
- provider and its verifier: [ActionRpc-failures.md](ActionRpc-failures.md).
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
+
97
117
  - **The same actions as JSON too.** Merge `ActionHttp.layer(Http, implementations)` beside the
98
118
  rpc layer, each given its descriptor's provider
99
119
  ([kitchen-sink/src/routes.ts](../examples/kitchen-sink/src/routes.ts) defines both, and
100
120
  [server.ts](../examples/kitchen-sink/src/server.ts) merges them): one provider given to both
101
- is built once. The routes need what
102
- the rpcs did not: an `httpApiStatus` on each declared error, which answers 422 without one
103
- ([live-rpc/src/chat/Chat.ts](../examples/live-rpc/src/chat/Chat.ts)); a request body limit;
104
- and router middleware for a service an `RpcMiddleware` provides
105
- ([ActionHttp.md](ActionHttp.md#serve-the-routes)). Write the header's decoding once, in a
106
- function both middleware call.
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)).
107
124
 
108
125
  ## Call the rpcs
109
126
 
110
127
  `ActionRpc.client(Rpc)` has the methods of `ActionHttp.client`
111
- ([ActionHttp.md](ActionHttp.md#call-the-api-from-effect)): `client.readMessages({ after })`.
112
- Its connection is a Layer of Effect's own:
113
-
114
- ```ts
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:
130
+
131
+ ```ts example=live-rpc/src/browser.ts
132
+ import { Effect, Layer } from "effect";
133
+ import { RpcClient, RpcSerialization } from "effect/rpc";
134
+ import { Socket } from "effect/socket";
135
+ import * as ActionRpc from "@gjermundgaraba/effect-actions/ActionRpc";
136
+ import { roomHeader } from "./chat/Chat.js";
137
+ import { Rpc } from "./Rpc.js";
138
+
139
+ // Client module: what a web page imports. It holds the binding and nothing of the server, so a
140
+ // bundler gets the contracts and Effect's RPC client, no handler and nothing of Node.
141
+
142
+ // Effect's own client protocol: one WebSocket, speaking the server's JSON. This layer owns
143
+ // the socket, so a page provides it once, around its whole program, and every call shares
144
+ // that one socket. Which WebSocket is left to the host: a page provides
145
+ // `Socket.layerWebSocketConstructorGlobal`, the browser's own, and client.ts provides Node's.
115
146
  export const connection = (url: string) =>
116
147
  RpcClient.layerProtocolSocket().pipe(
117
148
  Layer.provide(Socket.layerWebSocket(url)),
118
149
  Layer.provide(RpcSerialization.layerJson),
119
150
  );
120
- ```
121
151
 
122
- It leaves the WebSocket constructor to its host: a page provides
123
- `Socket.layerWebSocketConstructorGlobal`, and Node `NodeSocket.layerWebSocketConstructor`.
124
- [live-rpc/src/browser.ts](../examples/live-rpc/src/browser.ts) is a page's client module, and
125
- [live-rpc/src/client.ts](../examples/live-rpc/src/client.ts) runs it from Node.
152
+ // The chat as a page uses it, on the connection provided around it.
153
+ export const openChat = Effect.gen(function* () {
154
+ // One typed method per action of the binding, all on the one connection.
155
+ const client = yield* ActionRpc.client(Rpc);
156
+
157
+ return {
158
+ // The public rpc: no token.
159
+ rooms: Effect.map(client.listRooms(), ({ rooms }) => rooms),
160
+
161
+ room: (name: string, token: string) => {
162
+ // A browser WebSocket cannot set an `Authorization` header on its upgrade, so the token
163
+ // 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 });
165
+
166
+ 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.
169
+ read: (after: number) =>
170
+ client.readMessages({ after }).pipe(
171
+ here,
172
+ Effect.map(({ messages }) => ({ access: "member" as const, messages })),
173
+ Effect.catchTags({
174
+ Unauthenticated: () =>
175
+ Effect.succeed({ access: "signed-out" as const, messages: [] }),
176
+ Forbidden: () => Effect.succeed({ access: "not-a-member" as const, messages: [] }),
177
+ }),
178
+ ),
179
+
180
+ post: (text: string) => client.postMessage({ text }).pipe(here),
181
+
182
+ // A declared error arrives as the contract's class. A message someone else already
183
+ // removed is gone, which is what the page asked for.
184
+ remove: (id: number) =>
185
+ client.deleteMessage({ id }).pipe(
186
+ here,
187
+ Effect.as("removed" as const),
188
+ Effect.catchTags({
189
+ MessageNotFound: () => Effect.succeed("removed" as const),
190
+ NotAuthor: () => Effect.succeed("not-yours" as const),
191
+ }),
192
+ ),
193
+ };
194
+ },
195
+ };
196
+ });
197
+ ```
126
198
 
127
- - The socket belongs to the Layer providing the protocol, not to the scope around the client:
128
- provide the connection once, around the page's whole program, never around one call. A page
129
- that is not one Effect program keeps it in a `ManagedRuntime` and calls with promises:
130
- `connect(url)` in `browser.ts`, whose socket opens on the first call.
131
- - A declared error or a refusal is its call's typed failure: only that call fails, and the
132
- socket stays open. RPC answers no 401 or 403 challenge, and a token may expire under an open
133
- socket, so the page handles `Unauthenticated` itself
134
- ([guarantees.md](guarantees.md#wire-behavior)).
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`.
135
202
 
136
203
  ## Send request context with a call
137
204
 
138
- What travels beside a call's input, such as a token, a tenant or a room, goes in headers. On
139
- the client, wrap the call in `RpcClient.withHeaders`:
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`:
140
209
 
141
- ```ts
142
- const here = RpcClient.withHeaders({ authorization: `Bearer ${token}`, [roomHeader]: name });
143
-
144
- client.postMessage({ text }).pipe(here);
145
- ```
210
+ ```ts example=live-rpc/src/chat/room.ts
211
+ import { Context, Effect, Layer } from "effect";
212
+ import { RpcMiddleware } from "effect/rpc";
213
+ import { defaultRoom, roomHeader } from "./Chat.js";
146
214
 
147
- On the server, authentication decodes the credential from those headers. Anything else is read
148
- by native `RpcMiddleware` listed in `layer`'s `middleware`, which provides it as a service to
149
- the handlers and to `authorize`:
215
+ /**
216
+ * The room a message is about. A request-time service: handlers and `authorize` read it per
217
+ * call, and it has no Layer, because no one value of it is right for a whole server.
218
+ */
219
+ export class CurrentRoom extends Context.Service<CurrentRoom, string>()("live-rpc/CurrentRoom") {}
150
220
 
151
- ```ts
221
+ // Native Effect RPC middleware, declared as providing `CurrentRoom`. Listed in
222
+ // `ActionRpc.layer`'s `middleware`, it runs once per message.
152
223
  export class ResolveRoom extends RpcMiddleware.Service<ResolveRoom, { provides: CurrentRoom }>()(
153
224
  "live-rpc/ResolveRoom",
154
225
  ) {}
155
226
 
227
+ // The middleware's implementation. It gets the message's headers, which over a WebSocket are
228
+ // the upgrade request's merged with the message's own, and runs the rest of the call with the
229
+ // room they name, or the default room when they name none. It refuses nothing: whether the
230
+ // caller may be in that room is `authorize`'s rule.
156
231
  export const resolveRoom = Layer.succeed(ResolveRoom, (call, { headers }) =>
157
232
  Effect.provideService(call, CurrentRoom, headers[roomHeader] ?? defaultRoom),
158
233
  );
159
234
  ```
160
235
 
161
- [live-rpc/src/chat/room.ts](../examples/live-rpc/src/chat/room.ts) is that middleware, and
162
- [live-rpc/src/browser.ts](../examples/live-rpc/src/browser.ts) the client sending the header.
236
+ Why router middleware is no substitute over a WebSocket is on
237
+ `ActionRpc.LayerOptions.middleware`.
163
238
 
164
- - Over a WebSocket, a message's headers are the upgrade request's with its own merged over
165
- them. A browser WebSocket sets no header on its upgrade, so a bearer token goes on each
166
- message, and a refreshed token needs no new socket.
167
- - A cookie the browser attaches to the upgrade covers every message
239
+ - **A cookie the browser attaches to the upgrade covers every message**
168
240
  ([kitchen-sink/src/browser.ts](../examples/kitchen-sink/src/browser.ts)). A browser attaches
169
241
  it from any page, so refuse a foreign `Origin` on the upgrade, with a request policy
170
242
  ([Authentication-advanced.md](Authentication-advanced.md#run-a-request-policy-before-authentication)).
171
- - An API key in the query string never reaches authentication.
172
- - A request-time requirement is never a startup service: `Layer.succeed(CurrentRoom, "lobby")`
173
- provided to the layer leaves `CurrentRoom` owed, in its type.
174
- - Router middleware or `HttpRouter.provideRequest` around the layer provides one too, but over
175
- a WebSocket it runs once, for the upgrade, so its value holds for the whole connection. What
176
- differs per message needs a listed middleware.
177
- - Authentication runs outside the listed middleware, so none of them provides what a verifier
178
- reads
179
- ([Authentication-advanced.md](Authentication-advanced.md#read-request-context-in-a-verifier)).
@@ -3,14 +3,7 @@
3
3
  What you see when something is wrong, and the fix. Search for a fragment of the message.
4
4
  The tasks are in [ActionToolkit.md](ActionToolkit.md).
5
5
 
6
- - An approved call never runs, and nothing is raised: a `tool-approval-response` finds its request by `approvalId` in the prompt history, and was sent without it. Use a `Chat`, or resend the assistant message holding the call and its request before the response.
7
- - `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 is provided around the turn, or around the whole call effect, not to the stream alone, and a protected tool owes its identity whatever its handler reads. A startup service's Layer is provided to `layer`.
8
- - A returned failure whose `result` is an `AiError` with reason `ToolParameterValidationError` (`failureOrigin: "parameters"`), not a declared error: the arguments did not decode. Neither `authorize` nor the handler ran.
9
- - A call dies with the handler's failure and a `SchemaError` beside it: the handler failed with an error its action does not declare, which a cast or plain JavaScript lets through, or with one breaking a check of its schema, such as `Infinity` in a struct error's `Schema.Finite` field. Declare the error on the action, or fix the value.
6
+ - `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
+ - 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.
10
8
  - A turn fails with an `AiError`, `Failed to encode result for tool '<name>'`: the handler's success does not encode, such as `Infinity` for a `Schema.Finite`.
11
- - A model reads base64 text where an image was meant: a tool's success holds an `Action.Image`, which a Toolkit gives the model as JSON text. Leave the action out of `actions`, or serve it over MCP, which sends an image block ([ActionMcp.md](ActionMcp.md#return-images-and-other-media)).
12
- - `Listed in actions, but no implementation holds it: <names>` thrown at `make`: those actions are not the implementations', by identity. `(another contract)` marks one whose name an implementation holds, as a second copy of the contracts module makes. Pass the implementation holding the action, or list the contract it implements.
13
- - `Duplicate tool: <name>` thrown at `make`: two implementations serve actions of one name. Rename one action; of two implementations of one contract, pass one.
14
- - A defect when a tool handles a call: the `layer` provided is another implementation's, with tools of the same names, which the types accept. Provide the `layer` that `make` returned with the `toolkit`.
15
- - Two toolkits of one implementation answer with one set of startup services: their `layer`s, given different services, were provided together, and one serves both. Use separate implementations for separate services.
16
- - A job or an agent beside `HttpRouter.serve` reads other state than the routes, or its builder runs twice: the toolkit's `layer` builds in another layer graph than the routes. Provide that `layer` inside the layer the server serves, or `Action.layer` and the services they share above both ([Action-in-process.md](Action-in-process.md#share-one-build-across-servers)).
9
+ - `TypeError: Cannot read properties of undefined (reading 'context')` when a tool handles a call: the `layer` provided is another implementation's, with tools of the same names, which the types accept. Provide the `layer` that `make` returned with the `toolkit`.