@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.
- package/README.md +71 -26
- package/build/{ActionPayload.types-B-OSg09t.d.mts → AcceptorHandler-BizUtq4u.d.mts} +1267 -1543
- package/build/{ActionPayload.types-DIOeVapm.d.cts → AcceptorHandler-CxPfZtIl.d.cts} +1267 -1543
- package/build/{ActionDevtoolsCore-BjbhFqc0.d.mts → ActionDevtoolsCore-D9KBBI2V.d.cts} +2 -2
- package/build/{ActionDevtoolsCore-kk7oZBv9.d.cts → ActionDevtoolsCore-xZjAtB4H.d.mts} +2 -2
- package/build/advanced/index.cjs +115 -0
- package/build/advanced/index.cjs.map +1 -0
- package/build/advanced/index.d.cts +249 -0
- package/build/advanced/index.d.mts +249 -0
- package/build/advanced/index.mjs +88 -0
- package/build/advanced/index.mjs.map +1 -0
- package/build/{httpAcceptorCarrier-DJVxzDVd.mjs → createHibernatableWsServerAdapter-BkjESd01.mjs} +243 -429
- package/build/createHibernatableWsServerAdapter-BkjESd01.mjs.map +1 -0
- package/build/{httpAcceptorCarrier-hYPuoNuP.cjs → createHibernatableWsServerAdapter-FSDWrxoF.cjs} +268 -478
- package/build/createHibernatableWsServerAdapter-FSDWrxoF.cjs.map +1 -0
- 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-BQYaXI9j.cjs +454 -0
- package/build/httpAcceptorCarrier-BQYaXI9j.cjs.map +1 -0
- package/build/httpAcceptorCarrier-DWqsCz3h.mjs +401 -0
- package/build/httpAcceptorCarrier-DWqsCz3h.mjs.map +1 -0
- package/build/index.cjs +73 -449
- 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 +13 -365
- package/build/index.mjs.map +1 -1
- package/build/platform/cloudflare/index.cjs +45 -1
- package/build/platform/cloudflare/index.cjs.map +1 -1
- package/build/platform/cloudflare/index.d.cts +42 -4
- package/build/platform/cloudflare/index.d.mts +42 -4
- package/build/platform/cloudflare/index.mjs +45 -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 +15 -4
- package/build/httpAcceptorCarrier-DJVxzDVd.mjs.map +0 -1
- 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`)
|
|
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],
|
|
134
|
-
toConnector: [],
|
|
133
|
+
toAcceptor: [userDomain, lobbyDomain], // client → server requests
|
|
134
|
+
toConnector: [lobbyDomain], // server → client pushes (lobbyDomain is bidirectional)
|
|
135
135
|
});
|
|
136
136
|
```
|
|
137
137
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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` (
|
|
247
|
-
`carriers`, `handlers`, `channelCases` (connection-aware cases — see below), plus
|
|
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 =
|
|
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, {
|
|
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`.
|
|
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,
|
|
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 `
|
|
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.
|