@nice-code/action 0.20.0 → 0.22.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 +140 -109
- package/build/{ActionDevtoolsCore-D_JvgPmz.d.mts → ActionDevtoolsCore-CQ0vrvPD.d.cts} +2 -2
- package/build/{ActionDevtoolsCore-dV-IVPcP.d.cts → ActionDevtoolsCore-CiLBYC3K.d.mts} +2 -2
- package/build/{ActionPayload.types-CnfWlkA1.d.cts → ActionPayload.types-Dx1JPyfs.d.mts} +292 -222
- package/build/{ActionPayload.types-D0DM-g65.d.mts → ActionPayload.types-L9k0LyBd.d.cts} +292 -222
- package/build/devtools/browser/index.d.cts +1 -1
- package/build/devtools/browser/index.d.mts +1 -1
- package/build/devtools/server/index.d.cts +1 -1
- package/build/devtools/server/index.d.mts +1 -1
- package/build/httpAcceptorCarrier-DL8lf0xB.mjs +3906 -0
- package/build/httpAcceptorCarrier-DL8lf0xB.mjs.map +1 -0
- package/build/httpAcceptorCarrier-OnJxzsAD.cjs +4291 -0
- package/build/httpAcceptorCarrier-OnJxzsAD.cjs.map +1 -0
- package/build/index.cjs +395 -4125
- package/build/index.cjs.map +1 -1
- package/build/index.d.cts +2 -2
- package/build/index.d.mts +2 -2
- package/build/index.mjs +331 -4058
- package/build/index.mjs.map +1 -1
- package/build/platform/cloudflare/index.cjs +30 -2
- package/build/platform/cloudflare/index.cjs.map +1 -1
- package/build/platform/cloudflare/index.d.cts +55 -2
- package/build/platform/cloudflare/index.d.mts +55 -2
- package/build/platform/cloudflare/index.mjs +28 -2
- package/build/platform/cloudflare/index.mjs.map +1 -1
- package/build/react-query/index.d.cts +1 -1
- package/build/react-query/index.d.mts +1 -1
- package/package.json +4 -4
- package/build/wsAcceptorCarrier-BDJRIPfu.cjs +0 -103
- package/build/wsAcceptorCarrier-BDJRIPfu.cjs.map +0 -1
- package/build/wsAcceptorCarrier-CW2qX25W.mjs +0 -80
- package/build/wsAcceptorCarrier-CW2qX25W.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ The pieces:
|
|
|
33
33
|
| **ActionRuntime** | One per runtime; identifies it and dispatches actions to handlers |
|
|
34
34
|
| **Channel** | The transport-agnostic routing contract between two runtimes, declared *by role* (`toAcceptor` / `toConnector`) — `defineChannel` (plain) or `defineSecureChannel` (binary + encryption) |
|
|
35
35
|
| **Carrier** | How bytes actually move: `wsCarrier` / `httpCarrier` / `inMemoryCarrier` / `rtcCarrier` (connector side), `wsAcceptorCarrier` / `httpAcceptorCarrier` (acceptor side) |
|
|
36
|
-
| **Transport** | A carrier wrapped with a security policy
|
|
36
|
+
| **Transport** | A carrier wrapped with a security policy (handshake + optional encryption, or plain). You don't build these directly — `connectChannel` / `serveChannel` apply the policy to each carrier for you |
|
|
37
37
|
| **RuntimeCoordinate** | Identifies an environment (frontend, backend, worker…) and is how actions are routed |
|
|
38
38
|
|
|
39
39
|
> **One runtime per client.** A client (a frontend, a backend, a worker) has a *single* `ActionRuntime`
|
|
@@ -45,6 +45,10 @@ The high-level entry points — `connectChannel` (dial out) and `serveChannel` (
|
|
|
45
45
|
reach for 95% of the time. The lower-level handler/carrier/transport objects they desugar to are
|
|
46
46
|
documented at the end under [Lower-level building blocks](#lower-level-building-blocks).
|
|
47
47
|
|
|
48
|
+
> **Code blocks below are labelled by where they run:** `// shared.ts` (imported by both ends),
|
|
49
|
+
> `// server.ts` (the acceptor), `// client.ts` (the connector). A typical app is exactly these three
|
|
50
|
+
> files.
|
|
51
|
+
|
|
48
52
|
---
|
|
49
53
|
|
|
50
54
|
## Defining actions
|
|
@@ -52,6 +56,7 @@ documented at the end under [Lower-level building blocks](#lower-level-building-
|
|
|
52
56
|
### 1. Create a root domain (shared between both ends)
|
|
53
57
|
|
|
54
58
|
```ts
|
|
59
|
+
// shared.ts
|
|
55
60
|
import { createActionRootDomain, actionSchema } from "@nice-code/action";
|
|
56
61
|
import * as v from "valibot";
|
|
57
62
|
|
|
@@ -159,6 +164,7 @@ A **local handler** runs actions in the current process. Build one and register
|
|
|
159
164
|
is what answers incoming requests.
|
|
160
165
|
|
|
161
166
|
```ts
|
|
167
|
+
// server.ts
|
|
162
168
|
import { ActionRuntime, createLocalHandler, RuntimeCoordinate } from "@nice-code/action";
|
|
163
169
|
|
|
164
170
|
export const serverCoord = RuntimeCoordinate.env("backend");
|
|
@@ -194,9 +200,10 @@ for local-first clients that resolve a few actions themselves and forward the re
|
|
|
194
200
|
|
|
195
201
|
`serveChannel` is the one call that stands up an acceptor: it builds the crypto identity **once**, fans it
|
|
196
202
|
across every carrier, registers your handlers, wires hibernation, and returns a server object whose
|
|
197
|
-
`fetch` / `
|
|
203
|
+
`fetch` / `receive` / `drop` / `pushToClient` you forward straight to the host.
|
|
198
204
|
|
|
199
205
|
```ts
|
|
206
|
+
// server.ts
|
|
200
207
|
import {
|
|
201
208
|
ActionRuntime,
|
|
202
209
|
RuntimeCoordinate,
|
|
@@ -217,75 +224,81 @@ const server = serveChannel(runtime, appChannel, {
|
|
|
217
224
|
],
|
|
218
225
|
});
|
|
219
226
|
|
|
220
|
-
// Wire the host's events to the server:
|
|
227
|
+
// Wire the host's events to the server — `receive`/`drop` are the universal forwarding seam:
|
|
221
228
|
// fetch(req) => server.fetch(req)
|
|
222
|
-
// webSocketMessage(ws, msg) => server.
|
|
223
|
-
// webSocketClose/Error(ws) => server.
|
|
229
|
+
// webSocketMessage(ws, msg) => server.receive(ws, msg)
|
|
230
|
+
// webSocketClose/Error(ws) => server.drop(ws)
|
|
224
231
|
```
|
|
225
232
|
|
|
226
|
-
`serveChannel(runtime, channel, options)` returns `{ handlers, fetch,
|
|
233
|
+
`serveChannel(runtime, channel, options)` returns `{ handlers, fetch, receive, drop, pushToClient, broadcast }`:
|
|
227
234
|
|
|
228
235
|
- **`fetch`** — a web-standard handler that does the WS upgrade, the (secure or plain) HTTP action POST,
|
|
229
236
|
CORS preflight, and a `404` fallback. Forward the host's `fetch` straight to it.
|
|
230
|
-
- **`
|
|
231
|
-
|
|
232
|
-
handle directly
|
|
233
|
-
- **`pushToClient(target, request, opts?)`** — push a
|
|
234
|
-
(see
|
|
237
|
+
- **`receive(conn, frame)` / `drop(conn)`** — the universal connection lifecycle; forward your host's
|
|
238
|
+
message and close/error events here. Routes to the sole duplex carrier (the common single-WebSocket
|
|
239
|
+
case); with several duplex carriers, feed each carrier handle directly.
|
|
240
|
+
- **`pushToClient(target, request, opts?)`** / **`broadcast(makeRequest, opts?)`** — push a
|
|
241
|
+
server-initiated action to one / every connected client (see
|
|
242
|
+
[Bi-directional](#bi-directional-communication-acceptor--connector)).
|
|
235
243
|
- **`handlers`** — the acceptor handlers it built, one per duplex carrier (reach for these for per-handler
|
|
236
244
|
`broadcast`).
|
|
237
245
|
|
|
238
246
|
Key options: `clientEnv` (required), `storage` (required only when a carrier is secure — the default),
|
|
239
|
-
`carriers`, `handlers`,
|
|
247
|
+
`carriers`, `handlers`, `channelCases` (connection-aware cases — see below), plus `securityLevel` /
|
|
248
|
+
`link` / `verifyKeyResolver` / `defaultTimeout`.
|
|
240
249
|
|
|
241
250
|
> On Cloudflare, [`@nice-code/action/platform/cloudflare`](#cloudflare-durable-objects) collapses the
|
|
242
|
-
> Durable Object carrier + storage boilerplate
|
|
251
|
+
> Durable Object carrier + storage + lifecycle boilerplate into a single `serveDurableObject` call.
|
|
252
|
+
>
|
|
253
|
+
> Serving in another environment (Bun, Node, …)? `serveChannel`'s carriers are environment-neutral, and
|
|
254
|
+
> `serveHost(runtime, channel, hostAdapter, options)` folds a reusable `{ carriers, storage, onServed }`
|
|
255
|
+
> host adapter into it — the same seam `serveDurableObject` is built on.
|
|
243
256
|
|
|
244
257
|
### Connecting — `connectChannel`
|
|
245
258
|
|
|
246
|
-
The connector has one runtime and `connectChannel`s to each acceptor it talks to.
|
|
247
|
-
|
|
248
|
-
|
|
259
|
+
The connector has one runtime and `connectChannel`s to each acceptor it talks to. One call binds the
|
|
260
|
+
shared facts — the channel (its codec + routing), the runtime, and one crypto identity over `storage` —
|
|
261
|
+
into every transport, so you state them *once*. It routes the channel's `toAcceptor` domains out over the
|
|
262
|
+
transports (first = preferred, rest = fallback) and registers local handlers for the `toConnector` pushes
|
|
263
|
+
from `onPush` — all derived from the channel, no restated lists. It's the exact dial-out dual of
|
|
264
|
+
`serveChannel`.
|
|
249
265
|
|
|
250
266
|
```ts
|
|
267
|
+
// client.ts
|
|
251
268
|
import {
|
|
252
269
|
ActionRuntime,
|
|
253
270
|
RuntimeCoordinate,
|
|
254
271
|
ESecurityLevel,
|
|
255
272
|
connectChannel,
|
|
256
|
-
secureTransport,
|
|
257
|
-
plainTransport,
|
|
258
273
|
wsCarrier,
|
|
259
274
|
httpCarrier,
|
|
260
275
|
} from "@nice-code/action";
|
|
261
276
|
|
|
262
277
|
export const clientRuntime = new ActionRuntime(RuntimeCoordinate.env("frontend"));
|
|
263
278
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
runtime: clientRuntime, // its coordinate is the authenticated identity in the handshake
|
|
268
|
-
storageAdapter, // persists this client's crypto identity across reloads
|
|
279
|
+
connectChannel(clientRuntime, appChannel, {
|
|
280
|
+
peer: serverCoord,
|
|
281
|
+
storage, // one crypto identity, fanned across every secure transport
|
|
269
282
|
securityLevel: ESecurityLevel.encrypted,
|
|
270
|
-
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
connectChannel(clientRuntime, serverCoord, {
|
|
277
|
-
channel: appChannel,
|
|
278
|
-
transports: [wsTransport, httpTransport], // secure WS preferred, HTTP fallback
|
|
283
|
+
transports: [
|
|
284
|
+
{ carrier: wsCarrier("wss://api.example.com/resolve_action/ws") }, // secure WS, preferred
|
|
285
|
+
{ carrier: httpCarrier(() => ({ url: "https://api.example.com/resolve_action" })), secure: false }, // plain HTTP fallback
|
|
286
|
+
],
|
|
279
287
|
// onPush: { ... } // handlers for the channel's toConnector pushes (see below)
|
|
280
288
|
});
|
|
281
289
|
```
|
|
282
290
|
|
|
283
|
-
`connectChannel(runtime,
|
|
291
|
+
`connectChannel(runtime, channel, options)` returns the `ConnectorHandler` so you can later
|
|
284
292
|
`handler.clearTransportCache()` (which also closes any live sockets) on teardown. Options:
|
|
285
293
|
|
|
286
|
-
- **`
|
|
287
|
-
- **`transports`** —
|
|
288
|
-
manager falls through on failure.
|
|
294
|
+
- **`peer`** — the acceptor's `RuntimeCoordinate` this connection dials.
|
|
295
|
+
- **`transports`** — declared by *carrier* `{ carrier, secure? }`, in preference order; all carry the
|
|
296
|
+
channel's `toAcceptor` domains and the manager falls through on failure. `secure` defaults to `true`.
|
|
297
|
+
- **`storage`** — one backing store for the connection's crypto identity, fanned across every secure
|
|
298
|
+
transport. Required when any transport is secure; omit for a fully-plain connection. (`link` shares an
|
|
299
|
+
existing identity instead.)
|
|
300
|
+
- **`securityLevel`** — default level for secure transports (`authenticated` if omitted; override per
|
|
301
|
+
transport with `securityLevel` on the descriptor).
|
|
289
302
|
- **`onPush`** — handlers for the channel's `toConnector` pushes (optional; omit for send-only).
|
|
290
303
|
- **`defaultTimeout`** — default per-action timeout.
|
|
291
304
|
|
|
@@ -293,10 +306,11 @@ connectChannel(clientRuntime, serverCoord, {
|
|
|
293
306
|
|
|
294
307
|
## Calling actions
|
|
295
308
|
|
|
296
|
-
Once a runtime is wired, calling an action looks the same
|
|
297
|
-
over a carrier):
|
|
309
|
+
Once a runtime is wired, calling an action looks the same on **any** side — client or server — and
|
|
310
|
+
regardless of where it resolves (locally or over a carrier):
|
|
298
311
|
|
|
299
312
|
```ts
|
|
313
|
+
// any runtime with the domain wired
|
|
300
314
|
// Run and get the output directly (throws on a declared/transport error)
|
|
301
315
|
const output = await userDomain.action.getUser
|
|
302
316
|
.request({ userId: "u_123" })
|
|
@@ -326,7 +340,7 @@ connection — no second channel, no polling. The shape:
|
|
|
326
340
|
### Shared channel
|
|
327
341
|
|
|
328
342
|
```ts
|
|
329
|
-
//
|
|
343
|
+
// shared.ts — bidirectional: client sends `start_feed`; server pushes `position_update` back.
|
|
330
344
|
export const lobbyDomain = appRoot.createChildDomain({
|
|
331
345
|
domain: "lobby",
|
|
332
346
|
actions: {
|
|
@@ -348,9 +362,11 @@ export const appChannel = defineSecureChannel({
|
|
|
348
362
|
### Connector side — handle pushes with `onPush`
|
|
349
363
|
|
|
350
364
|
```ts
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
365
|
+
// client.ts
|
|
366
|
+
connectChannel(clientRuntime, appChannel, {
|
|
367
|
+
peer: serverCoord,
|
|
368
|
+
storage,
|
|
369
|
+
transports: [{ carrier: wsCarrier(wsUrl) }, { carrier: httpCarrier(httpUrl), secure: false }],
|
|
354
370
|
onPush: {
|
|
355
371
|
// Keyed by the toConnector action id; input + output typed from the channel.
|
|
356
372
|
position_update: async ({ player, x, y }) => {
|
|
@@ -366,6 +382,7 @@ connectChannel(clientRuntime, serverCoord, {
|
|
|
366
382
|
The local handler reads `action.context.originClient` to know who asked, then pushes to them:
|
|
367
383
|
|
|
368
384
|
```ts
|
|
385
|
+
// server.ts
|
|
369
386
|
const lobbyHandler = createLocalHandler().forDomainActionCases(lobbyDomain, {
|
|
370
387
|
start_feed: async (action) => {
|
|
371
388
|
let delivered = 0;
|
|
@@ -392,26 +409,28 @@ server.broadcast(
|
|
|
392
409
|
);
|
|
393
410
|
```
|
|
394
411
|
|
|
395
|
-
> **When a handler needs the originating connection itself** (to register it in a room,
|
|
396
|
-
> just the client coordinate, pass `channelCases` to `serveChannel`
|
|
397
|
-
>
|
|
398
|
-
> `
|
|
399
|
-
> (
|
|
412
|
+
> **When a handler needs the originating connection itself** (to register it in a room, track per-socket
|
|
413
|
+
> state, etc.) rather than just the client coordinate, pass `channelCases` to `serveChannel` /
|
|
414
|
+
> `serveDurableObject` — each case receives the request *and* an `IConnectionContext` (`conn.state` /
|
|
415
|
+
> `conn.setState` / `conn.broadcast({ exceptSelf }) ` / `conn.pushBack` / `conn.connection`). With several
|
|
416
|
+
> duplex carriers (no sole handler), reach for a specific `server.handlers[i].broadcast(...)` and
|
|
417
|
+
> `acceptChannelConnections(handler, channel, { ... })` directly (see
|
|
418
|
+
> [Lower-level building blocks](#lower-level-building-blocks)).
|
|
400
419
|
|
|
401
420
|
---
|
|
402
421
|
|
|
403
422
|
## Cloudflare Durable Objects
|
|
404
423
|
|
|
405
|
-
`@nice-code/action/platform/cloudflare` collapses the DO
|
|
406
|
-
|
|
407
|
-
|
|
424
|
+
`@nice-code/action/platform/cloudflare` collapses the *entire* DO transport stack — the hibernatable
|
|
425
|
+
secure WebSocket, an HTTP fallback, the DO-storage crypto identity, and the `ping`/`pong` keepalive —
|
|
426
|
+
into a single `serveDurableObject` call, leaving the DO to forward its four socket lifecycle methods. The
|
|
427
|
+
core library stays platform-agnostic — nothing here is reachable from the main entry.
|
|
408
428
|
|
|
409
429
|
```ts
|
|
410
430
|
import { DurableObject } from "cloudflare:workers";
|
|
411
|
-
import { ActionRuntime
|
|
431
|
+
import { ActionRuntime } from "@nice-code/action";
|
|
412
432
|
import {
|
|
413
|
-
|
|
414
|
-
durableObjectStorage,
|
|
433
|
+
serveDurableObject,
|
|
415
434
|
type TDurableObjectChannelServer,
|
|
416
435
|
} from "@nice-code/action/platform/cloudflare";
|
|
417
436
|
|
|
@@ -422,13 +441,12 @@ export class MyDurableObject extends DurableObject {
|
|
|
422
441
|
if (this._server != null) return this._server;
|
|
423
442
|
const runtime = new ActionRuntime(serverCoord);
|
|
424
443
|
|
|
425
|
-
//
|
|
426
|
-
|
|
427
|
-
|
|
444
|
+
// Hibernatable secure WS + plain HTTP fallback + DO-storage crypto identity + keepalive, all folded in.
|
|
445
|
+
this._server = serveDurableObject(this.ctx, appChannel, {
|
|
446
|
+
runtime,
|
|
428
447
|
clientEnv: RuntimeCoordinate.env("frontend"),
|
|
429
|
-
|
|
448
|
+
keyPrefix: "ws:",
|
|
430
449
|
handlers: [userHandler],
|
|
431
|
-
carriers: [durableObjectWsCarrier(this.ctx), httpAcceptorCarrier()],
|
|
432
450
|
});
|
|
433
451
|
return this._server;
|
|
434
452
|
}
|
|
@@ -437,40 +455,53 @@ export class MyDurableObject extends DurableObject {
|
|
|
437
455
|
return this.getServer().fetch(request);
|
|
438
456
|
}
|
|
439
457
|
async webSocketMessage(ws: WebSocket, msg: string | ArrayBuffer) {
|
|
440
|
-
this.getServer().
|
|
458
|
+
this.getServer().receive(ws, msg);
|
|
441
459
|
}
|
|
442
|
-
async webSocketClose(ws: WebSocket) { this.getServer().
|
|
443
|
-
async webSocketError(ws: WebSocket) { this.getServer().
|
|
460
|
+
async webSocketClose(ws: WebSocket) { this.getServer().drop(ws); }
|
|
461
|
+
async webSocketError(ws: WebSocket) { this.getServer().drop(ws); }
|
|
444
462
|
}
|
|
445
463
|
```
|
|
446
464
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
- **`
|
|
453
|
-
`
|
|
465
|
+
`serveDurableObject(ctx, channel, options)` takes the same `serveChannel` surface (`clientEnv`,
|
|
466
|
+
`handlers`, `channelCases`, `connectionState`, …) plus the host knobs:
|
|
467
|
+
|
|
468
|
+
- **`runtime`** — this DO's runtime.
|
|
469
|
+
- **`keyPrefix`** — namespace for the DO-storage crypto-identity keys.
|
|
470
|
+
- **`httpFallback`** — `"plain"` (default), `"secure"` (handshake-protected exchange sharing the WS
|
|
471
|
+
identity), or `false` (WebSocket only).
|
|
472
|
+
- **`secure`** — whether the WebSocket itself runs the handshake (default `true`).
|
|
473
|
+
|
|
474
|
+
For finer control the lower-level pieces are still exported: `cloudflareDurableObjectHost(ctx, opts)`
|
|
475
|
+
builds just the `{ carriers, storage, onServed }` host adapter (hand it to `serveHost`), and
|
|
476
|
+
`durableObjectWsCarrier` / `durableObjectStorage` build the individual carrier / storage adapter.
|
|
454
477
|
|
|
455
478
|
### Per-connection state + broadcast (stateful DOs)
|
|
456
479
|
|
|
457
|
-
A presence/room DO that tracks who's on each socket and fans messages out adds two
|
|
458
|
-
|
|
480
|
+
A presence/room DO that tracks who's on each socket and fans messages out adds two knobs —
|
|
481
|
+
`connectionState` (typed per-socket app state, co-stored with the routing binding so both survive a wake)
|
|
482
|
+
and `channelCases`. Each case gets an **`IConnectionContext`** as its second argument — the originating
|
|
483
|
+
connection plus everything a case reaches for, so it never threads `ws` through `server.connections` /
|
|
484
|
+
`server.broadcast` by hand:
|
|
459
485
|
|
|
460
486
|
```ts
|
|
461
|
-
const server =
|
|
487
|
+
const server = serveDurableObject(this.ctx, lobbyChannel, {
|
|
488
|
+
runtime,
|
|
462
489
|
clientEnv: RuntimeCoordinate.env("web"),
|
|
463
|
-
|
|
464
|
-
carriers: [durableObjectWsCarrier(this.ctx), httpAcceptorCarrier({ secure: false })],
|
|
465
|
-
// Co-store each player's identity with the routing binding in the socket attachment (survives a wake).
|
|
490
|
+
keyPrefix: "lobby-ws:",
|
|
466
491
|
connectionState: { schema: vs_player },
|
|
467
|
-
// Connection-aware cases: each gets the request + the originating socket.
|
|
468
492
|
channelCases: {
|
|
469
|
-
join: (action,
|
|
470
|
-
|
|
471
|
-
|
|
493
|
+
join: (action, conn) => {
|
|
494
|
+
conn.setState(action.input); // typed by the schema; co-stored with the binding
|
|
495
|
+
conn.broadcast(() => lobbyPush.action.player_joined.request(action.input), { exceptSelf: true });
|
|
472
496
|
return { players: this.roster() };
|
|
473
497
|
},
|
|
498
|
+
move: (action, conn) => {
|
|
499
|
+
const player = conn.state; // this socket's typed app state (or null)
|
|
500
|
+
if (!player) return; // a move before join is dropped
|
|
501
|
+
conn.broadcast(() => lobbyPush.action.player_moved.request({ id: player.id, ...action.input }), {
|
|
502
|
+
exceptSelf: true,
|
|
503
|
+
});
|
|
504
|
+
},
|
|
474
505
|
},
|
|
475
506
|
});
|
|
476
507
|
|
|
@@ -478,18 +509,18 @@ const server = serveChannel(runtime, lobbyChannel, {
|
|
|
478
509
|
for (const [, player] of server.connections.entries()) this.players.set(player.id, player);
|
|
479
510
|
```
|
|
480
511
|
|
|
481
|
-
`
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
512
|
+
The `IConnectionContext` (`conn`) gives a case: `conn.state` / `conn.setState(x)` / `conn.clearState()`
|
|
513
|
+
(the typed app state), `conn.broadcast(makeRequest, { exceptSelf })`, `conn.pushBack(request)` (push down
|
|
514
|
+
this same socket), and `conn.connection` (the raw socket, `null` on the HTTP-exchange path). Passing
|
|
515
|
+
`connectionState` narrows the return so `server.connections` is non-optional. Because the WS carrier
|
|
516
|
+
persists each connection's binding on bind and replays it on construction, results and pushes still route
|
|
517
|
+
to the right socket after the object wakes from eviction.
|
|
487
518
|
|
|
488
519
|
---
|
|
489
520
|
|
|
490
521
|
## Security levels
|
|
491
522
|
|
|
492
|
-
`ESecurityLevel` (used by `
|
|
523
|
+
`ESecurityLevel` (used by `connectChannel` and `serveChannel`):
|
|
493
524
|
|
|
494
525
|
- **`none`** — identity self-asserted, no handshake. Fastest; fine for dev/trusted networks.
|
|
495
526
|
- **`authenticated`** — the handshake verifies identity (sign/verify + trust-on-first-use key pin);
|
|
@@ -502,7 +533,7 @@ crypto identity; the server pins client keys trust-on-first-use. Persisting the
|
|
|
502
533
|
hibernatable carrier) lets an `authenticated`/`encrypted` connection resume after eviction without
|
|
503
534
|
re-handshaking.
|
|
504
535
|
|
|
505
|
-
The whole thing rides one channel: the same `
|
|
536
|
+
The whole thing rides one channel: the same secure `{ carrier: wsCarrier(...) }` transport works at any
|
|
506
537
|
level, and `httpCarrier` runs the *same* secure session over HTTP (handshake → token → encrypted frames),
|
|
507
538
|
with the request/reply correlation provided for free by the HTTP transaction. Pair a secure WS with a
|
|
508
539
|
plain HTTP fallback by giving the acceptor a `httpAcceptorCarrier({ secure: false })`.
|
|
@@ -514,7 +545,8 @@ plain HTTP fallback by giving the acceptor a `httpAcceptorCarrier({ secure: fals
|
|
|
514
545
|
`serveChannel` accepts **any number of duplex carriers** (e.g. WebSocket + WebRTC) plus at most one
|
|
515
546
|
exchange carrier. They all share one crypto identity and one runtime, and each result/push routes back
|
|
516
547
|
over the carrier its client actually connected on (connection-aware return routing). With several duplex
|
|
517
|
-
carriers, `server.
|
|
548
|
+
carriers, `server.receive`/`server.drop` throw (they can't pick a carrier) — feed each carrier handle's
|
|
549
|
+
own `receive`/`drop` directly.
|
|
518
550
|
|
|
519
551
|
```ts
|
|
520
552
|
const ws = wsAcceptorCarrier({ send: wsSend, upgrade, attachmentStore });
|
|
@@ -541,18 +573,20 @@ predicate is re-evaluated per action dispatch, so the transport switches on the
|
|
|
541
573
|
holds, with no reconnect.
|
|
542
574
|
|
|
543
575
|
```ts
|
|
544
|
-
//
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
576
|
+
// client.ts — prefer the secure socket, but only once a session id exists; fall back to HTTP meanwhile.
|
|
577
|
+
connectChannel(runtime, appChannel, {
|
|
578
|
+
peer: serverCoord,
|
|
579
|
+
storage,
|
|
580
|
+
transports: [
|
|
581
|
+
{
|
|
582
|
+
carrier: wsCarrier(`${url}/ws`, {
|
|
583
|
+
// Only ever called once `available` passes, so no need for a placeholder cache key.
|
|
584
|
+
getTransportCacheKey: () => [sessionId],
|
|
585
|
+
}),
|
|
586
|
+
available: () => sessionId != null, // ws preferred; HTTP serves while ws is gated off
|
|
587
|
+
},
|
|
588
|
+
{ carrier: httpCarrier(httpUrl), secure: false },
|
|
589
|
+
],
|
|
556
590
|
});
|
|
557
591
|
```
|
|
558
592
|
|
|
@@ -656,17 +690,14 @@ try {
|
|
|
656
690
|
|
|
657
691
|
## Lower-level building blocks
|
|
658
692
|
|
|
659
|
-
`connectChannel` and `serveChannel` are
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
— what `connectChannel` desugars to: build a `ConnectorHandler` for a peer, route domains/actions to
|
|
663
|
-
it, register it (plus any local push handlers), and apply — in one call. Use it directly when your
|
|
664
|
-
routing isn't expressed as a single channel.
|
|
693
|
+
`connectChannel` and `serveChannel` are the supported entry points — they bind the channel, runtime, and
|
|
694
|
+
crypto identity for you. The pieces below are what they're built on; reach for them only for the rare
|
|
695
|
+
routing that isn't a single channel.
|
|
665
696
|
|
|
666
697
|
- **Carriers vs transports.** A *carrier* (`wsCarrier`, `httpCarrier`, `inMemoryCarrier`, `rtcCarrier`) is
|
|
667
|
-
raw byte movement; a *transport*
|
|
668
|
-
`
|
|
669
|
-
|
|
698
|
+
raw byte movement; a *transport* wraps one with a security policy. You name carriers in
|
|
699
|
+
`connectChannel`'s `transports` (the `secure` flag picks the policy) and in `serveChannel`'s `carriers`;
|
|
700
|
+
the transport wrapping happens internally, so there's no separate transport-builder to call.
|
|
670
701
|
|
|
671
702
|
- **`acceptChannel(runtime, channel, { clientEnv, storageAdapter, send, ... })`** — build the secure
|
|
672
703
|
`AcceptorHandler` for a channel by hand (the accept-in counterpart to a single transport), when you're
|
|
@@ -695,5 +726,5 @@ try {
|
|
|
695
726
|
same-process peers) with no network.
|
|
696
727
|
|
|
697
728
|
- **Custom carriers** — for any channel nice-action doesn't model natively, implement an
|
|
698
|
-
`IDuplexCarrierSource` / `IExchangeCarrierSource` and
|
|
729
|
+
`IDuplexCarrierSource` / `IExchangeCarrierSource` and name it in `connectChannel`'s `transports`
|
|
699
730
|
(connector) or build an acceptor carrier for `serveChannel`.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { _ as TActionProgress, zr as IRuntimeCoordinate } from "./ActionPayload.types-L9k0LyBd.cjs";
|
|
2
2
|
//#region ../nice-devtools-shared/src/components/PanelChrome.d.ts
|
|
3
3
|
/** Where a devtools panel is docked. */
|
|
4
4
|
type TDevtoolsPosition = "dock-bottom" | "dock-top" | "dock-left" | "dock-right";
|
|
@@ -76,4 +76,4 @@ declare class ActionDevtoolsCore {
|
|
|
76
76
|
}
|
|
77
77
|
//#endregion
|
|
78
78
|
export { TDevtoolsActionStatus as a, IDevtoolsObservableDomain as i, IActionDevtoolsCoreOptions as n, TDevtoolsListener as o, IDevtoolsActionEntry as r, TDevtoolsPosition as s, ActionDevtoolsCore as t };
|
|
79
|
-
//# sourceMappingURL=ActionDevtoolsCore-
|
|
79
|
+
//# sourceMappingURL=ActionDevtoolsCore-CQ0vrvPD.d.cts.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { _ as TActionProgress, zr as IRuntimeCoordinate } from "./ActionPayload.types-Dx1JPyfs.mjs";
|
|
2
2
|
//#region ../nice-devtools-shared/src/components/PanelChrome.d.ts
|
|
3
3
|
/** Where a devtools panel is docked. */
|
|
4
4
|
type TDevtoolsPosition = "dock-bottom" | "dock-top" | "dock-left" | "dock-right";
|
|
@@ -76,4 +76,4 @@ declare class ActionDevtoolsCore {
|
|
|
76
76
|
}
|
|
77
77
|
//#endregion
|
|
78
78
|
export { TDevtoolsActionStatus as a, IDevtoolsObservableDomain as i, IActionDevtoolsCoreOptions as n, TDevtoolsListener as o, IDevtoolsActionEntry as r, TDevtoolsPosition as s, ActionDevtoolsCore as t };
|
|
79
|
-
//# sourceMappingURL=ActionDevtoolsCore-
|
|
79
|
+
//# sourceMappingURL=ActionDevtoolsCore-CiLBYC3K.d.mts.map
|