@nice-code/action 0.23.0 → 0.25.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 (40) hide show
  1. package/README.md +71 -26
  2. package/build/{ActionPayload.types-B-OSg09t.d.mts → AcceptorHandler-BizUtq4u.d.mts} +1267 -1543
  3. package/build/{ActionPayload.types-DIOeVapm.d.cts → AcceptorHandler-CxPfZtIl.d.cts} +1267 -1543
  4. package/build/{ActionDevtoolsCore-BjbhFqc0.d.mts → ActionDevtoolsCore-D9KBBI2V.d.cts} +2 -2
  5. package/build/{ActionDevtoolsCore-kk7oZBv9.d.cts → ActionDevtoolsCore-xZjAtB4H.d.mts} +2 -2
  6. package/build/advanced/index.cjs +115 -0
  7. package/build/advanced/index.cjs.map +1 -0
  8. package/build/advanced/index.d.cts +249 -0
  9. package/build/advanced/index.d.mts +249 -0
  10. package/build/advanced/index.mjs +88 -0
  11. package/build/advanced/index.mjs.map +1 -0
  12. package/build/{httpAcceptorCarrier-DJVxzDVd.mjs → createHibernatableWsServerAdapter-BkjESd01.mjs} +243 -429
  13. package/build/createHibernatableWsServerAdapter-BkjESd01.mjs.map +1 -0
  14. package/build/{httpAcceptorCarrier-hYPuoNuP.cjs → createHibernatableWsServerAdapter-FSDWrxoF.cjs} +268 -478
  15. package/build/createHibernatableWsServerAdapter-FSDWrxoF.cjs.map +1 -0
  16. package/build/devtools/browser/index.d.cts +1 -1
  17. package/build/devtools/browser/index.d.mts +1 -1
  18. package/build/devtools/server/index.d.cts +1 -1
  19. package/build/devtools/server/index.d.mts +1 -1
  20. package/build/httpAcceptorCarrier-BQYaXI9j.cjs +454 -0
  21. package/build/httpAcceptorCarrier-BQYaXI9j.cjs.map +1 -0
  22. package/build/httpAcceptorCarrier-DWqsCz3h.mjs +401 -0
  23. package/build/httpAcceptorCarrier-DWqsCz3h.mjs.map +1 -0
  24. package/build/index.cjs +73 -449
  25. package/build/index.cjs.map +1 -1
  26. package/build/index.d.cts +2 -2
  27. package/build/index.d.mts +2 -2
  28. package/build/index.mjs +13 -365
  29. package/build/index.mjs.map +1 -1
  30. package/build/platform/cloudflare/index.cjs +45 -1
  31. package/build/platform/cloudflare/index.cjs.map +1 -1
  32. package/build/platform/cloudflare/index.d.cts +42 -4
  33. package/build/platform/cloudflare/index.d.mts +42 -4
  34. package/build/platform/cloudflare/index.mjs +45 -2
  35. package/build/platform/cloudflare/index.mjs.map +1 -1
  36. package/build/react-query/index.d.cts +1 -1
  37. package/build/react-query/index.d.mts +1 -1
  38. package/package.json +15 -4
  39. package/build/httpAcceptorCarrier-DJVxzDVd.mjs.map +0 -1
  40. package/build/httpAcceptorCarrier-hYPuoNuP.cjs.map +0 -1
package/README.md CHANGED
@@ -31,7 +31,7 @@ The pieces:
31
31
  | **ActionDomain** | A named group of typed actions (your API surface) |
32
32
  | **ActionSchema** | Input/output schema + declared error types for one action |
33
33
  | **ActionRuntime** | One per runtime; identifies it and dispatches actions to handlers |
34
- | **Channel** | The transport-agnostic routing contract between two runtimes, declared *by role* (`toAcceptor` / `toConnector`) `defineChannel` (plain) or `defineSecureChannel` (binary + encryption) |
34
+ | **Channel** | The transport-agnostic routing contract + binary wire identity between two runtimes, declared *by role* (`toAcceptor` / `toConnector`) with `defineChannel`. Security is a per-transport choice, not a channel one |
35
35
  | **Carrier** | How bytes actually move: `wsCarrier` / `httpCarrier` / `inMemoryCarrier` / `rtcCarrier` (connector side), `wsAcceptorCarrier` / `httpAcceptorCarrier` (acceptor side) |
36
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 |
@@ -130,30 +130,21 @@ Define it once in code shared by both ends:
130
130
  import { defineChannel } from "@nice-code/action";
131
131
 
132
132
  export const appChannel = defineChannel({
133
- toAcceptor: [userDomain], // client → server requests
134
- toConnector: [], // server → client pushes (none here)
133
+ toAcceptor: [userDomain, lobbyDomain], // client → server requests
134
+ toConnector: [lobbyDomain], // server → client pushes (lobbyDomain is bidirectional)
135
135
  });
