@nice-code/action 0.19.0 → 0.20.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 (30) hide show
  1. package/README.md +699 -666
  2. package/build/{ActionDevtoolsCore-CCZXQBAo.d.cts → ActionDevtoolsCore-D_JvgPmz.d.mts} +2 -2
  3. package/build/{ActionDevtoolsCore-bjYQ8O2_.d.mts → ActionDevtoolsCore-dV-IVPcP.d.cts} +2 -2
  4. package/build/{ActionPayload.types-Bmkzw2df.d.mts → ActionPayload.types-CnfWlkA1.d.cts} +161 -106
  5. package/build/{ActionPayload.types-CdHOGGZK.d.cts → ActionPayload.types-D0DM-g65.d.mts} +161 -106
  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/index.cjs +147 -138
  11. package/build/index.cjs.map +1 -1
  12. package/build/index.d.cts +2 -2
  13. package/build/index.d.mts +2 -2
  14. package/build/index.mjs +147 -138
  15. package/build/index.mjs.map +1 -1
  16. package/build/platform/cloudflare/index.cjs +8 -4
  17. package/build/platform/cloudflare/index.cjs.map +1 -1
  18. package/build/platform/cloudflare/index.d.cts +8 -3
  19. package/build/platform/cloudflare/index.d.mts +8 -3
  20. package/build/platform/cloudflare/index.mjs +8 -4
  21. package/build/platform/cloudflare/index.mjs.map +1 -1
  22. package/build/react-query/index.d.cts +1 -1
  23. package/build/react-query/index.d.mts +1 -1
  24. package/build/{wsAcceptorCarrier-DHRbsY1X.cjs → wsAcceptorCarrier-BDJRIPfu.cjs} +2 -2
  25. package/build/wsAcceptorCarrier-BDJRIPfu.cjs.map +1 -0
  26. package/build/{wsAcceptorCarrier-CXGlQU_f.mjs → wsAcceptorCarrier-CW2qX25W.mjs} +2 -2
  27. package/build/wsAcceptorCarrier-CW2qX25W.mjs.map +1 -0
  28. package/package.json +4 -4
  29. package/build/wsAcceptorCarrier-CXGlQU_f.mjs.map +0 -1
  30. package/build/wsAcceptorCarrier-DHRbsY1X.cjs.map +0 -1
package/build/index.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_rolldown_runtime = require("./rolldown-runtime-emK7D4bc.cjs");
3
3
  const require_RunningAction_types = require("./RunningAction.types-DjCX1xp5.cjs");
4
- const require_wsAcceptorCarrier = require("./wsAcceptorCarrier-DHRbsY1X.cjs");
4
+ const require_wsAcceptorCarrier = require("./wsAcceptorCarrier-BDJRIPfu.cjs");
5
5
  let nanoid = require("nanoid");
6
6
  let _nice_code_error = require("@nice-code/error");
7
7
  let _nice_code_common_errors = require("@nice-code/common-errors");
@@ -3538,6 +3538,113 @@ function createActionFetchHandler(runtime, options = {}) {
3538
3538
  };
3539
3539
  }
3540
3540
  //#endregion
