@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.
- package/README.md +20 -47
- package/dist/Action.d.mts +96 -47
- package/dist/Action.mjs +11 -14
- package/dist/ActionCli.d.mts +81 -42
- package/dist/ActionCli.mjs +52 -33
- package/dist/ActionHttp.d.mts +84 -43
- package/dist/ActionHttp.mjs +2 -2
- package/dist/ActionMcp.d.mts +54 -27
- package/dist/ActionMcp.mjs +5 -5
- package/dist/ActionRpc.d.mts +48 -35
- package/dist/ActionRpc.mjs +1 -1
- package/dist/ActionToolkit.d.mts +32 -13
- package/dist/ActionToolkit.mjs +10 -7
- package/dist/Authentication.d.mts +92 -40
- package/dist/Authentication.mjs +37 -35
- package/dist/Testing.d.mts +22 -24
- package/dist/authentication/provider.d.mts +1 -4
- package/dist/authentication/provider.mjs +1 -1
- package/dist/authentication/refusal.mjs +2 -1
- package/dist/cli/commands.d.mts +34 -11
- package/dist/cli/commands.mjs +47 -22
- package/dist/contract/caller.mjs +1 -1
- package/dist/contract/declared.mjs +27 -14
- package/dist/contract/errors.d.mts +42 -17
- package/dist/contract/errors.mjs +42 -17
- package/dist/contract/hints.mjs +3 -2
- package/dist/contract/implementation.d.mts +5 -8
- package/dist/contract/implementation.mjs +29 -17
- package/dist/contract/media.d.mts +6 -2
- package/dist/contract/media.mjs +6 -2
- package/dist/contract/names.mjs +14 -12
- package/dist/contract/status.mjs +3 -10
- package/dist/http/client.d.mts +15 -4
- package/dist/http/client.mjs +1 -1
- package/dist/mcp/client.d.mts +21 -8
- package/dist/mcp/client.mjs +6 -8
- package/docs/Action-failures.md +6 -15
- package/docs/Action.md +159 -191
- package/docs/ActionCli-failures.md +12 -92
- package/docs/ActionCli.md +102 -156
- package/docs/ActionHttp-failures.md +14 -76
- package/docs/ActionHttp.md +150 -176
- package/docs/ActionMcp-failures.md +2 -22
- package/docs/ActionMcp.md +79 -170
- package/docs/ActionRpc-failures.md +3 -12
- package/docs/ActionRpc.md +183 -120
- package/docs/ActionToolkit-failures.md +3 -10
- package/docs/ActionToolkit.md +112 -109
- package/docs/Authentication-advanced.md +167 -108
- package/docs/Authentication-failures.md +11 -91
- package/docs/Authentication.md +114 -135
- package/docs/README.md +45 -30
- package/docs/Testing-failures.md +2 -6
- package/docs/Testing.md +126 -159
- package/docs/setup-failures.md +8 -32
- package/docs/setup.md +42 -37
- package/examples/README.md +6 -10
- package/examples/assistant/README.md +20 -38
- package/examples/assistant/src/agent.ts +2 -3
- package/examples/assistant/src/cli.ts +4 -9
- package/examples/assistant/src/commands.ts +6 -4
- package/examples/assistant/src/identity/authorize.ts +3 -3
- package/examples/assistant/src/tasks/Tasks.ts +1 -2
- package/examples/assistant/src/tasks/handlers.ts +3 -3
- package/examples/assistant/src/toolkit.ts +6 -8
- package/examples/assistant/test/commands.test.ts +16 -11
- package/examples/assistant/test/tasks/tasks.test.ts +0 -2
- package/examples/assistant/test/toolkit.test.ts +0 -1
- package/examples/kitchen-sink/README.md +30 -52
- package/examples/kitchen-sink/src/Actions.ts +2 -3
- package/examples/kitchen-sink/src/Api.ts +3 -4
- package/examples/kitchen-sink/src/admin/Admin.ts +1 -1
- package/examples/kitchen-sink/src/admin/authenticate.ts +3 -3
- package/examples/kitchen-sink/src/admin/authorize.ts +1 -2
- package/examples/kitchen-sink/src/agent.ts +1 -1
- package/examples/kitchen-sink/src/assistant/handlers.ts +1 -6
- package/examples/kitchen-sink/src/attachments/Attachments.ts +4 -8
- package/examples/kitchen-sink/src/attachments/download.ts +5 -8
- package/examples/kitchen-sink/src/attachments/handlers.ts +2 -3
- package/examples/kitchen-sink/src/browser.ts +3 -4
- package/examples/kitchen-sink/src/cli.ts +6 -7
- package/examples/kitchen-sink/src/config.ts +2 -0
- package/examples/kitchen-sink/src/identity/Identity.ts +7 -7
- package/examples/kitchen-sink/src/identity/authenticate.ts +3 -5
- package/examples/kitchen-sink/src/identity/authorize.ts +4 -3
- package/examples/kitchen-sink/src/identity/quota.ts +1 -1
- package/examples/kitchen-sink/src/issues/Issues.ts +2 -2
- package/examples/kitchen-sink/src/issues/assistant.ts +3 -3
- package/examples/kitchen-sink/src/issues/guide.ts +2 -3
- package/examples/kitchen-sink/src/ops.ts +3 -6
- package/examples/kitchen-sink/src/request-policy.ts +3 -3
- package/examples/kitchen-sink/src/routes.ts +4 -6
- package/examples/kitchen-sink/src/seed.ts +0 -1
- package/examples/kitchen-sink/src/server.ts +11 -12
- package/examples/kitchen-sink/src/stdio.ts +3 -2
- package/examples/kitchen-sink/src/workspaces/tenant.ts +3 -1
- package/examples/kitchen-sink/test/admin/admin.test.ts +0 -1
- package/examples/kitchen-sink/test/assistant/assistant.test.ts +1 -1
- package/examples/kitchen-sink/test/attachments/attachments.test.ts +1 -2
- package/examples/kitchen-sink/test/identity/identity.test.ts +0 -1
- package/examples/kitchen-sink/test/issues/issues.test.ts +0 -2
- package/examples/kitchen-sink/test/request-policy.test.ts +1 -1
- package/examples/kitchen-sink/test/routes.test.ts +3 -3
- package/examples/live-rpc/README.md +8 -49
- package/examples/live-rpc/src/browser.ts +8 -41
- package/examples/live-rpc/src/chat/Chat.ts +9 -3
- package/examples/live-rpc/src/chat/room.ts +1 -1
- package/examples/live-rpc/src/client.ts +2 -3
- package/examples/live-rpc/src/page.ts +34 -0
- package/examples/live-rpc/src/routes.ts +6 -7
- package/examples/live-rpc/src/server.ts +6 -4
- package/examples/live-rpc/test/browser.test.ts +6 -5
- package/examples/live-rpc/test/routes.test.ts +16 -15
- package/examples/live-rpc/test/server.test.ts +1 -1
- package/examples/mcp-tools/README.md +22 -45
- package/examples/mcp-tools/src/printers/Printers.ts +3 -7
- package/examples/mcp-tools/src/routes.ts +0 -7
- package/examples/mcp-tools/src/server.ts +1 -1
- package/examples/mcp-tools/test/printers/printers.test.ts +2 -3
- package/examples/mcp-tools/test/routes.test.ts +2 -3
- package/examples/notes-service/README.md +9 -43
- package/examples/notes-service/src/cli.ts +0 -3
- package/examples/notes-service/src/commands.ts +7 -17
- package/examples/notes-service/src/identity/authorize.ts +2 -5
- package/examples/notes-service/src/identity/verifier.ts +3 -4
- package/examples/notes-service/src/notes/Notes.ts +2 -6
- package/examples/notes-service/src/notes/store.ts +5 -1
- package/examples/notes-service/src/routes.ts +6 -8
- package/examples/notes-service/src/server.ts +4 -3
- package/examples/notes-service/src/stdio.ts +4 -3
- package/examples/notes-service/test/commands.test.ts +1 -1
- package/examples/notes-service/test/notes/as.ts +6 -0
- package/examples/notes-service/test/notes/idempotency.test.ts +1 -4
- package/examples/notes-service/test/notes/in-process.test.ts +50 -0
- package/examples/notes-service/test/notes/notes.test.ts +3 -42
- package/examples/notes-service/test/routes.test.ts +2 -2
- package/examples/quickstart/README.md +1 -1
- package/examples/quickstart/src/client.ts +1 -0
- package/examples/quickstart/src/server.ts +2 -4
- package/examples/quickstart/test/{greeting/greeting.test.ts → routes.test.ts} +3 -3
- package/package.json +1 -1
- package/docs/Action-in-process.md +0 -93
- package/docs/CONTEXT.md +0 -46
- 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
|
-
|
|
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
|
-
|
|
38
|
-
([
|
|
39
|
-
|
|
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
|
-
[
|
|
47
|
-
|
|
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
|
|
56
|
-
serves a protected action, the authentication
|
|
57
|
-
([Authentication.md](Authentication.md#verify-credentials)).
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
-
|
|
77
|
-
|
|
78
|
-
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
103
|
-
([
|
|
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))
|
|
112
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
139
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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
|
-
|
|
162
|
-
|
|
236
|
+
Why router middleware is no substitute over a WebSocket is on
|
|
237
|
+
`ActionRpc.LayerOptions.middleware`.
|
|
163
238
|
|
|
164
|
-
-
|
|
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
|
-
-
|
|
7
|
-
-
|
|
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
|
-
-
|
|
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`.
|