136
136
  ```
137
137
 
138
- For an authenticated/encrypted binary WebSocket, use **`defineSecureChannel`** insteadsame role-based
139
- shape, plus it bakes in the positional binary wire dictionary and a version derived from the domains, so
140
- the codec and version can never drift between the two ends:
141
-
142
- ```ts
143
- import { defineSecureChannel } from "@nice-code/action";
144
-
145
- export const appChannel = defineSecureChannel({
146
- toAcceptor: [userDomain, lobbyDomain], // requests
147
- toConnector: [lobbyDomain], // pushes (lobbyDomain is bidirectional)
148
- });
149
- ```
138
+ A channel carries both its routing (`toAcceptor` / `toConnector` domains) **and** its wire identity the
139
+ positional binary wire dictionary and a version derived from the domains, so the per-connection codec and
140
+ version can never drift between the two ends. Whether a given transport actually runs encrypted is a
141
+ per-transport choice (`secure` on `connectChannel`'s transports and the acceptor's `securityLevel`), not a
142
+ property of the channel — so this one definition serves both plain and secure transports.
150
143
 
151
144
  > The lists are **positional** for the binary wire dictionary — **add new domains to the end** of their
152
145
  > list. Reordering shifts the version, and a stale peer is then cleanly rejected by the handshake instead
153
146
  > of silently misrouting a frame.
154
147
 
155
- A secure channel is still an ordinary channel, so it works anywhere a channel is expected.
156
-
157
148
  ---
158
149
 
159
150
  ## Runtimes & handlers
@@ -243,9 +234,17 @@ const server = serveChannel(runtime, appChannel, {
243
234
  - **`handlers`** — the acceptor handlers it built, one per duplex carrier (reach for these for per-handler
244
235
  `broadcast`).
245
236
 
246
- Key options: `clientEnv` (required), `storage` (required only when a carrier is secure — the default),
247
- `carriers`, `handlers`, `channelCases` (connection-aware cases — see below), plus `securityLevel` /
248
- `link` / `verifyKeyResolver` / `defaultTimeout`.
237
+ Key options: `clientEnv` (optional — see below), `storage` (required only when a carrier is secure — the
238
+ default), `carriers`, `handlers`, `channelCases` (connection-aware cases — see below), plus
239
+ `securityLevel` / `link` / `verifyKeyResolver` / `defaultTimeout`.
240
+
241
+ > **One server, several client envs (multi-role).** `clientEnv` is **optional**. A result or push to a
242
+ > *connected* client always routes back over the carrier it actually connected on (the acceptor knows each
243
+ > client's exact coordinate from its handshake), so a single `serveChannel` accepts clients of *different*
244
+ > envs — e.g. a `wallet` role and a `partner` role on one bridge — over one acceptor. Omit `clientEnv`
245
+ > entirely for a multi-role server; only set it if you want a scoring fallback for returning to a client
246
+ > that is *no longer connected* (the offline case). This replaces the old workaround of standing up a
247
+ > second `serveChannel` per role.
249
248
 
250
249
  > On Cloudflare, [`@nice-code/action/platform/cloudflare`](#cloudflare-durable-objects) collapses the
251
250
  > Durable Object carrier + storage + lifecycle boilerplate into a single `serveDurableObject` call.
@@ -353,7 +352,7 @@ export const lobbyDomain = appRoot.createChildDomain({
353
352
  },
354
353
  });
355
354
 
356
- export const appChannel = defineSecureChannel({
355
+ export const appChannel = defineChannel({
357
356
  toAcceptor: [userDomain, lobbyDomain], // start_feed flows here
358
357
  toConnector: [lobbyDomain], // position_update pushes back here
359
358
  });
@@ -462,7 +461,7 @@ export class MyDurableObject extends DurableObject {
462
461
  }
463
462
  ```
464
463
 