3541
+ //#region src/ActionRuntime/Handler/PeerLink/Acceptor/Hibernation/ConnectionStateStore.ts
3542
+ /**
3543
+ * A typed per-connection state store that co-owns the app state and the acceptor handler's routing
3544
+ * binding in one attachment, so neither the consumer nor the handler has to hand-merge the two. Create
3545
+ * it through {@link createConnectionStateStore} (which also wires binding persistence and replays
3546
+ * surviving connections after a wake), then `get`/`set`/`clearApp` the app state directly.
3547
+ *
3548
+ * The mechanism is carrier-neutral — it only needs read/write/enumerate callbacks for the connection's
3549
+ * attachment — but it pays off on transports whose connections outlive process eviction (e.g. a
3550
+ * Durable Object's hibernatable WebSockets), which is why it lives beside the hibernation adapter.
3551
+ *
3552
+ * ```ts
3553
+ * const players = createConnectionStateStore(serverHandler, {
3554
+ * schema: vs_player,
3555
+ * read: (ws) => ws.deserializeAttachment(),
3556
+ * write: (ws, v) => ws.serializeAttachment(v),
3557
+ * getConnections: () => ctx.getWebSockets(),
3558
+ * });
3559
+ * players.set(ws, player); // binding is preserved automatically
3560
+ * const player = players.get(ws);
3561
+ * ```
3562
+ */
3563
+ var ConnectionStateStore = class {
3564
+ options;
3565
+ constructor(options) {
3566
+ this.options = options;
3567
+ }
3568
+ /** The validated app state for a connection, or `null` if unset / invalid. */
3569
+ get(connection) {
3570
+ return this._readAttachment(connection).app ?? null;
3571
+ }
3572
+ /** Set the app state, preserving the runtime binding already pinned to the connection. */
3573
+ set(connection, app) {
3574
+ const existing = this._readAttachment(connection);
3575
+ this.options.write(connection, {
3576
+ app,
3577
+ binding: existing.binding
3578
+ });
3579
+ }
3580
+ /** Clear the app state but keep the binding (e.g. a spectator that stopped watching). */
3581
+ clearApp(connection) {
3582
+ const existing = this._readAttachment(connection);
3583
+ this.options.write(connection, { binding: existing.binding });
3584
+ }
3585
+ /** Every live connection paired with its (validated) app state — for rebuilding in-memory state after a wake. */
3586
+ entries() {
3587
+ return this.options.getConnections().map((connection) => [connection, this._readAttachment(connection).app ?? null]);
3588
+ }
3589
+ /** @internal Persist a freshly-bound connection's binding, preserving any app state already stored. */
3590
+ _persistBinding(connection, binding) {
3591
+ const existing = this._readAttachment(connection);
3592
+ this.options.write(connection, {
3593
+ app: existing.app,
3594
+ binding
3595
+ });
3596
+ }
3597
+ /** @internal The persisted binding for a connection, if any (used to replay routing after a wake). */
3598
+ _readBinding(connection) {
3599
+ return this._readAttachment(connection).binding;
3600
+ }
3601
+ _readAttachment(connection) {
3602
+ try {
3603
+ const raw = this.options.read(connection);
3604
+ if (typeof raw !== "object" || raw === null) return {};
3605
+ const attachment = raw;
3606
+ const result = {};
3607
+ if (attachment.binding != null) result.binding = attachment.binding;
3608
+ if (attachment.app !== void 0) {
3609
+ const app = this._validateApp(attachment.app);
3610
+ if (app !== void 0) result.app = app;
3611
+ }
3612
+ return result;
3613
+ } catch {
3614
+ return {};
3615
+ }
3616
+ }
3617
+ _validateApp(value) {
3618
+ const schema = this.options.schema;
3619
+ if (schema == null) return value;
3620
+ const result = schema["~standard"].validate(value);
3621
+ if (result instanceof Promise) return void 0;
3622
+ if (result.issues != null) return void 0;
3623
+ return result.value;
3624
+ }
3625
+ };
3626
+ /**
3627
+ * Build a per-connection {@link ConnectionStateStore} bound to an {@link AcceptorHandler}: it registers
3628
+ * itself as the handler's connection-bound persistence callback (so bindings are written without
3629
+ * overwriting app state) and immediately replays every live connection's stored binding via
3630
+ * {@link AcceptorHandler.rehydrateConnection} — so on a transport that resumes after eviction (e.g. a
3631
+ * Durable Object waking from hibernation) both the app identity and the action routing come back from a
3632
+ * single attachment, with no storage reads and no hand-rolled merge.
3633
+ *
3634
+ * Lives outside the handler so the generic {@link AcceptorHandler} stays free of any attachment/
3635
+ * hibernation concern — it exposes only the neutral `setOnConnectionBound` + `rehydrateConnection`
3636
+ * hooks this builder drives.
3637
+ */
3638
+ function createConnectionStateStore(handler, options) {
3639
+ const store = new ConnectionStateStore(options);
3640
+ handler.setOnConnectionBound((connection, binding) => store._persistBinding(connection, binding));
3641
+ for (const connection of options.getConnections()) {
3642
+ const binding = store._readBinding(connection);
3643
+ if (binding != null) handler.rehydrateConnection(connection, binding);
3644
+ }
3645
+ return store;
3646
+ }
3647
+ //#endregion
3541
3648
  //#region src/ActionRuntime/Handler/PeerLink/Acceptor/Hibernation/createHibernatableWsServerAdapter.ts
