@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.
Files changed (32) hide show
  1. package/README.md +140 -109
  2. package/build/{ActionDevtoolsCore-D_JvgPmz.d.mts → ActionDevtoolsCore-CQ0vrvPD.d.cts} +2 -2
  3. package/build/{ActionDevtoolsCore-dV-IVPcP.d.cts → ActionDevtoolsCore-CiLBYC3K.d.mts} +2 -2
  4. package/build/{ActionPayload.types-CnfWlkA1.d.cts → ActionPayload.types-Dx1JPyfs.d.mts} +292 -222
  5. package/build/{ActionPayload.types-D0DM-g65.d.mts → ActionPayload.types-L9k0LyBd.d.cts} +292 -222
  6. package/build/devtools/browser/index.d.cts +1 -1
  7. package/build/devtools/browser/index.d.mts +1 -1
  8. package/build/devtools/server/index.d.cts +1 -1
  9. package/build/devtools/server/index.d.mts +1 -1
  10. package/build/httpAcceptorCarrier-DL8lf0xB.mjs +3906 -0
  11. package/build/httpAcceptorCarrier-DL8lf0xB.mjs.map +1 -0
  12. package/build/httpAcceptorCarrier-OnJxzsAD.cjs +4291 -0
  13. package/build/httpAcceptorCarrier-OnJxzsAD.cjs.map +1 -0
  14. package/build/index.cjs +395 -4125
  15. package/build/index.cjs.map +1 -1
  16. package/build/index.d.cts +2 -2
  17. package/build/index.d.mts +2 -2
  18. package/build/index.mjs +331 -4058
  19. package/build/index.mjs.map +1 -1
  20. package/build/platform/cloudflare/index.cjs +30 -2
  21. package/build/platform/cloudflare/index.cjs.map +1 -1
  22. package/build/platform/cloudflare/index.d.cts +55 -2
  23. package/build/platform/cloudflare/index.d.mts +55 -2
  24. package/build/platform/cloudflare/index.mjs +28 -2
  25. package/build/platform/cloudflare/index.mjs.map +1 -1
  26. package/build/react-query/index.d.cts +1 -1
  27. package/build/react-query/index.d.mts +1 -1
  28. package/package.json +4 -4
  29. package/build/wsAcceptorCarrier-BDJRIPfu.cjs +0 -103
  30. package/build/wsAcceptorCarrier-BDJRIPfu.cjs.map +0 -1
  31. package/build/wsAcceptorCarrier-CW2qX25W.mjs +0 -80
  32. 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: `secureTransport` (handshake + optional encryption) or `plainTransport` |
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` / `duplex` / `pushToClient` you forward straight to the host.
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.duplex?.receive(ws, msg)
223
- // webSocketClose/Error(ws) => server.duplex?.drop(ws)
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, duplex?, pushToClient }`:
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
- - **`duplex`** the `{ receive, drop }` lifecycle for the *sole* duplex carrier (a shortcut for the
231
- common single-WebSocket case; `undefined` when there are zero or several then feed each carrier
232
- handle directly).
233
- - **`pushToClient(target, request, opts?)`** — push a server-initiated action to one connected client
234
- (see [Bi-directional](#bi-directional-communication-acceptor--connector)).
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`, plus `securityLevel` / `link` / `verifyKeyResolver` / `defaultTimeout`.
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 to one-liners.
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. It routes the channel's
247
- `toAcceptor` domains out over the transports (first = preferred, rest = fallback) and registers local
248
- handlers for the `toConnector` pushes from `onPush` all derived from the channel, no restated lists.
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
- // A carrier wrapped in a security policy = a transport.
265
- const wsTransport = secureTransport({
266
- channel: appChannel,
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
- carrier: wsCarrier("wss://api.example.com/resolve_action/ws"),
271
- });
272
- const httpTransport = plainTransport({
273
- carrier: httpCarrier(() => ({ url: "https://api.example.com/resolve_action" })),
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, acceptorCoordinate, options)` returns the `ConnectorHandler` so you can later
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
- - **`channel`** — the shared channel; its `toAcceptor`/`toConnector` drive all routing.
287
- - **`transports`** — to the acceptor, in preference order; all carry the same `toAcceptor` domains and the
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 regardless of where it resolves (locally or
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
- // Bidirectional: client sends `start_feed`; server pushes `position_update` back.
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
- connectChannel(clientRuntime, serverCoord, {
352
- channel: appChannel,
353
- transports: [wsTransport, httpTransport],
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, etc.) rather than
396
- > just the client coordinate, pass `channelCases` to `serveChannel` — each case receives the request *and*
397
- > that client's live connection. With several duplex carriers (no sole handler), reach for a specific
398
- > `server.handlers[i].broadcast(...)` and `acceptChannelConnections(handler, channel, { ... })` directly
399
- > (see [Lower-level building blocks](#lower-level-building-blocks)).
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 boilerplate (the `WebSocketPair` upgrade, the
406
- hibernation attachment wiring, and the DO-storage adapter) into one-liners you hand to `serveChannel`.
407
- The core library stays platform-agnostic nothing here is reachable from the main entry.
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, serveChannel, httpAcceptorCarrier } from "@nice-code/action";
431
+ import { ActionRuntime } from "@nice-code/action";
412
432
  import {
413
- durableObjectWsCarrier,
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
- // One WS carrier (duplex pushes + hibernation persistence) + a secure HTTP fallback (exchange),
426
- // both sharing one crypto identity built from this DO's storage, surviving eviction.
427
- this._server = serveChannel(runtime, appChannel, {
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
- storage: durableObjectStorage(this.ctx, { keyPrefix: "ws:" }),
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().duplex?.receive(ws, msg);
458
+ this.getServer().receive(ws, msg);
441
459
  }
442
- async webSocketClose(ws: WebSocket) { this.getServer().duplex?.drop(ws); }
443
- async webSocketError(ws: WebSocket) { this.getServer().duplex?.drop(ws); }
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
- - **`durableObjectWsCarrier(ctx, { secure? })`** a hibernatable-WebSocket acceptor carrier with `send`,
448
- the `WebSocketPair` upgrade, and the socket-attachment access all derived from the DO's `ctx`. Pass
449
- `secure: false` for a plain WS endpoint (then no `storage` is needed for it). It exposes the attachment to
450
- `serveChannel`, which owns the layout: it persists the routing binding there and (when `connectionState`
451
- is requested) co-stores per-connection app state in the *same* slot, so both survive eviction.
452
- - **`durableObjectStorage(ctx, { keyPrefix? })`** — wraps the DO's storage as a `StorageAdapter` for
453
- `serveChannel`'s `storage`.
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 `serveChannel` knobs —
458
- no `handlers[0]`, no hand-wired attachment store:
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 = serveChannel(runtime, lobbyChannel, {
487
+ const server = serveDurableObject(this.ctx, lobbyChannel, {
488
+ runtime,
462
489
  clientEnv: RuntimeCoordinate.env("web"),
463
- storage: durableObjectStorage(this.ctx, { keyPrefix: "lobby-ws:" }),
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, ws) => {
470
- if (ws) server.connections.set(ws, action.input); // typed by the schema
471
- server.broadcast(() => lobbyPush.action.player_joined.request(action.input), { except: ws });
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
- `connectionState` narrows the return so `server.connections` is non-optional, and `server.broadcast`
482
- fans out over the sole duplex carrier. The store's `read`/`write` are pluggable, so a DO that prefers to
483
- keep state in its SQLite/KV storage rather than the socket attachment can supply its own.
484
-
485
- Because the WS carrier persists each connection's binding on bind and replays it on construction, results
486
- and pushes still route to the right socket after the object wakes from eviction.
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 `secureTransport` and `serveChannel`):
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 `secureTransport({ carrier: wsCarrier(...) })` works at any
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.duplex` is `undefined` — feed each carrier handle's own `receive`/`drop` directly.
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
- // Prefer the secure socket, but only once a session id exists; fall back to HTTP meanwhile.
545
- const wsTransport = secureTransport({
546
- channel, runtime, storageAdapter, securityLevel,
547
- available: () => sessionId != null,
548
- carrier: wsCarrier(`${url}/ws`, {
549
- // Only ever called once `available` passes, so no need for a placeholder cache key.
550
- getTransportCacheKey: () => [sessionId],
551
- }),
552
- });
553
-
554
- connectChannel(runtime, serverCoord, {
555
- transports: [wsTransport, httpTransport], // ws preferred; HTTP serves while ws is gated off
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 sugar; reach for these when you need finer control.
660
-
661
- - **`ActionRuntime.connectTo(coordinate, { transports, domains, actions, localHandlers, defaultTimeout })`**
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* (`secureTransport`, `plainTransport`) wraps one with a security policy.
668
- `connectTo` takes transports; `serveChannel` takes acceptor carriers (`wsAcceptorCarrier`,
669
- `httpAcceptorCarrier`) and applies the security policy itself.
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 hand it to `secureTransport` / `plainTransport`
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 { Lr as IRuntimeCoordinate, _ as TActionProgress } from "./ActionPayload.types-D0DM-g65.mjs";
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-D_JvgPmz.d.mts.map
79
+ //# sourceMappingURL=ActionDevtoolsCore-CQ0vrvPD.d.cts.map
@@ -1,4 +1,4 @@
1
- import { Lr as IRuntimeCoordinate, _ as TActionProgress } from "./ActionPayload.types-CnfWlkA1.cjs";
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-dV-IVPcP.d.cts.map
79
+ //# sourceMappingURL=ActionDevtoolsCore-CiLBYC3K.d.mts.map