465
- `serveDurableObject(ctx, channel, options)` takes the same `serveChannel` surface (`clientEnv`,
464
+ `serveDurableObject(ctx, channel, options)` takes the same `serveChannel` surface (`clientEnv` — optional,
466
465
  `handlers`, `channelCases`, `connectionState`, …) plus the host knobs:
467
466
 
468
467
  - **`runtime`** — this DO's runtime.
@@ -475,6 +474,45 @@ For finer control the lower-level pieces are still exported: `cloudflareDurableO
475
474
  builds just the `{ carriers, storage, onServed }` host adapter (hand it to `serveHost`), and
476
475
  `durableObjectWsCarrier` / `durableObjectStorage` build the individual carrier / storage adapter.
477
476
 
477
+ ### Fanning HTTP out to a per-id Durable Object — `forwardExchangeToDurableObject`
478
+
479
+ When each DO instance is one *thing* (a bridge, a room, a game) and the Worker has to pick the right one
480
+ per request, route by the **URL** — a secure exchange body is opaque to the Worker (handshake / encrypted
481
+ frames), so it can't choose by inspecting the body. `forwardExchangeToDurableObject` picks the stub, hands
482
+ the request to its `fetch` (where `serveDurableObject` serves the exchange), and answers the CORS
483
+ `OPTIONS` preflight *at the edge* so a per-id DO is never woken (or billed) just to reply to a preflight:
484
+
485
+ ```ts
486
+ import { forwardExchangeToDurableObject } from "@nice-code/action/platform/cloudflare";
487
+
488
+ export default {
489
+ fetch(request: Request, env: Env) {
490
+ return forwardExchangeToDurableObject(request, (req, url) => {
491
+ const bridgeId = url.pathname.split("/")[2]; // e.g. /bridge/:id/action
492
+ return env.BRIDGE.get(env.BRIDGE.idFromName(bridgeId));
493
+ });
494
+ },
495
+ };
496
+ ```
497
+
498
+ Inside the DO, serve the exchange (and the WS upgrade) exactly as above — make the HTTP fallback secure so
499
+ the whole path is handshake-protected:
500
+
501
+ ```ts
502
+ this._server = serveDurableObject(this.ctx, bridgeChannel, { runtime, httpFallback: "secure" });
503
+ // fetch(request) => this._server.fetch(request)
504
+ ```
505
+
506
+ The matching connector points its `httpCarrier` at `/bridge/:id/action`. `pickStub` may be async (e.g. to
507
+ resolve an id first) and receives the parsed `URL` alongside the request.
508
+
509
+ > Need to handle the exchange yourself instead of handing the endpoint to `serveChannel`? The secure
510
+ > exchange acceptor and its plain `{k:"act",w}` envelope codec are exported from the **main** entry:
511
+ > `ExchangeAcceptor` (drive the handshake + token sessions + decrypt over your own `fetch`), and
512
+ > `encodeExchange` / `decodeExchangeRequest` / `decodeExchangeReply` (read/write the plain envelope when
513
+ > you must inspect or rewrite the wire before running it). Secure bodies are only decodable through an
514
+ > `ExchangeAcceptor` session — route-before-decode by URL, as above.
515
+
478
516
  ### Per-connection state + broadcast (stateful DOs)
479
517
 
480
518
  A presence/room DO that tracks who's on each socket and fans messages out adds two knobs —
@@ -694,18 +732,25 @@ try {
694
732
  crypto identity for you. The pieces below are what they're built on; reach for them only for the rare
695
733
  routing that isn't a single channel.
696
734
 
735
+ > Most of these live under the **`@nice-code/action/advanced`** subpath, not the main entry — the default
736
+ > `@nice-code/action` export stays focused on the high-level API (`acceptChannel` /
737
+ > `acceptChannelConnections` and the carriers remain on the main entry). Import the raw handler classes
738
+ > (`AcceptorHandler`, `ConnectorHandler`, …), the transport classes/codec, and the handshake primitives
739
+ > from `@nice-code/action/advanced`.
740
+
697
741
  - **Carriers vs transports.** A *carrier* (`wsCarrier`, `httpCarrier`, `inMemoryCarrier`, `rtcCarrier`) is
698
742
  raw byte movement; a *transport* wraps one with a security policy. You name carriers in
699
743
  `connectChannel`'s `transports` (the `secure` flag picks the policy) and in `serveChannel`'s `carriers`;
700
744
  the transport wrapping happens internally, so there's no separate transport-builder to call.
701
745
 
702
- - **`acceptChannel(runtime, channel, { clientEnv, storageAdapter, send, ... })`** — build the secure
746
+ - **`acceptChannel(runtime, channel, { storage, send, clientEnv?, ... })`** — build the secure
703
747
  `AcceptorHandler` for a channel by hand (the accept-in counterpart to a single transport), when you're
704
- not using `serveChannel`. Pair it with **`acceptChannelConnections(handler, channel, cases)`** to
748
+ not using `serveChannel`. `clientEnv` is optional here too (one acceptor serves several client envs — a
749
+ live connection always wins the return path). Pair it with **`acceptChannelConnections(handler, channel, cases)`** to
705
750
  register connection-aware execution — each case receives the request *and* the originating connection:
706
751
 
707
752
  ```ts
708
- const acceptor = acceptChannel(runtime, appChannel, { clientEnv, storageAdapter, send });
753
+ const acceptor = acceptChannel(runtime, appChannel, { clientEnv, storage, send });
709
754
  const cases = acceptChannelConnections(acceptor, appChannel, {
710
755
  join: ({ input }, conn) => { if (conn != null) rooms.add(input.roomId, conn); return { ok: true }; },
711
756
  });
@@ -720,7 +765,7 @@ routing that isn't a single channel.
720
765
  `AcceptorHandler`.
721
766
 
722
767
  - **`createBinaryWireAdapter(domains)`** / **`createBinaryWireSessionFactory(domains)`** — the positional
723
- binary codecs `defineSecureChannel` builds for you; useful for custom carriers.
768
+ binary codecs `defineChannel` builds for you; useful for custom carriers.
724
769
 
725
770
  - **`createInMemoryChannelPair()` / `inMemoryCarrier`** — wire two runtimes together in-process (tests,
726
771
  same-process peers) with no network.