3542
3649
  /**
3543
3650
  * Wire the hibernation lifecycle for an acceptor handler on a transport whose connections outlive process
@@ -3576,34 +3683,14 @@ const DEFAULT_SERVER_SECURITY_LEVELS = [
3576
3683
  "authenticated",
3577
3684
  "encrypted"
3578
3685
  ];
3579
- /**
3580
- * Serve a secure channel over one or more carriers from a single call — the accept-in dual of
3581
- * `connectChannel`. It builds the crypto identity (a {@link ClientCryptoKeyLink} + a storage-backed TOFU
3582
- * resolver) and the security block (coordinate, dictionary version, accepted levels) *once* from
3583
- * `(runtime, channel)` and fans them across every carrier, so the WebSocket and the secure-HTTP endpoint
3584
- * can never drift apart. It registers your handlers (plus the duplex acceptor it builds) on the runtime,
3585
- * wires hibernation when the duplex carrier declares it, and returns a single {@link IChannelServer} whose
3586
- * `fetch` / `duplex` / `pushToClient` you forward straight to the host:
3587
- * ```ts
3588
- * const server = serveChannel(runtime, channel, {
3589
- * clientEnv, storage,
3590
- * carriers: [wsAcceptorCarrier({ send, upgrade, hibernation }), httpAcceptorCarrier()],
3591
- * handlers: [localHandler],
3592
- * });
3593
- * // fetch(req) => server.fetch(req)
3594
- * // webSocketMessage(conn, m) => server.duplex?.receive(conn, m)
3595
- * // webSocketClose/Error(conn) => server.duplex?.drop(conn)
3596
- * ```
3597
- *
3598
- * `TConn` (the live-connection token a duplex carrier hands back through `send`/`receive`/`drop`) is
3599
- * inferred from the carriers — `WebSocket` for `wsAcceptorCarrier`, the data-channel type for a WebRTC
3600
- * carrier, and so on — so it stays carrier-agnostic.
3601
- */
3602
3686
  function serveChannel(runtime, channel, options) {
3603
3687
  const duplexCarriers = options.carriers.filter((carrier) => !require_wsAcceptorCarrier.isExchangeAcceptorCarrier(carrier));
3604
3688
  const exchangeCarriers = options.carriers.filter(require_wsAcceptorCarrier.isExchangeAcceptorCarrier);
3605
3689
  if (exchangeCarriers.length > 1) throw new Error("serveChannel: at most one exchange carrier is supported");
3606
3690
  const exchangeCarrier = exchangeCarriers[0];
3691
+ const singleDuplex = duplexCarriers.length === 1;
3692
+ if (options.connectionState != null && !singleDuplex) throw new Error("serveChannel: `connectionState` requires exactly one duplex carrier");
3693
+ if (options.channelCases != null && !singleDuplex) throw new Error("serveChannel: `channelCases` requires exactly one duplex carrier");
3607
3694
  const exchangeSecure = exchangeCarrier != null && (exchangeCarrier.secure ?? true);
3608
3695
  const anyDuplexSecure = duplexCarriers.some((carrier) => carrier.secure ?? true);
3609
3696
  const securityLevel = options.securityLevel ?? DEFAULT_SERVER_SECURITY_LEVELS;
@@ -3617,7 +3704,13 @@ function serveChannel(runtime, channel, options) {
3617
3704
  verifyKeyResolver: options.verifyKeyResolver ?? createStorageTofuVerifyKeyResolver(storage)
3618
3705
  };
3619
3706
  }
3707
+ const plainRouter = (handler) => ({
3708
+ receive: (connection, frame) => handler.receive(connection, frame),
3709
+ drop: (connection) => handler.dropConnection(connection)
3710
+ });
3711
+ const asObject = (value) => typeof value === "object" && value != null ? value : {};
3620
3712
  const handlers = [];
3713
+ let connections;
3621
3714
  for (const carrier of duplexCarriers) {
3622
3715
  const handler = (carrier.secure ?? true) && secure != null ? acceptChannel(runtime, channel, {
3623
3716
  clientEnv: options.clientEnv,
@@ -3634,17 +3727,31 @@ function serveChannel(runtime, channel, options) {
3634
3727
  runtime,
3635
3728
  defaultTimeout: options.defaultTimeout
3636
3729
  });
3637
- const router = carrier.hibernation != null ? createHibernatableWsServerAdapter({
3730
+ const attach = carrier.attachmentStore;
3731
+ let router;
3732
+ if (attach == null) router = plainRouter(handler);
3733
+ else if (options.connectionState != null) {
3734
+ connections = createConnectionStateStore(handler, {
3735
+ schema: options.connectionState.schema,
3736
+ getConnections: attach.getConnections,
3737
+ read: attach.read,
3738
+ write: attach.write
3739
+ });
3740
+ router = plainRouter(handler);
3741
+ } else router = createHibernatableWsServerAdapter({
3638
3742
  handler,
3639
- ...carrier.hibernation
3640
- }) : {
3641
- receive: (connection, frame) => handler.receive(connection, frame),
3642
- drop: (connection) => handler.dropConnection(connection)
3643
- };
3743
+ getConnections: attach.getConnections,
3744
+ getAttachment: (connection) => attach.read(connection)?.binding,
3745
+ setAttachment: (connection, binding) => attach.write(connection, {
3746
+ ...asObject(attach.read(connection)),
3747
+ binding
3748
+ })
3749
+ });
3644
3750
  carrier._activate(router);
3645
3751
  handlers.push(handler);
3646
3752
  }
3647
3753
  runtime.addHandlers([...options.handlers ?? [], ...handlers]);
3754
+ if (options.channelCases != null) runtime.addHandlers([acceptChannelConnections(handlers[0], channel, options.channelCases)]);
3648
3755
  const exchangeSecurity = exchangeSecure && secure != null ? {
3649
3756
  link: secure.link,
3650
3757
  verifyKeyResolver: secure.verifyKeyResolver,
@@ -3675,121 +3782,23 @@ function serveChannel(runtime, channel, options) {
3675
3782
  if (owner == null) throw new Error("serveChannel: no duplex carrier holds a connection for the push target");
3676
3783
  return owner.pushToClient(runtime, target, request, pushOptions);
3677
3784
  };
3785
+ const broadcast = (makeRequest, broadcastOptions) => {
3786
+ if (!singleDuplex) throw new Error("serveChannel: broadcast requires exactly one duplex carrier — broadcast over a specific handlers[i] instead");
3787
+ handlers[0].broadcast(makeRequest, {
3788
+ runtime,
3789
+ ...broadcastOptions
3790
+ });
3791
+ };
3678
3792
  return {
3679
3793
  handlers,
3680
3794
  fetch,
3681
3795
  duplex,
3682
- pushToClient
3796
+ pushToClient,
3797
+ broadcast,
3798
+ connections
3683
3799
  };
3684
3800
  }
3685
3801
  //#endregion
3686
- //#region src/ActionRuntime/Handler/PeerLink/Acceptor/Hibernation/ConnectionStateStore.ts
3687
- /**
3688
- * A typed per-connection state store that co-owns the app state and the acceptor handler's routing
3689
- * binding in one attachment, so neither the consumer nor the handler has to hand-merge the two. Create
3690
- * it through {@link createConnectionStateStore} (which also wires binding persistence and replays
3691
- * surviving connections after a wake), then `get`/`set`/`clearApp` the app state directly.
3692
- *
3693
- * The mechanism is carrier-neutral — it only needs read/write/enumerate callbacks for the connection's
3694
- * attachment — but it pays off on transports whose connections outlive process eviction (e.g. a
3695
- * Durable Object's hibernatable WebSockets), which is why it lives beside the hibernation adapter.
3696
- *
3697
- * ```ts
3698
- * const players = createConnectionStateStore(serverHandler, {
3699
- * schema: vs_player,
3700
- * read: (ws) => ws.deserializeAttachment(),
3701
- * write: (ws, v) => ws.serializeAttachment(v),
3702
- * getConnections: () => ctx.getWebSockets(),
3703
- * });
3704
- * players.set(ws, player); // binding is preserved automatically
3705
- * const player = players.get(ws);
3706
- * ```
3707
- */
3708
- var ConnectionStateStore = class {
3709
- options;
3710
- constructor(options) {
3711
- this.options = options;
3712
- }
3713
- /** The validated app state for a connection, or `null` if unset / invalid. */
3714
- get(connection) {
3715
- return this._readAttachment(connection).app ?? null;
3716
- }
3717
- /** Set the app state, preserving the runtime binding already pinned to the connection. */
3718
- set(connection, app) {
3719
- const existing = this._readAttachment(connection);
3720
- this.options.write(connection, {
3721
- app,
3722
- binding: existing.binding
3723
- });
3724
- }
3725
- /** Clear the app state but keep the binding (e.g. a spectator that stopped watching). */
3726
- clearApp(connection) {
3727
- const existing = this._readAttachment(connection);
3728
- this.options.write(connection, { binding: existing.binding });
3729
- }
3730
- /** Every live connection paired with its (validated) app state — for rebuilding in-memory state after a wake. */
3731
- entries() {
3732
- return this.options.getConnections().map((connection) => [connection, this._readAttachment(connection).app ?? null]);
3733
- }
3734
- /** @internal Persist a freshly-bound connection's binding, preserving any app state already stored. */
3735
- _persistBinding(connection, binding) {
3736
- const existing = this._readAttachment(connection);
3737
- this.options.write(connection, {
3738
- app: existing.app,
3739
- binding
3740
- });
3741
- }
3742
- /** @internal The persisted binding for a connection, if any (used to replay routing after a wake). */
3743
- _readBinding(connection) {
3744
- return this._readAttachment(connection).binding;
3745
- }
3746
- _readAttachment(connection) {
3747
- try {
3748
- const raw = this.options.read(connection);
3749
- if (typeof raw !== "object" || raw === null) return {};
3750
- const attachment = raw;
3751
- const result = {};
3752
- if (attachment.binding != null) result.binding = attachment.binding;
3753
- if (attachment.app !== void 0) {
3754
- const app = this._validateApp(attachment.app);
3755
- if (app !== void 0) result.app = app;
3756
- }
3757
- return result;
3758
- } catch {
3759
- return {};
3760
- }
3761
- }
3762
- _validateApp(value) {
3763
- const schema = this.options.schema;
3764
- if (schema == null) return value;
3765
- const result = schema["~standard"].validate(value);
3766
- if (result instanceof Promise) return void 0;
3767
- if (result.issues != null) return void 0;
3768
- return result.value;
3769
- }
3770
- };
3771
- /**
3772
- * Build a per-connection {@link ConnectionStateStore} bound to an {@link AcceptorHandler}: it registers
3773
- * itself as the handler's connection-bound persistence callback (so bindings are written without
3774
- * overwriting app state) and immediately replays every live connection's stored binding via
3775
- * {@link AcceptorHandler.rehydrateConnection} — so on a transport that resumes after eviction (e.g. a
3776
- * Durable Object waking from hibernation) both the app identity and the action routing come back from a
3777
- * single attachment, with no storage reads and no hand-rolled merge.
3778
- *
3779
- * Lives outside the handler so the generic {@link AcceptorHandler} stays free of any attachment/
3780
- * hibernation concern — it exposes only the neutral `setOnConnectionBound` + `rehydrateConnection`
3781
- * hooks this builder drives.
3782
- */
3783
- function createConnectionStateStore(handler, options) {
3784
- const store = new ConnectionStateStore(options);
3785
- handler.setOnConnectionBound((connection, binding) => store._persistBinding(connection, binding));
3786
- for (const connection of options.getConnections()) {
3787
- const binding = store._readBinding(connection);
3788
- if (binding != null) handler.rehydrateConnection(connection, binding);
3789
- }
3790
- return store;
3791
- }
3792
- //#endregion
3793
3802
  //#region src/ActionRuntime/Transport/Carrier/duplex/inMemory/createInMemoryChannel.ts
3794
3803
  /**
3795
3804
  * Two cross-wired in-process byte channels — a loopback carrier with no socket. The client end is a