@nice-code/action 0.24.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 (36) hide show
  1. package/README.md +54 -6
  2. package/build/{AcceptorHandler-11-QMdx2.d.mts → AcceptorHandler-BizUtq4u.d.mts} +118 -15
  3. package/build/{AcceptorHandler-CxD0c1BE.d.cts → AcceptorHandler-CxPfZtIl.d.cts} +118 -15
  4. package/build/{ActionDevtoolsCore-37JP4bOG.d.cts → ActionDevtoolsCore-D9KBBI2V.d.cts} +2 -2
  5. package/build/{ActionDevtoolsCore-Cgq-go1R.d.mts → ActionDevtoolsCore-xZjAtB4H.d.mts} +2 -2
  6. package/build/advanced/index.cjs +1 -1
  7. package/build/advanced/index.d.cts +1 -96
  8. package/build/advanced/index.d.mts +1 -96
  9. package/build/advanced/index.mjs +1 -1
  10. package/build/{createHibernatableWsServerAdapter-C07RfUTH.mjs → createHibernatableWsServerAdapter-BkjESd01.mjs} +11 -9
  11. package/build/createHibernatableWsServerAdapter-BkjESd01.mjs.map +1 -0
  12. package/build/{createHibernatableWsServerAdapter-BNi4k9j3.cjs → createHibernatableWsServerAdapter-FSDWrxoF.cjs} +11 -9
  13. package/build/createHibernatableWsServerAdapter-FSDWrxoF.cjs.map +1 -0
  14. package/build/devtools/browser/index.d.cts +1 -1
  15. package/build/devtools/browser/index.d.mts +1 -1
  16. package/build/devtools/server/index.d.cts +1 -1
  17. package/build/devtools/server/index.d.mts +1 -1
  18. package/build/{httpAcceptorCarrier-C3S_bDkL.cjs → httpAcceptorCarrier-BQYaXI9j.cjs} +2 -2
  19. package/build/{httpAcceptorCarrier-C3S_bDkL.cjs.map → httpAcceptorCarrier-BQYaXI9j.cjs.map} +1 -1
  20. package/build/{httpAcceptorCarrier-DPBEuewS.mjs → httpAcceptorCarrier-DWqsCz3h.mjs} +2 -2
  21. package/build/{httpAcceptorCarrier-DPBEuewS.mjs.map → httpAcceptorCarrier-DWqsCz3h.mjs.map} +1 -1
  22. package/build/index.cjs +6 -2
  23. package/build/index.d.cts +2 -2
  24. package/build/index.d.mts +2 -2
  25. package/build/index.mjs +3 -3
  26. package/build/platform/cloudflare/index.cjs +45 -1
  27. package/build/platform/cloudflare/index.cjs.map +1 -1
  28. package/build/platform/cloudflare/index.d.cts +40 -2
  29. package/build/platform/cloudflare/index.d.mts +40 -2
  30. package/build/platform/cloudflare/index.mjs +45 -2
  31. package/build/platform/cloudflare/index.mjs.map +1 -1
  32. package/build/react-query/index.d.cts +1 -1
  33. package/build/react-query/index.d.mts +1 -1
  34. package/package.json +4 -4
  35. package/build/createHibernatableWsServerAdapter-BNi4k9j3.cjs.map +0 -1
  36. package/build/createHibernatableWsServerAdapter-C07RfUTH.mjs.map +0 -1
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_httpAcceptorCarrier = require("../../httpAcceptorCarrier-C3S_bDkL.cjs");
2
+ const require_httpAcceptorCarrier = require("../../httpAcceptorCarrier-BQYaXI9j.cjs");
3
3
  let _nice_code_util = require("@nice-code/util");
4
4
  //#region src/platform/cloudflare/index.ts
5
5
  /**
@@ -79,10 +79,54 @@ function serveDurableObject(ctx, channel, options) {
79
79
  secure
80
80
  }), serveOptions);
81
81
  }
82
+ /** Permissive defaults, matching `serveDurableObject`'s HTTP fallback. */
83
+ const DEFAULT_FORWARD_CORS = {
84
+ "Access-Control-Allow-Origin": "*",
85
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
86
+ "Access-Control-Allow-Headers": "Content-Type",
87
+ "Access-Control-Max-Age": "86400"
88
+ };
89
+ /**
90
+ * Fan a Worker's incoming action request out to a *per-id* Durable Object that serves the exchange itself.
91
+ *
92
+ * A secure exchange body is opaque to the Worker (handshake / encrypted frames), so the DO it belongs to
93
+ * has to be chosen from the **URL**, not the body. This picks the stub with your `pickStub` and forwards
94
+ * the request to its `fetch` (where `serveDurableObject` serves the exchange), while answering the CORS
95
+ * `OPTIONS` preflight *at the edge* so a per-id DO is never woken (or billed) just to reply to a preflight:
96
+ * ```ts
97
+ * // wrangler: a Durable Object namespace `BRIDGE`, each instance one bridge serving `bridgeChannel`.
98
+ * export default {
99
+ * fetch(request: Request, env: Env) {
100
+ * return forwardExchangeToDurableObject(request, (req, url) => {
101
+ * const bridgeId = url.pathname.split("/")[2]; // e.g. /bridge/:id/action
102
+ * return env.BRIDGE.get(env.BRIDGE.idFromName(bridgeId));
103
+ * });
104
+ * },
105
+ * };
106
+ *
107
+ * // …and in the Durable Object, serve the secure exchange (+ a WS upgrade) as usual:
108
+ * // fetch(request) { return this.server.fetch(request); }
109
+ * // where this.server = serveDurableObject(this.ctx, bridgeChannel, { runtime, httpFallback: "secure" });
110
+ * ```
111
+ * The matching connector points its `httpCarrier` at `/bridge/:id/action`. `pickStub` may be async (e.g.
112
+ * to look an id up first); it receives the parsed {@link URL} alongside the request.
113
+ */
114
+ function forwardExchangeToDurableObject(request, pickStub, options = {}) {
115
+ if (request.method === "OPTIONS") {
116
+ const headers = options.cors === false ? void 0 : options.cors ?? DEFAULT_FORWARD_CORS;
117
+ return Promise.resolve(new Response(null, {
118
+ status: 204,
119
+ headers
120
+ }));
121
+ }
122
+ const url = new URL(request.url);
123
+ return Promise.resolve(pickStub(request, url)).then((stub) => stub.fetch(request));
124
+ }
82
125
  //#endregion
83
126
  exports.cloudflareDurableObjectHost = cloudflareDurableObjectHost;
84
127
  exports.durableObjectStorage = durableObjectStorage;
85
128
  exports.durableObjectWsCarrier = durableObjectWsCarrier;
129
+ exports.forwardExchangeToDurableObject = forwardExchangeToDurableObject;
86
130
  exports.serveDurableObject = serveDurableObject;
87
131
 
88
132
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["wsAcceptorCarrier","httpAcceptorCarrier","serveHost"],"sources":["../../../src/platform/cloudflare/index.ts"],"sourcesContent":["import {\r\n createDurableObjectStorageAdapter,\r\n type StorageAdapter,\r\n type TCreateDurableObjectStorageOptions,\r\n} from \"@nice-code/util\";\r\nimport type { ActionDomain } from \"../../ActionDefinition/Domain/ActionDomain\";\r\nimport type { ActionRuntime } from \"../../ActionRuntime/ActionRuntime\";\r\nimport type { IActionChannel } from \"../../ActionRuntime/Channel/ActionChannel\";\r\nimport type {\r\n IChannelServer,\r\n IServeConnectionStateOptions,\r\n} from \"../../ActionRuntime/Channel/serveChannel\";\r\nimport {\r\n type IChannelHostAdapter,\r\n serveHost,\r\n type TServeHostOptions,\r\n} from \"../../ActionRuntime/Channel/serveHost\";\r\nimport type { ConnectionStateStore } from \"../../ActionRuntime/Handler/PeerLink/Acceptor/Hibernation/ConnectionStateStore\";\r\nimport type {\r\n IDuplexAcceptorCarrier,\r\n TAcceptorCarrier,\r\n} from \"../../ActionRuntime/Transport/Carrier/AcceptorCarrier.types\";\r\nimport { wsAcceptorCarrier } from \"../../ActionRuntime/Transport/Carrier/duplex/ws/wsAcceptorCarrier\";\r\nimport { httpAcceptorCarrier } from \"../../ActionRuntime/Transport/Carrier/exchange/http/httpAcceptorCarrier\";\r\n\r\n/**\r\n * Cloudflare-specific helpers for `@nice-code/action`, imported from `@nice-code/action/platform/cloudflare`.\r\n * They collapse the Durable Object boilerplate (the `WebSocketPair` upgrade, hibernation attachment wiring,\r\n * and DO-storage adapter) into one-liners you hand to `serveChannel`. The core library stays\r\n * platform-agnostic — nothing here is reachable from the main entry.\r\n *\r\n * The Workers runtime surface this module needs is declared *structurally* (and the two globals it\r\n * constructs are declared module-locally below) rather than pulled from `@cloudflare/workers-types`, so the\r\n * library's own DOM-lib build never clashes with that package's global `Response`/`WebSocket` redefinitions.\r\n * A real `DurableObjectState` and its hibernatable `WebSocket`s satisfy these shapes.\r\n */\r\n\r\ntype TDurableObjectStorage = TCreateDurableObjectStorageOptions[\"durableObjectStorage\"];\r\n\r\n/** The slice of a Durable Object's hibernatable WebSocket these helpers touch. */\r\nexport interface IDurableObjectWebSocket {\r\n send(data: string | ArrayBuffer | Uint8Array): void;\r\n serializeAttachment(value: unknown): void;\r\n // Mirrors the Workers runtime's `any` return so a typed connection binding round-trips without a cast.\r\n deserializeAttachment(): any;\r\n}\r\n\r\n/** The slice of a Durable Object's `state` (its `ctx`) these helpers touch. */\r\nexport interface IDurableObjectContext {\r\n storage: TDurableObjectStorage;\r\n getWebSockets(): IDurableObjectWebSocket[];\r\n acceptWebSocket(ws: IDurableObjectWebSocket): void;\r\n /** Register a runtime-answered keepalive so pings never wake the DO. */\r\n setWebSocketAutoResponse(pair: IWebSocketRequestResponsePair): void;\r\n}\r\n\r\n/** An `IChannelServer` whose connections are a Durable Object's hibernatable WebSockets — the type to\r\n * store the result of `serveChannel(...)` in when serving over {@link durableObjectWsCarrier}. `TApp` is the\r\n * per-connection app-state type when `connectionState` is used (defaults to `unknown` otherwise). */\r\nexport type TDurableObjectChannelServer<TApp = unknown> = IChannelServer<\r\n IDurableObjectWebSocket,\r\n TApp\r\n>;\r\n\r\n// Workers runtime globals, declared module-locally (the runtime provides them at deploy time). Declaring\r\n// them here keeps them out of the package's global scope, so the DOM lib's `Response`/`WebSocket` stand\r\n// elsewhere. The `Response` constructor type still returns the DOM `Response` — only its init gains the\r\n// Workers-only `webSocket` field.\r\ndeclare const WebSocketPair: {\r\n new (): { 0: IDurableObjectWebSocket; 1: IDurableObjectWebSocket };\r\n};\r\ndeclare const Response: {\r\n new (\r\n body: BodyInit | null,\r\n init?: {\r\n status?: number;\r\n statusText?: string;\r\n headers?: HeadersInit;\r\n webSocket?: IDurableObjectWebSocket;\r\n },\r\n ): Response;\r\n};\r\n/** The keepalive pair shape — just the fields the Workers runtime's `WebSocketRequestResponsePair` exposes. */\r\ninterface IWebSocketRequestResponsePair {\r\n readonly request: string;\r\n readonly response: string;\r\n}\r\ndeclare const WebSocketRequestResponsePair: {\r\n new (request: string, response: string): IWebSocketRequestResponsePair;\r\n};\r\n\r\nexport interface IDurableObjectWsCarrierOptions {\r\n /**\r\n * Whether each socket runs the secure handshake (default `true`). Pass `false` for a plain WS endpoint —\r\n * then `serveChannel` needs no `storage` for this carrier.\r\n */\r\n secure?: boolean;\r\n}\r\n\r\n/**\r\n * Build a hibernatable-WebSocket acceptor carrier for a Durable Object in one call — the `send`, the\r\n * `WebSocketPair` upgrade, and the hibernation attachment hooks all derived from the DO's `ctx`. Hand it\r\n * straight to `serveChannel`'s `carriers`, and forward the DO's socket events to the returned handle:\r\n * ```ts\r\n * const ws = durableObjectWsCarrier(this.ctx);\r\n * const server = serveChannel(runtime, channel, {\r\n * clientEnv,\r\n * storage: durableObjectStorage(this.ctx, { keyPrefix: \"ws:\" }),\r\n * handlers: [localHandler],\r\n * carriers: [ws, httpAcceptorCarrier()],\r\n * });\r\n * // webSocketMessage(c, m) => ws.receive(c, m);\r\n * // webSocketClose/Error(c) => ws.drop(c);\r\n * ```\r\n *\r\n * The carrier exposes the DO's socket attachment to `serveChannel`, which persists the routing binding\r\n * there and replays it on wake — and, when `connectionState` is requested, co-stores per-connection app\r\n * state in the *same* attachment, so both survive eviction without the DO wiring any of it by hand.\r\n */\r\nexport function durableObjectWsCarrier(\r\n ctx: IDurableObjectContext,\r\n options: IDurableObjectWsCarrierOptions = {},\r\n): IDuplexAcceptorCarrier<IDurableObjectWebSocket> {\r\n return wsAcceptorCarrier<IDurableObjectWebSocket>({\r\n secure: options.secure,\r\n send: (ws, frame) => ws.send(frame),\r\n upgrade: () => {\r\n const pair = new WebSocketPair();\r\n const client = pair[0];\r\n const server = pair[1];\r\n // Hibernatable WebSocket — the DO can sleep between messages.\r\n ctx.acceptWebSocket(server);\r\n return new Response(null, { status: 101, webSocket: client });\r\n },\r\n // Raw access to each socket's attachment; `serveChannel` owns the composite (binding + app) layout.\r\n attachmentStore: {\r\n getConnections: () => ctx.getWebSockets(),\r\n read: (ws) => ws.deserializeAttachment(),\r\n write: (ws, value) => ws.serializeAttachment(value),\r\n },\r\n });\r\n}\r\n\r\nexport interface IDurableObjectStorageOptions {\r\n /** Namespace prefix for every key (e.g. `\"demo-ws:\"`), so several adapters can share one DO storage. */\r\n keyPrefix?: string;\r\n}\r\n\r\n/**\r\n * Wrap a Durable Object's storage as a {@link StorageAdapter} for `serveChannel`'s `storage` — sugar over\r\n * `createDurableObjectStorageAdapter({ durableObjectStorage: ctx.storage, … })` so a DO needs one import.\r\n */\r\nexport function durableObjectStorage(\r\n ctx: IDurableObjectContext,\r\n options: IDurableObjectStorageOptions = {},\r\n): StorageAdapter {\r\n return createDurableObjectStorageAdapter({\r\n durableObjectStorage: ctx.storage,\r\n keyPrefix: options.keyPrefix,\r\n });\r\n}\r\n\r\nexport interface ICloudflareDurableObjectHostOptions {\r\n /** Namespace prefix for the DO-storage crypto identity keys (e.g. `\"lobby-ws:\"`). */\r\n keyPrefix?: string;\r\n /**\r\n * The HTTP fallback that sits beside the WebSocket: `\"plain\"` (default — POSTs the raw action wire, the\r\n * usual fallback for a public client), `\"secure\"` (the full handshake-protected exchange, sharing the WS\r\n * identity), or `false` (WebSocket only).\r\n */\r\n httpFallback?: \"plain\" | \"secure\" | false;\r\n /** Whether the WebSocket runs the secure handshake (default `true`). `false` = a plain WS endpoint. */\r\n secure?: boolean;\r\n}\r\n\r\n/**\r\n * Build the {@link IChannelHostAdapter} for a Durable Object in one call — the entire repeated transport\r\n * stack a DO would otherwise assemble by hand: a hibernatable secure WebSocket carrier, an HTTP fallback,\r\n * the DO-storage-backed crypto identity, and a runtime-answered `ping`/`pong` keepalive (so pings never\r\n * wake the DO). Hand it to {@link serveHost}, or use {@link serveDurableObject} which composes both.\r\n */\r\nexport function cloudflareDurableObjectHost(\r\n ctx: IDurableObjectContext,\r\n options: ICloudflareDurableObjectHostOptions = {},\r\n): IChannelHostAdapter<IDurableObjectWebSocket> {\r\n const httpFallback = options.httpFallback ?? \"plain\";\r\n const carriers: TAcceptorCarrier<IDurableObjectWebSocket>[] = [\r\n durableObjectWsCarrier(ctx, { secure: options.secure }),\r\n ];\r\n if (httpFallback !== false) {\r\n carriers.push(httpAcceptorCarrier({ secure: httpFallback === \"secure\" }));\r\n }\r\n\r\n return {\r\n carriers,\r\n storage: durableObjectStorage(ctx, { keyPrefix: options.keyPrefix }),\r\n onServed: () => {\r\n // Keepalive answered by the runtime itself — pings never wake the DO.\r\n ctx.setWebSocketAutoResponse(new WebSocketRequestResponsePair(\"ping\", \"pong\"));\r\n },\r\n };\r\n}\r\n\r\n/** {@link serveDurableObject}'s options: the `serveHost` surface + the DO runtime + the host knobs. */\r\nexport type TServeDurableObjectOptions<\r\n TO_ACCEPTOR extends readonly ActionDomain<any>[],\r\n TApp = unknown,\r\n> = TServeHostOptions<TO_ACCEPTOR, IDurableObjectWebSocket, TApp> &\r\n ICloudflareDurableObjectHostOptions & {\r\n /** This DO's runtime (e.g. `new ActionRuntime(coord.withPersistentId(ctx.id.toString()))`). */\r\n runtime: ActionRuntime;\r\n };\r\n\r\n/**\r\n * Serve a secure channel from a Durable Object in one call — the whole transport stack\r\n * ({@link cloudflareDurableObjectHost}: hibernatable secure WebSocket + HTTP fallback + DO-storage crypto\r\n * identity + keepalive) folded in, leaving the DO to forward its four socket lifecycle methods to the\r\n * returned server's `fetch` / `receive` / `drop`:\r\n * ```ts\r\n * const server = serveDurableObject(this.ctx, lobbyChannel, {\r\n * runtime, clientEnv,\r\n * connectionState: { schema: vs_player }, // optional, survives hibernation\r\n * channelCases: { join: (action, conn) => { conn.setState(action.input); conn.broadcast(…); } },\r\n * });\r\n * // fetch(req) => server.fetch(req)\r\n * // webSocketMessage(ws, m) => server.receive(ws, m)\r\n * // webSocketClose/Error(ws)=> server.drop(ws)\r\n * ```\r\n * Passing `connectionState` narrows the return so `server.connections` is non-optional.\r\n */\r\nexport function serveDurableObject<\r\n TO_ACCEPTOR extends readonly ActionDomain<any>[],\r\n TO_CONNECTOR extends readonly ActionDomain<any>[],\r\n TApp,\r\n>(\r\n ctx: IDurableObjectContext,\r\n channel: IActionChannel<TO_ACCEPTOR, TO_CONNECTOR>,\r\n options: TServeDurableObjectOptions<TO_ACCEPTOR, TApp> & {\r\n connectionState: IServeConnectionStateOptions<TApp>;\r\n },\r\n): TDurableObjectChannelServer<TApp> & {\r\n connections: ConnectionStateStore<IDurableObjectWebSocket, TApp>;\r\n};\r\nexport function serveDurableObject<\r\n TO_ACCEPTOR extends readonly ActionDomain<any>[] = readonly ActionDomain<any>[],\r\n TO_CONNECTOR extends readonly ActionDomain<any>[] = readonly ActionDomain<any>[],\r\n TApp = unknown,\r\n>(\r\n ctx: IDurableObjectContext,\r\n channel: IActionChannel<TO_ACCEPTOR, TO_CONNECTOR>,\r\n options: TServeDurableObjectOptions<TO_ACCEPTOR, TApp>,\r\n): TDurableObjectChannelServer<TApp>;\r\nexport function serveDurableObject(\r\n ctx: IDurableObjectContext,\r\n channel: IActionChannel<readonly ActionDomain<any>[], readonly ActionDomain<any>[]>,\r\n options: TServeDurableObjectOptions<readonly ActionDomain<any>[], any>,\r\n): TDurableObjectChannelServer<any> {\r\n const { runtime, keyPrefix, httpFallback, secure, ...serveOptions } = options;\r\n const host = cloudflareDurableObjectHost(ctx, { keyPrefix, httpFallback, secure });\r\n return serveHost(runtime, channel, host, serveOptions);\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAuHA,SAAgB,uBACd,KACA,UAA0C,CAAC,GACM;CACjD,OAAOA,4BAAAA,kBAA2C;EAChD,QAAQ,QAAQ;EAChB,OAAO,IAAI,UAAU,GAAG,KAAK,KAAK;EAClC,eAAe;GACb,MAAM,OAAO,IAAI,cAAc;GAC/B,MAAM,SAAS,KAAK;GACpB,MAAM,SAAS,KAAK;GAEpB,IAAI,gBAAgB,MAAM;GAC1B,OAAO,IAAI,SAAS,MAAM;IAAE,QAAQ;IAAK,WAAW;GAAO,CAAC;EAC9D;EAEA,iBAAiB;GACf,sBAAsB,IAAI,cAAc;GACxC,OAAO,OAAO,GAAG,sBAAsB;GACvC,QAAQ,IAAI,UAAU,GAAG,oBAAoB,KAAK;EACpD;CACF,CAAC;AACH;;;;;AAWA,SAAgB,qBACd,KACA,UAAwC,CAAC,GACzB;CAChB,QAAA,GAAA,gBAAA,kCAAA,CAAyC;EACvC,sBAAsB,IAAI;EAC1B,WAAW,QAAQ;CACrB,CAAC;AACH;;;;;;;AAqBA,SAAgB,4BACd,KACA,UAA+C,CAAC,GACF;CAC9C,MAAM,eAAe,QAAQ,gBAAgB;CAC7C,MAAM,WAAwD,CAC5D,uBAAuB,KAAK,EAAE,QAAQ,QAAQ,OAAO,CAAC,CACxD;CACA,IAAI,iBAAiB,OACnB,SAAS,KAAKC,4BAAAA,oBAAoB,EAAE,QAAQ,iBAAiB,SAAS,CAAC,CAAC;CAG1E,OAAO;EACL;EACA,SAAS,qBAAqB,KAAK,EAAE,WAAW,QAAQ,UAAU,CAAC;EACnE,gBAAgB;GAEd,IAAI,yBAAyB,IAAI,6BAA6B,QAAQ,MAAM,CAAC;EAC/E;CACF;AACF;AAmDA,SAAgB,mBACd,KACA,SACA,SACkC;CAClC,MAAM,EAAE,SAAS,WAAW,cAAc,QAAQ,GAAG,iBAAiB;CAEtE,OAAOC,4BAAAA,UAAU,SAAS,SADb,4BAA4B,KAAK;EAAE;EAAW;EAAc;CAAO,CAC1C,GAAG,YAAY;AACvD"}
1
+ {"version":3,"file":"index.cjs","names":["wsAcceptorCarrier","httpAcceptorCarrier","serveHost"],"sources":["../../../src/platform/cloudflare/index.ts"],"sourcesContent":["import {\r\n createDurableObjectStorageAdapter,\r\n type StorageAdapter,\r\n type TCreateDurableObjectStorageOptions,\r\n} from \"@nice-code/util\";\r\nimport type { ActionDomain } from \"../../ActionDefinition/Domain/ActionDomain\";\r\nimport type { ActionRuntime } from \"../../ActionRuntime/ActionRuntime\";\r\nimport type { IActionChannel } from \"../../ActionRuntime/Channel/ActionChannel\";\r\nimport type {\r\n IChannelServer,\r\n IServeConnectionStateOptions,\r\n} from \"../../ActionRuntime/Channel/serveChannel\";\r\nimport {\r\n type IChannelHostAdapter,\r\n serveHost,\r\n type TServeHostOptions,\r\n} from \"../../ActionRuntime/Channel/serveHost\";\r\nimport type { ConnectionStateStore } from \"../../ActionRuntime/Handler/PeerLink/Acceptor/Hibernation/ConnectionStateStore\";\r\nimport type {\r\n IDuplexAcceptorCarrier,\r\n TAcceptorCarrier,\r\n} from \"../../ActionRuntime/Transport/Carrier/AcceptorCarrier.types\";\r\nimport { wsAcceptorCarrier } from \"../../ActionRuntime/Transport/Carrier/duplex/ws/wsAcceptorCarrier\";\r\nimport { httpAcceptorCarrier } from \"../../ActionRuntime/Transport/Carrier/exchange/http/httpAcceptorCarrier\";\r\n\r\n/**\r\n * Cloudflare-specific helpers for `@nice-code/action`, imported from `@nice-code/action/platform/cloudflare`.\r\n * They collapse the Durable Object boilerplate (the `WebSocketPair` upgrade, hibernation attachment wiring,\r\n * and DO-storage adapter) into one-liners you hand to `serveChannel`. The core library stays\r\n * platform-agnostic — nothing here is reachable from the main entry.\r\n *\r\n * The Workers runtime surface this module needs is declared *structurally* (and the two globals it\r\n * constructs are declared module-locally below) rather than pulled from `@cloudflare/workers-types`, so the\r\n * library's own DOM-lib build never clashes with that package's global `Response`/`WebSocket` redefinitions.\r\n * A real `DurableObjectState` and its hibernatable `WebSocket`s satisfy these shapes.\r\n */\r\n\r\ntype TDurableObjectStorage = TCreateDurableObjectStorageOptions[\"durableObjectStorage\"];\r\n\r\n/** The slice of a Durable Object's hibernatable WebSocket these helpers touch. */\r\nexport interface IDurableObjectWebSocket {\r\n send(data: string | ArrayBuffer | Uint8Array): void;\r\n serializeAttachment(value: unknown): void;\r\n // Mirrors the Workers runtime's `any` return so a typed connection binding round-trips without a cast.\r\n deserializeAttachment(): any;\r\n}\r\n\r\n/** The slice of a Durable Object's `state` (its `ctx`) these helpers touch. */\r\nexport interface IDurableObjectContext {\r\n storage: TDurableObjectStorage;\r\n getWebSockets(): IDurableObjectWebSocket[];\r\n acceptWebSocket(ws: IDurableObjectWebSocket): void;\r\n /** Register a runtime-answered keepalive so pings never wake the DO. */\r\n setWebSocketAutoResponse(pair: IWebSocketRequestResponsePair): void;\r\n}\r\n\r\n/** An `IChannelServer` whose connections are a Durable Object's hibernatable WebSockets — the type to\r\n * store the result of `serveChannel(...)` in when serving over {@link durableObjectWsCarrier}. `TApp` is the\r\n * per-connection app-state type when `connectionState` is used (defaults to `unknown` otherwise). */\r\nexport type TDurableObjectChannelServer<TApp = unknown> = IChannelServer<\r\n IDurableObjectWebSocket,\r\n TApp\r\n>;\r\n\r\n// Workers runtime globals, declared module-locally (the runtime provides them at deploy time). Declaring\r\n// them here keeps them out of the package's global scope, so the DOM lib's `Response`/`WebSocket` stand\r\n// elsewhere. The `Response` constructor type still returns the DOM `Response` — only its init gains the\r\n// Workers-only `webSocket` field.\r\ndeclare const WebSocketPair: {\r\n new (): { 0: IDurableObjectWebSocket; 1: IDurableObjectWebSocket };\r\n};\r\ndeclare const Response: {\r\n new (\r\n body: BodyInit | null,\r\n init?: {\r\n status?: number;\r\n statusText?: string;\r\n headers?: HeadersInit;\r\n webSocket?: IDurableObjectWebSocket;\r\n },\r\n ): Response;\r\n};\r\n/** The keepalive pair shape — just the fields the Workers runtime's `WebSocketRequestResponsePair` exposes. */\r\ninterface IWebSocketRequestResponsePair {\r\n readonly request: string;\r\n readonly response: string;\r\n}\r\ndeclare const WebSocketRequestResponsePair: {\r\n new (request: string, response: string): IWebSocketRequestResponsePair;\r\n};\r\n\r\nexport interface IDurableObjectWsCarrierOptions {\r\n /**\r\n * Whether each socket runs the secure handshake (default `true`). Pass `false` for a plain WS endpoint —\r\n * then `serveChannel` needs no `storage` for this carrier.\r\n */\r\n secure?: boolean;\r\n}\r\n\r\n/**\r\n * Build a hibernatable-WebSocket acceptor carrier for a Durable Object in one call — the `send`, the\r\n * `WebSocketPair` upgrade, and the hibernation attachment hooks all derived from the DO's `ctx`. Hand it\r\n * straight to `serveChannel`'s `carriers`, and forward the DO's socket events to the returned handle:\r\n * ```ts\r\n * const ws = durableObjectWsCarrier(this.ctx);\r\n * const server = serveChannel(runtime, channel, {\r\n * clientEnv,\r\n * storage: durableObjectStorage(this.ctx, { keyPrefix: \"ws:\" }),\r\n * handlers: [localHandler],\r\n * carriers: [ws, httpAcceptorCarrier()],\r\n * });\r\n * // webSocketMessage(c, m) => ws.receive(c, m);\r\n * // webSocketClose/Error(c) => ws.drop(c);\r\n * ```\r\n *\r\n * The carrier exposes the DO's socket attachment to `serveChannel`, which persists the routing binding\r\n * there and replays it on wake — and, when `connectionState` is requested, co-stores per-connection app\r\n * state in the *same* attachment, so both survive eviction without the DO wiring any of it by hand.\r\n */\r\nexport function durableObjectWsCarrier(\r\n ctx: IDurableObjectContext,\r\n options: IDurableObjectWsCarrierOptions = {},\r\n): IDuplexAcceptorCarrier<IDurableObjectWebSocket> {\r\n return wsAcceptorCarrier<IDurableObjectWebSocket>({\r\n secure: options.secure,\r\n send: (ws, frame) => ws.send(frame),\r\n upgrade: () => {\r\n const pair = new WebSocketPair();\r\n const client = pair[0];\r\n const server = pair[1];\r\n // Hibernatable WebSocket — the DO can sleep between messages.\r\n ctx.acceptWebSocket(server);\r\n return new Response(null, { status: 101, webSocket: client });\r\n },\r\n // Raw access to each socket's attachment; `serveChannel` owns the composite (binding + app) layout.\r\n attachmentStore: {\r\n getConnections: () => ctx.getWebSockets(),\r\n read: (ws) => ws.deserializeAttachment(),\r\n write: (ws, value) => ws.serializeAttachment(value),\r\n },\r\n });\r\n}\r\n\r\nexport interface IDurableObjectStorageOptions {\r\n /** Namespace prefix for every key (e.g. `\"demo-ws:\"`), so several adapters can share one DO storage. */\r\n keyPrefix?: string;\r\n}\r\n\r\n/**\r\n * Wrap a Durable Object's storage as a {@link StorageAdapter} for `serveChannel`'s `storage` — sugar over\r\n * `createDurableObjectStorageAdapter({ durableObjectStorage: ctx.storage, … })` so a DO needs one import.\r\n */\r\nexport function durableObjectStorage(\r\n ctx: IDurableObjectContext,\r\n options: IDurableObjectStorageOptions = {},\r\n): StorageAdapter {\r\n return createDurableObjectStorageAdapter({\r\n durableObjectStorage: ctx.storage,\r\n keyPrefix: options.keyPrefix,\r\n });\r\n}\r\n\r\nexport interface ICloudflareDurableObjectHostOptions {\r\n /** Namespace prefix for the DO-storage crypto identity keys (e.g. `\"lobby-ws:\"`). */\r\n keyPrefix?: string;\r\n /**\r\n * The HTTP fallback that sits beside the WebSocket: `\"plain\"` (default — POSTs the raw action wire, the\r\n * usual fallback for a public client), `\"secure\"` (the full handshake-protected exchange, sharing the WS\r\n * identity), or `false` (WebSocket only).\r\n */\r\n httpFallback?: \"plain\" | \"secure\" | false;\r\n /** Whether the WebSocket runs the secure handshake (default `true`). `false` = a plain WS endpoint. */\r\n secure?: boolean;\r\n}\r\n\r\n/**\r\n * Build the {@link IChannelHostAdapter} for a Durable Object in one call — the entire repeated transport\r\n * stack a DO would otherwise assemble by hand: a hibernatable secure WebSocket carrier, an HTTP fallback,\r\n * the DO-storage-backed crypto identity, and a runtime-answered `ping`/`pong` keepalive (so pings never\r\n * wake the DO). Hand it to {@link serveHost}, or use {@link serveDurableObject} which composes both.\r\n */\r\nexport function cloudflareDurableObjectHost(\r\n ctx: IDurableObjectContext,\r\n options: ICloudflareDurableObjectHostOptions = {},\r\n): IChannelHostAdapter<IDurableObjectWebSocket> {\r\n const httpFallback = options.httpFallback ?? \"plain\";\r\n const carriers: TAcceptorCarrier<IDurableObjectWebSocket>[] = [\r\n durableObjectWsCarrier(ctx, { secure: options.secure }),\r\n ];\r\n if (httpFallback !== false) {\r\n carriers.push(httpAcceptorCarrier({ secure: httpFallback === \"secure\" }));\r\n }\r\n\r\n return {\r\n carriers,\r\n storage: durableObjectStorage(ctx, { keyPrefix: options.keyPrefix }),\r\n onServed: () => {\r\n // Keepalive answered by the runtime itself — pings never wake the DO.\r\n ctx.setWebSocketAutoResponse(new WebSocketRequestResponsePair(\"ping\", \"pong\"));\r\n },\r\n };\r\n}\r\n\r\n/** {@link serveDurableObject}'s options: the `serveHost` surface + the DO runtime + the host knobs. */\r\nexport type TServeDurableObjectOptions<\r\n TO_ACCEPTOR extends readonly ActionDomain<any>[],\r\n TApp = unknown,\r\n> = TServeHostOptions<TO_ACCEPTOR, IDurableObjectWebSocket, TApp> &\r\n ICloudflareDurableObjectHostOptions & {\r\n /** This DO's runtime (e.g. `new ActionRuntime(coord.withPersistentId(ctx.id.toString()))`). */\r\n runtime: ActionRuntime;\r\n };\r\n\r\n/**\r\n * Serve a secure channel from a Durable Object in one call — the whole transport stack\r\n * ({@link cloudflareDurableObjectHost}: hibernatable secure WebSocket + HTTP fallback + DO-storage crypto\r\n * identity + keepalive) folded in, leaving the DO to forward its four socket lifecycle methods to the\r\n * returned server's `fetch` / `receive` / `drop`:\r\n * ```ts\r\n * const server = serveDurableObject(this.ctx, lobbyChannel, {\r\n * runtime, clientEnv,\r\n * connectionState: { schema: vs_player }, // optional, survives hibernation\r\n * channelCases: { join: (action, conn) => { conn.setState(action.input); conn.broadcast(…); } },\r\n * });\r\n * // fetch(req) => server.fetch(req)\r\n * // webSocketMessage(ws, m) => server.receive(ws, m)\r\n * // webSocketClose/Error(ws)=> server.drop(ws)\r\n * ```\r\n * Passing `connectionState` narrows the return so `server.connections` is non-optional.\r\n */\r\nexport function serveDurableObject<\r\n TO_ACCEPTOR extends readonly ActionDomain<any>[],\r\n TO_CONNECTOR extends readonly ActionDomain<any>[],\r\n TApp,\r\n>(\r\n ctx: IDurableObjectContext,\r\n channel: IActionChannel<TO_ACCEPTOR, TO_CONNECTOR>,\r\n options: TServeDurableObjectOptions<TO_ACCEPTOR, TApp> & {\r\n connectionState: IServeConnectionStateOptions<TApp>;\r\n },\r\n): TDurableObjectChannelServer<TApp> & {\r\n connections: ConnectionStateStore<IDurableObjectWebSocket, TApp>;\r\n};\r\nexport function serveDurableObject<\r\n TO_ACCEPTOR extends readonly ActionDomain<any>[] = readonly ActionDomain<any>[],\r\n TO_CONNECTOR extends readonly ActionDomain<any>[] = readonly ActionDomain<any>[],\r\n TApp = unknown,\r\n>(\r\n ctx: IDurableObjectContext,\r\n channel: IActionChannel<TO_ACCEPTOR, TO_CONNECTOR>,\r\n options: TServeDurableObjectOptions<TO_ACCEPTOR, TApp>,\r\n): TDurableObjectChannelServer<TApp>;\r\nexport function serveDurableObject(\r\n ctx: IDurableObjectContext,\r\n channel: IActionChannel<readonly ActionDomain<any>[], readonly ActionDomain<any>[]>,\r\n options: TServeDurableObjectOptions<readonly ActionDomain<any>[], any>,\r\n): TDurableObjectChannelServer<any> {\r\n const { runtime, keyPrefix, httpFallback, secure, ...serveOptions } = options;\r\n const host = cloudflareDurableObjectHost(ctx, { keyPrefix, httpFallback, secure });\r\n return serveHost(runtime, channel, host, serveOptions);\r\n}\r\n\r\n/** The slice of a Durable Object stub {@link forwardExchangeToDurableObject} calls — `env.NS.get(id)`. */\r\nexport interface IDurableObjectStub {\r\n fetch(request: Request): Promise<Response>;\r\n}\r\n\r\nexport interface IForwardExchangeToDurableObjectOptions {\r\n /**\r\n * CORS headers for the edge-answered `OPTIONS` preflight (defaults to the permissive `*` set; `false`\r\n * attaches none). Only the preflight is answered here — every other response comes straight from the DO\r\n * (whose `serveDurableObject` applies its own CORS), so the two should agree.\r\n */\r\n cors?: Record<string, string> | false;\r\n}\r\n\r\n/** Permissive defaults, matching `serveDurableObject`'s HTTP fallback. */\r\nconst DEFAULT_FORWARD_CORS: Record<string, string> = {\r\n \"Access-Control-Allow-Origin\": \"*\",\r\n \"Access-Control-Allow-Methods\": \"GET, POST, OPTIONS\",\r\n \"Access-Control-Allow-Headers\": \"Content-Type\",\r\n \"Access-Control-Max-Age\": \"86400\",\r\n};\r\n\r\n/**\r\n * Fan a Worker's incoming action request out to a *per-id* Durable Object that serves the exchange itself.\r\n *\r\n * A secure exchange body is opaque to the Worker (handshake / encrypted frames), so the DO it belongs to\r\n * has to be chosen from the **URL**, not the body. This picks the stub with your `pickStub` and forwards\r\n * the request to its `fetch` (where `serveDurableObject` serves the exchange), while answering the CORS\r\n * `OPTIONS` preflight *at the edge* so a per-id DO is never woken (or billed) just to reply to a preflight:\r\n * ```ts\r\n * // wrangler: a Durable Object namespace `BRIDGE`, each instance one bridge serving `bridgeChannel`.\r\n * export default {\r\n * fetch(request: Request, env: Env) {\r\n * return forwardExchangeToDurableObject(request, (req, url) => {\r\n * const bridgeId = url.pathname.split(\"/\")[2]; // e.g. /bridge/:id/action\r\n * return env.BRIDGE.get(env.BRIDGE.idFromName(bridgeId));\r\n * });\r\n * },\r\n * };\r\n *\r\n * // …and in the Durable Object, serve the secure exchange (+ a WS upgrade) as usual:\r\n * // fetch(request) { return this.server.fetch(request); }\r\n * // where this.server = serveDurableObject(this.ctx, bridgeChannel, { runtime, httpFallback: \"secure\" });\r\n * ```\r\n * The matching connector points its `httpCarrier` at `/bridge/:id/action`. `pickStub` may be async (e.g.\r\n * to look an id up first); it receives the parsed {@link URL} alongside the request.\r\n */\r\nexport function forwardExchangeToDurableObject(\r\n request: Request,\r\n pickStub: (request: Request, url: URL) => IDurableObjectStub | Promise<IDurableObjectStub>,\r\n options: IForwardExchangeToDurableObjectOptions = {},\r\n): Promise<Response> {\r\n if (request.method === \"OPTIONS\") {\r\n const headers = options.cors === false ? undefined : (options.cors ?? DEFAULT_FORWARD_CORS);\r\n return Promise.resolve(new Response(null, { status: 204, headers }));\r\n }\r\n const url = new URL(request.url);\r\n return Promise.resolve(pickStub(request, url)).then((stub) => stub.fetch(request));\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAuHA,SAAgB,uBACd,KACA,UAA0C,CAAC,GACM;CACjD,OAAOA,4BAAAA,kBAA2C;EAChD,QAAQ,QAAQ;EAChB,OAAO,IAAI,UAAU,GAAG,KAAK,KAAK;EAClC,eAAe;GACb,MAAM,OAAO,IAAI,cAAc;GAC/B,MAAM,SAAS,KAAK;GACpB,MAAM,SAAS,KAAK;GAEpB,IAAI,gBAAgB,MAAM;GAC1B,OAAO,IAAI,SAAS,MAAM;IAAE,QAAQ;IAAK,WAAW;GAAO,CAAC;EAC9D;EAEA,iBAAiB;GACf,sBAAsB,IAAI,cAAc;GACxC,OAAO,OAAO,GAAG,sBAAsB;GACvC,QAAQ,IAAI,UAAU,GAAG,oBAAoB,KAAK;EACpD;CACF,CAAC;AACH;;;;;AAWA,SAAgB,qBACd,KACA,UAAwC,CAAC,GACzB;CAChB,QAAA,GAAA,gBAAA,kCAAA,CAAyC;EACvC,sBAAsB,IAAI;EAC1B,WAAW,QAAQ;CACrB,CAAC;AACH;;;;;;;AAqBA,SAAgB,4BACd,KACA,UAA+C,CAAC,GACF;CAC9C,MAAM,eAAe,QAAQ,gBAAgB;CAC7C,MAAM,WAAwD,CAC5D,uBAAuB,KAAK,EAAE,QAAQ,QAAQ,OAAO,CAAC,CACxD;CACA,IAAI,iBAAiB,OACnB,SAAS,KAAKC,4BAAAA,oBAAoB,EAAE,QAAQ,iBAAiB,SAAS,CAAC,CAAC;CAG1E,OAAO;EACL;EACA,SAAS,qBAAqB,KAAK,EAAE,WAAW,QAAQ,UAAU,CAAC;EACnE,gBAAgB;GAEd,IAAI,yBAAyB,IAAI,6BAA6B,QAAQ,MAAM,CAAC;EAC/E;CACF;AACF;AAmDA,SAAgB,mBACd,KACA,SACA,SACkC;CAClC,MAAM,EAAE,SAAS,WAAW,cAAc,QAAQ,GAAG,iBAAiB;CAEtE,OAAOC,4BAAAA,UAAU,SAAS,SADb,4BAA4B,KAAK;EAAE;EAAW;EAAc;CAAO,CAC1C,GAAG,YAAY;AACvD;;AAiBA,MAAM,uBAA+C;CACnD,+BAA+B;CAC/B,gCAAgC;CAChC,gCAAgC;CAChC,0BAA0B;AAC5B;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,SAAgB,+BACd,SACA,UACA,UAAkD,CAAC,GAChC;CACnB,IAAI,QAAQ,WAAW,WAAW;EAChC,MAAM,UAAU,QAAQ,SAAS,QAAQ,KAAA,IAAa,QAAQ,QAAQ;EACtE,OAAO,QAAQ,QAAQ,IAAI,SAAS,MAAM;GAAE,QAAQ;GAAK;EAAQ,CAAC,CAAC;CACrE;CACA,MAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;CAC/B,OAAO,QAAQ,QAAQ,SAAS,SAAS,GAAG,CAAC,CAAC,CAAC,MAAM,SAAS,KAAK,MAAM,OAAO,CAAC;AACnF"}
@@ -1,4 +1,4 @@
1
- import { _t as IDuplexAcceptorCarrier, ct as IChannelHostAdapter, dt as IChannelServer, kt as IActionChannel, l as ActionDomain, lt as TServeHostOptions, mt as IServeConnectionStateOptions, u as ActionRuntime, wt as ConnectionStateStore } from "../../AcceptorHandler-CxD0c1BE.cjs";
1
+ import { Lt as IActionChannel, Mt as ConnectionStateStore, St as IServeConnectionStateOptions, Tt as IDuplexAcceptorCarrier, _t as TServeHostOptions, gt as IChannelHostAdapter, l as ActionDomain, u as ActionRuntime, yt as IChannelServer } from "../../AcceptorHandler-CxPfZtIl.cjs";
2
2
  import { StorageAdapter, TCreateDurableObjectStorageOptions } from "@nice-code/util";
3
3
 
4
4
  //#region src/platform/cloudflare/index.d.ts
@@ -120,6 +120,44 @@ declare function serveDurableObject<TO_ACCEPTOR extends readonly ActionDomain<an
120
120
  connections: ConnectionStateStore<IDurableObjectWebSocket, TApp>;
121
121
  };
122
122
  declare function serveDurableObject<TO_ACCEPTOR extends readonly ActionDomain<any>[] = readonly ActionDomain<any>[], TO_CONNECTOR extends readonly ActionDomain<any>[] = readonly ActionDomain<any>[], TApp = unknown>(ctx: IDurableObjectContext, channel: IActionChannel<TO_ACCEPTOR, TO_CONNECTOR>, options: TServeDurableObjectOptions<TO_ACCEPTOR, TApp>): TDurableObjectChannelServer<TApp>;
123
+ /** The slice of a Durable Object stub {@link forwardExchangeToDurableObject} calls — `env.NS.get(id)`. */
124
+ interface IDurableObjectStub {
125
+ fetch(request: Request): Promise<Response>;
126
+ }
127
+ interface IForwardExchangeToDurableObjectOptions {
128
+ /**
129
+ * CORS headers for the edge-answered `OPTIONS` preflight (defaults to the permissive `*` set; `false`
130
+ * attaches none). Only the preflight is answered here — every other response comes straight from the DO
131
+ * (whose `serveDurableObject` applies its own CORS), so the two should agree.
132
+ */
133
+ cors?: Record<string, string> | false;
134
+ }
135
+ /**
136
+ * Fan a Worker's incoming action request out to a *per-id* Durable Object that serves the exchange itself.
137
+ *
138
+ * A secure exchange body is opaque to the Worker (handshake / encrypted frames), so the DO it belongs to
139
+ * has to be chosen from the **URL**, not the body. This picks the stub with your `pickStub` and forwards
140
+ * the request to its `fetch` (where `serveDurableObject` serves the exchange), while answering the CORS
141
+ * `OPTIONS` preflight *at the edge* so a per-id DO is never woken (or billed) just to reply to a preflight:
142
+ * ```ts
143
+ * // wrangler: a Durable Object namespace `BRIDGE`, each instance one bridge serving `bridgeChannel`.
144
+ * export default {
145
+ * fetch(request: Request, env: Env) {
146
+ * return forwardExchangeToDurableObject(request, (req, url) => {
147
+ * const bridgeId = url.pathname.split("/")[2]; // e.g. /bridge/:id/action
148
+ * return env.BRIDGE.get(env.BRIDGE.idFromName(bridgeId));
149
+ * });
150
+ * },
151
+ * };
152
+ *
153
+ * // …and in the Durable Object, serve the secure exchange (+ a WS upgrade) as usual:
154
+ * // fetch(request) { return this.server.fetch(request); }
155
+ * // where this.server = serveDurableObject(this.ctx, bridgeChannel, { runtime, httpFallback: "secure" });
156
+ * ```
157
+ * The matching connector points its `httpCarrier` at `/bridge/:id/action`. `pickStub` may be async (e.g.
158
+ * to look an id up first); it receives the parsed {@link URL} alongside the request.
159
+ */
160
+ declare function forwardExchangeToDurableObject(request: Request, pickStub: (request: Request, url: URL) => IDurableObjectStub | Promise<IDurableObjectStub>, options?: IForwardExchangeToDurableObjectOptions): Promise<Response>;
123
161
  //#endregion
124
- export { ICloudflareDurableObjectHostOptions, IDurableObjectContext, IDurableObjectStorageOptions, IDurableObjectWebSocket, IDurableObjectWsCarrierOptions, TDurableObjectChannelServer, TServeDurableObjectOptions, cloudflareDurableObjectHost, durableObjectStorage, durableObjectWsCarrier, serveDurableObject };
162
+ export { ICloudflareDurableObjectHostOptions, IDurableObjectContext, IDurableObjectStorageOptions, IDurableObjectStub, IDurableObjectWebSocket, IDurableObjectWsCarrierOptions, IForwardExchangeToDurableObjectOptions, TDurableObjectChannelServer, TServeDurableObjectOptions, cloudflareDurableObjectHost, durableObjectStorage, durableObjectWsCarrier, forwardExchangeToDurableObject, serveDurableObject };
125
163
  //# sourceMappingURL=index.d.cts.map
@@ -1,4 +1,4 @@
1
- import { _t as IDuplexAcceptorCarrier, ct as IChannelHostAdapter, dt as IChannelServer, kt as IActionChannel, l as ActionDomain, lt as TServeHostOptions, mt as IServeConnectionStateOptions, u as ActionRuntime, wt as ConnectionStateStore } from "../../AcceptorHandler-11-QMdx2.mjs";
1
+ import { Lt as IActionChannel, Mt as ConnectionStateStore, St as IServeConnectionStateOptions, Tt as IDuplexAcceptorCarrier, _t as TServeHostOptions, gt as IChannelHostAdapter, l as ActionDomain, u as ActionRuntime, yt as IChannelServer } from "../../AcceptorHandler-BizUtq4u.mjs";
2
2
  import { StorageAdapter, TCreateDurableObjectStorageOptions } from "@nice-code/util";
3
3
 
4
4
  //#region src/platform/cloudflare/index.d.ts
@@ -120,6 +120,44 @@ declare function serveDurableObject<TO_ACCEPTOR extends readonly ActionDomain<an
120
120
  connections: ConnectionStateStore<IDurableObjectWebSocket, TApp>;
121
121
  };
122
122
  declare function serveDurableObject<TO_ACCEPTOR extends readonly ActionDomain<any>[] = readonly ActionDomain<any>[], TO_CONNECTOR extends readonly ActionDomain<any>[] = readonly ActionDomain<any>[], TApp = unknown>(ctx: IDurableObjectContext, channel: IActionChannel<TO_ACCEPTOR, TO_CONNECTOR>, options: TServeDurableObjectOptions<TO_ACCEPTOR, TApp>): TDurableObjectChannelServer<TApp>;
123
+ /** The slice of a Durable Object stub {@link forwardExchangeToDurableObject} calls — `env.NS.get(id)`. */
124
+ interface IDurableObjectStub {
125
+ fetch(request: Request): Promise<Response>;
126
+ }
127
+ interface IForwardExchangeToDurableObjectOptions {
128
+ /**
129
+ * CORS headers for the edge-answered `OPTIONS` preflight (defaults to the permissive `*` set; `false`
130
+ * attaches none). Only the preflight is answered here — every other response comes straight from the DO
131
+ * (whose `serveDurableObject` applies its own CORS), so the two should agree.
132
+ */
133
+ cors?: Record<string, string> | false;
134
+ }
135
+ /**
136
+ * Fan a Worker's incoming action request out to a *per-id* Durable Object that serves the exchange itself.
137
+ *
138
+ * A secure exchange body is opaque to the Worker (handshake / encrypted frames), so the DO it belongs to
139
+ * has to be chosen from the **URL**, not the body. This picks the stub with your `pickStub` and forwards
140
+ * the request to its `fetch` (where `serveDurableObject` serves the exchange), while answering the CORS
141
+ * `OPTIONS` preflight *at the edge* so a per-id DO is never woken (or billed) just to reply to a preflight:
142
+ * ```ts
143
+ * // wrangler: a Durable Object namespace `BRIDGE`, each instance one bridge serving `bridgeChannel`.
144
+ * export default {
145
+ * fetch(request: Request, env: Env) {
146
+ * return forwardExchangeToDurableObject(request, (req, url) => {
147
+ * const bridgeId = url.pathname.split("/")[2]; // e.g. /bridge/:id/action
148
+ * return env.BRIDGE.get(env.BRIDGE.idFromName(bridgeId));
149
+ * });
150
+ * },
151
+ * };
152
+ *
153
+ * // …and in the Durable Object, serve the secure exchange (+ a WS upgrade) as usual:
154
+ * // fetch(request) { return this.server.fetch(request); }
155
+ * // where this.server = serveDurableObject(this.ctx, bridgeChannel, { runtime, httpFallback: "secure" });
156
+ * ```
157
+ * The matching connector points its `httpCarrier` at `/bridge/:id/action`. `pickStub` may be async (e.g.
158
+ * to look an id up first); it receives the parsed {@link URL} alongside the request.
159
+ */
160
+ declare function forwardExchangeToDurableObject(request: Request, pickStub: (request: Request, url: URL) => IDurableObjectStub | Promise<IDurableObjectStub>, options?: IForwardExchangeToDurableObjectOptions): Promise<Response>;
123
161
  //#endregion
124
- export { ICloudflareDurableObjectHostOptions, IDurableObjectContext, IDurableObjectStorageOptions, IDurableObjectWebSocket, IDurableObjectWsCarrierOptions, TDurableObjectChannelServer, TServeDurableObjectOptions, cloudflareDurableObjectHost, durableObjectStorage, durableObjectWsCarrier, serveDurableObject };
162
+ export { ICloudflareDurableObjectHostOptions, IDurableObjectContext, IDurableObjectStorageOptions, IDurableObjectStub, IDurableObjectWebSocket, IDurableObjectWsCarrierOptions, IForwardExchangeToDurableObjectOptions, TDurableObjectChannelServer, TServeDurableObjectOptions, cloudflareDurableObjectHost, durableObjectStorage, durableObjectWsCarrier, forwardExchangeToDurableObject, serveDurableObject };
125
163
  //# sourceMappingURL=index.d.mts.map
@@ -1,4 +1,4 @@
1
- import { n as wsAcceptorCarrier, r as serveHost, t as httpAcceptorCarrier } from "../../httpAcceptorCarrier-DPBEuewS.mjs";
1
+ import { n as wsAcceptorCarrier, r as serveHost, t as httpAcceptorCarrier } from "../../httpAcceptorCarrier-DWqsCz3h.mjs";
2
2
  import { createDurableObjectStorageAdapter } from "@nice-code/util";
3
3
  //#region src/platform/cloudflare/index.ts
4
4
  /**
@@ -78,7 +78,50 @@ function serveDurableObject(ctx, channel, options) {
78
78
  secure
79
79
  }), serveOptions);
80
80
  }
81
+ /** Permissive defaults, matching `serveDurableObject`'s HTTP fallback. */
82
+ const DEFAULT_FORWARD_CORS = {
83
+ "Access-Control-Allow-Origin": "*",
84
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
85
+ "Access-Control-Allow-Headers": "Content-Type",
86
+ "Access-Control-Max-Age": "86400"
87
+ };
88
+ /**
89
+ * Fan a Worker's incoming action request out to a *per-id* Durable Object that serves the exchange itself.
90
+ *
91
+ * A secure exchange body is opaque to the Worker (handshake / encrypted frames), so the DO it belongs to
92
+ * has to be chosen from the **URL**, not the body. This picks the stub with your `pickStub` and forwards
93
+ * the request to its `fetch` (where `serveDurableObject` serves the exchange), while answering the CORS
94
+ * `OPTIONS` preflight *at the edge* so a per-id DO is never woken (or billed) just to reply to a preflight:
95
+ * ```ts
96
+ * // wrangler: a Durable Object namespace `BRIDGE`, each instance one bridge serving `bridgeChannel`.
97
+ * export default {
98
+ * fetch(request: Request, env: Env) {
99
+ * return forwardExchangeToDurableObject(request, (req, url) => {
100
+ * const bridgeId = url.pathname.split("/")[2]; // e.g. /bridge/:id/action
101
+ * return env.BRIDGE.get(env.BRIDGE.idFromName(bridgeId));
102
+ * });
103
+ * },
104
+ * };
105
+ *
106
+ * // …and in the Durable Object, serve the secure exchange (+ a WS upgrade) as usual:
107
+ * // fetch(request) { return this.server.fetch(request); }
108
+ * // where this.server = serveDurableObject(this.ctx, bridgeChannel, { runtime, httpFallback: "secure" });
109
+ * ```
110
+ * The matching connector points its `httpCarrier` at `/bridge/:id/action`. `pickStub` may be async (e.g.
111
+ * to look an id up first); it receives the parsed {@link URL} alongside the request.
112
+ */
113
+ function forwardExchangeToDurableObject(request, pickStub, options = {}) {
114
+ if (request.method === "OPTIONS") {
115
+ const headers = options.cors === false ? void 0 : options.cors ?? DEFAULT_FORWARD_CORS;
116
+ return Promise.resolve(new Response(null, {
117
+ status: 204,
118
+ headers
119
+ }));
120
+ }
121
+ const url = new URL(request.url);
122
+ return Promise.resolve(pickStub(request, url)).then((stub) => stub.fetch(request));
123
+ }
81
124
  //#endregion
82
- export { cloudflareDurableObjectHost, durableObjectStorage, durableObjectWsCarrier, serveDurableObject };
125
+ export { cloudflareDurableObjectHost, durableObjectStorage, durableObjectWsCarrier, forwardExchangeToDurableObject, serveDurableObject };
83
126
 
84
127
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../../src/platform/cloudflare/index.ts"],"sourcesContent":["import {\r\n createDurableObjectStorageAdapter,\r\n type StorageAdapter,\r\n type TCreateDurableObjectStorageOptions,\r\n} from \"@nice-code/util\";\r\nimport type { ActionDomain } from \"../../ActionDefinition/Domain/ActionDomain\";\r\nimport type { ActionRuntime } from \"../../ActionRuntime/ActionRuntime\";\r\nimport type { IActionChannel } from \"../../ActionRuntime/Channel/ActionChannel\";\r\nimport type {\r\n IChannelServer,\r\n IServeConnectionStateOptions,\r\n} from \"../../ActionRuntime/Channel/serveChannel\";\r\nimport {\r\n type IChannelHostAdapter,\r\n serveHost,\r\n type TServeHostOptions,\r\n} from \"../../ActionRuntime/Channel/serveHost\";\r\nimport type { ConnectionStateStore } from \"../../ActionRuntime/Handler/PeerLink/Acceptor/Hibernation/ConnectionStateStore\";\r\nimport type {\r\n IDuplexAcceptorCarrier,\r\n TAcceptorCarrier,\r\n} from \"../../ActionRuntime/Transport/Carrier/AcceptorCarrier.types\";\r\nimport { wsAcceptorCarrier } from \"../../ActionRuntime/Transport/Carrier/duplex/ws/wsAcceptorCarrier\";\r\nimport { httpAcceptorCarrier } from \"../../ActionRuntime/Transport/Carrier/exchange/http/httpAcceptorCarrier\";\r\n\r\n/**\r\n * Cloudflare-specific helpers for `@nice-code/action`, imported from `@nice-code/action/platform/cloudflare`.\r\n * They collapse the Durable Object boilerplate (the `WebSocketPair` upgrade, hibernation attachment wiring,\r\n * and DO-storage adapter) into one-liners you hand to `serveChannel`. The core library stays\r\n * platform-agnostic — nothing here is reachable from the main entry.\r\n *\r\n * The Workers runtime surface this module needs is declared *structurally* (and the two globals it\r\n * constructs are declared module-locally below) rather than pulled from `@cloudflare/workers-types`, so the\r\n * library's own DOM-lib build never clashes with that package's global `Response`/`WebSocket` redefinitions.\r\n * A real `DurableObjectState` and its hibernatable `WebSocket`s satisfy these shapes.\r\n */\r\n\r\ntype TDurableObjectStorage = TCreateDurableObjectStorageOptions[\"durableObjectStorage\"];\r\n\r\n/** The slice of a Durable Object's hibernatable WebSocket these helpers touch. */\r\nexport interface IDurableObjectWebSocket {\r\n send(data: string | ArrayBuffer | Uint8Array): void;\r\n serializeAttachment(value: unknown): void;\r\n // Mirrors the Workers runtime's `any` return so a typed connection binding round-trips without a cast.\r\n deserializeAttachment(): any;\r\n}\r\n\r\n/** The slice of a Durable Object's `state` (its `ctx`) these helpers touch. */\r\nexport interface IDurableObjectContext {\r\n storage: TDurableObjectStorage;\r\n getWebSockets(): IDurableObjectWebSocket[];\r\n acceptWebSocket(ws: IDurableObjectWebSocket): void;\r\n /** Register a runtime-answered keepalive so pings never wake the DO. */\r\n setWebSocketAutoResponse(pair: IWebSocketRequestResponsePair): void;\r\n}\r\n\r\n/** An `IChannelServer` whose connections are a Durable Object's hibernatable WebSockets — the type to\r\n * store the result of `serveChannel(...)` in when serving over {@link durableObjectWsCarrier}. `TApp` is the\r\n * per-connection app-state type when `connectionState` is used (defaults to `unknown` otherwise). */\r\nexport type TDurableObjectChannelServer<TApp = unknown> = IChannelServer<\r\n IDurableObjectWebSocket,\r\n TApp\r\n>;\r\n\r\n// Workers runtime globals, declared module-locally (the runtime provides them at deploy time). Declaring\r\n// them here keeps them out of the package's global scope, so the DOM lib's `Response`/`WebSocket` stand\r\n// elsewhere. The `Response` constructor type still returns the DOM `Response` — only its init gains the\r\n// Workers-only `webSocket` field.\r\ndeclare const WebSocketPair: {\r\n new (): { 0: IDurableObjectWebSocket; 1: IDurableObjectWebSocket };\r\n};\r\ndeclare const Response: {\r\n new (\r\n body: BodyInit | null,\r\n init?: {\r\n status?: number;\r\n statusText?: string;\r\n headers?: HeadersInit;\r\n webSocket?: IDurableObjectWebSocket;\r\n },\r\n ): Response;\r\n};\r\n/** The keepalive pair shape — just the fields the Workers runtime's `WebSocketRequestResponsePair` exposes. */\r\ninterface IWebSocketRequestResponsePair {\r\n readonly request: string;\r\n readonly response: string;\r\n}\r\ndeclare const WebSocketRequestResponsePair: {\r\n new (request: string, response: string): IWebSocketRequestResponsePair;\r\n};\r\n\r\nexport interface IDurableObjectWsCarrierOptions {\r\n /**\r\n * Whether each socket runs the secure handshake (default `true`). Pass `false` for a plain WS endpoint —\r\n * then `serveChannel` needs no `storage` for this carrier.\r\n */\r\n secure?: boolean;\r\n}\r\n\r\n/**\r\n * Build a hibernatable-WebSocket acceptor carrier for a Durable Object in one call — the `send`, the\r\n * `WebSocketPair` upgrade, and the hibernation attachment hooks all derived from the DO's `ctx`. Hand it\r\n * straight to `serveChannel`'s `carriers`, and forward the DO's socket events to the returned handle:\r\n * ```ts\r\n * const ws = durableObjectWsCarrier(this.ctx);\r\n * const server = serveChannel(runtime, channel, {\r\n * clientEnv,\r\n * storage: durableObjectStorage(this.ctx, { keyPrefix: \"ws:\" }),\r\n * handlers: [localHandler],\r\n * carriers: [ws, httpAcceptorCarrier()],\r\n * });\r\n * // webSocketMessage(c, m) => ws.receive(c, m);\r\n * // webSocketClose/Error(c) => ws.drop(c);\r\n * ```\r\n *\r\n * The carrier exposes the DO's socket attachment to `serveChannel`, which persists the routing binding\r\n * there and replays it on wake — and, when `connectionState` is requested, co-stores per-connection app\r\n * state in the *same* attachment, so both survive eviction without the DO wiring any of it by hand.\r\n */\r\nexport function durableObjectWsCarrier(\r\n ctx: IDurableObjectContext,\r\n options: IDurableObjectWsCarrierOptions = {},\r\n): IDuplexAcceptorCarrier<IDurableObjectWebSocket> {\r\n return wsAcceptorCarrier<IDurableObjectWebSocket>({\r\n secure: options.secure,\r\n send: (ws, frame) => ws.send(frame),\r\n upgrade: () => {\r\n const pair = new WebSocketPair();\r\n const client = pair[0];\r\n const server = pair[1];\r\n // Hibernatable WebSocket — the DO can sleep between messages.\r\n ctx.acceptWebSocket(server);\r\n return new Response(null, { status: 101, webSocket: client });\r\n },\r\n // Raw access to each socket's attachment; `serveChannel` owns the composite (binding + app) layout.\r\n attachmentStore: {\r\n getConnections: () => ctx.getWebSockets(),\r\n read: (ws) => ws.deserializeAttachment(),\r\n write: (ws, value) => ws.serializeAttachment(value),\r\n },\r\n });\r\n}\r\n\r\nexport interface IDurableObjectStorageOptions {\r\n /** Namespace prefix for every key (e.g. `\"demo-ws:\"`), so several adapters can share one DO storage. */\r\n keyPrefix?: string;\r\n}\r\n\r\n/**\r\n * Wrap a Durable Object's storage as a {@link StorageAdapter} for `serveChannel`'s `storage` — sugar over\r\n * `createDurableObjectStorageAdapter({ durableObjectStorage: ctx.storage, … })` so a DO needs one import.\r\n */\r\nexport function durableObjectStorage(\r\n ctx: IDurableObjectContext,\r\n options: IDurableObjectStorageOptions = {},\r\n): StorageAdapter {\r\n return createDurableObjectStorageAdapter({\r\n durableObjectStorage: ctx.storage,\r\n keyPrefix: options.keyPrefix,\r\n });\r\n}\r\n\r\nexport interface ICloudflareDurableObjectHostOptions {\r\n /** Namespace prefix for the DO-storage crypto identity keys (e.g. `\"lobby-ws:\"`). */\r\n keyPrefix?: string;\r\n /**\r\n * The HTTP fallback that sits beside the WebSocket: `\"plain\"` (default — POSTs the raw action wire, the\r\n * usual fallback for a public client), `\"secure\"` (the full handshake-protected exchange, sharing the WS\r\n * identity), or `false` (WebSocket only).\r\n */\r\n httpFallback?: \"plain\" | \"secure\" | false;\r\n /** Whether the WebSocket runs the secure handshake (default `true`). `false` = a plain WS endpoint. */\r\n secure?: boolean;\r\n}\r\n\r\n/**\r\n * Build the {@link IChannelHostAdapter} for a Durable Object in one call — the entire repeated transport\r\n * stack a DO would otherwise assemble by hand: a hibernatable secure WebSocket carrier, an HTTP fallback,\r\n * the DO-storage-backed crypto identity, and a runtime-answered `ping`/`pong` keepalive (so pings never\r\n * wake the DO). Hand it to {@link serveHost}, or use {@link serveDurableObject} which composes both.\r\n */\r\nexport function cloudflareDurableObjectHost(\r\n ctx: IDurableObjectContext,\r\n options: ICloudflareDurableObjectHostOptions = {},\r\n): IChannelHostAdapter<IDurableObjectWebSocket> {\r\n const httpFallback = options.httpFallback ?? \"plain\";\r\n const carriers: TAcceptorCarrier<IDurableObjectWebSocket>[] = [\r\n durableObjectWsCarrier(ctx, { secure: options.secure }),\r\n ];\r\n if (httpFallback !== false) {\r\n carriers.push(httpAcceptorCarrier({ secure: httpFallback === \"secure\" }));\r\n }\r\n\r\n return {\r\n carriers,\r\n storage: durableObjectStorage(ctx, { keyPrefix: options.keyPrefix }),\r\n onServed: () => {\r\n // Keepalive answered by the runtime itself — pings never wake the DO.\r\n ctx.setWebSocketAutoResponse(new WebSocketRequestResponsePair(\"ping\", \"pong\"));\r\n },\r\n };\r\n}\r\n\r\n/** {@link serveDurableObject}'s options: the `serveHost` surface + the DO runtime + the host knobs. */\r\nexport type TServeDurableObjectOptions<\r\n TO_ACCEPTOR extends readonly ActionDomain<any>[],\r\n TApp = unknown,\r\n> = TServeHostOptions<TO_ACCEPTOR, IDurableObjectWebSocket, TApp> &\r\n ICloudflareDurableObjectHostOptions & {\r\n /** This DO's runtime (e.g. `new ActionRuntime(coord.withPersistentId(ctx.id.toString()))`). */\r\n runtime: ActionRuntime;\r\n };\r\n\r\n/**\r\n * Serve a secure channel from a Durable Object in one call — the whole transport stack\r\n * ({@link cloudflareDurableObjectHost}: hibernatable secure WebSocket + HTTP fallback + DO-storage crypto\r\n * identity + keepalive) folded in, leaving the DO to forward its four socket lifecycle methods to the\r\n * returned server's `fetch` / `receive` / `drop`:\r\n * ```ts\r\n * const server = serveDurableObject(this.ctx, lobbyChannel, {\r\n * runtime, clientEnv,\r\n * connectionState: { schema: vs_player }, // optional, survives hibernation\r\n * channelCases: { join: (action, conn) => { conn.setState(action.input); conn.broadcast(…); } },\r\n * });\r\n * // fetch(req) => server.fetch(req)\r\n * // webSocketMessage(ws, m) => server.receive(ws, m)\r\n * // webSocketClose/Error(ws)=> server.drop(ws)\r\n * ```\r\n * Passing `connectionState` narrows the return so `server.connections` is non-optional.\r\n */\r\nexport function serveDurableObject<\r\n TO_ACCEPTOR extends readonly ActionDomain<any>[],\r\n TO_CONNECTOR extends readonly ActionDomain<any>[],\r\n TApp,\r\n>(\r\n ctx: IDurableObjectContext,\r\n channel: IActionChannel<TO_ACCEPTOR, TO_CONNECTOR>,\r\n options: TServeDurableObjectOptions<TO_ACCEPTOR, TApp> & {\r\n connectionState: IServeConnectionStateOptions<TApp>;\r\n },\r\n): TDurableObjectChannelServer<TApp> & {\r\n connections: ConnectionStateStore<IDurableObjectWebSocket, TApp>;\r\n};\r\nexport function serveDurableObject<\r\n TO_ACCEPTOR extends readonly ActionDomain<any>[] = readonly ActionDomain<any>[],\r\n TO_CONNECTOR extends readonly ActionDomain<any>[] = readonly ActionDomain<any>[],\r\n TApp = unknown,\r\n>(\r\n ctx: IDurableObjectContext,\r\n channel: IActionChannel<TO_ACCEPTOR, TO_CONNECTOR>,\r\n options: TServeDurableObjectOptions<TO_ACCEPTOR, TApp>,\r\n): TDurableObjectChannelServer<TApp>;\r\nexport function serveDurableObject(\r\n ctx: IDurableObjectContext,\r\n channel: IActionChannel<readonly ActionDomain<any>[], readonly ActionDomain<any>[]>,\r\n options: TServeDurableObjectOptions<readonly ActionDomain<any>[], any>,\r\n): TDurableObjectChannelServer<any> {\r\n const { runtime, keyPrefix, httpFallback, secure, ...serveOptions } = options;\r\n const host = cloudflareDurableObjectHost(ctx, { keyPrefix, httpFallback, secure });\r\n return serveHost(runtime, channel, host, serveOptions);\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAuHA,SAAgB,uBACd,KACA,UAA0C,CAAC,GACM;CACjD,OAAO,kBAA2C;EAChD,QAAQ,QAAQ;EAChB,OAAO,IAAI,UAAU,GAAG,KAAK,KAAK;EAClC,eAAe;GACb,MAAM,OAAO,IAAI,cAAc;GAC/B,MAAM,SAAS,KAAK;GACpB,MAAM,SAAS,KAAK;GAEpB,IAAI,gBAAgB,MAAM;GAC1B,OAAO,IAAI,SAAS,MAAM;IAAE,QAAQ;IAAK,WAAW;GAAO,CAAC;EAC9D;EAEA,iBAAiB;GACf,sBAAsB,IAAI,cAAc;GACxC,OAAO,OAAO,GAAG,sBAAsB;GACvC,QAAQ,IAAI,UAAU,GAAG,oBAAoB,KAAK;EACpD;CACF,CAAC;AACH;;;;;AAWA,SAAgB,qBACd,KACA,UAAwC,CAAC,GACzB;CAChB,OAAO,kCAAkC;EACvC,sBAAsB,IAAI;EAC1B,WAAW,QAAQ;CACrB,CAAC;AACH;;;;;;;AAqBA,SAAgB,4BACd,KACA,UAA+C,CAAC,GACF;CAC9C,MAAM,eAAe,QAAQ,gBAAgB;CAC7C,MAAM,WAAwD,CAC5D,uBAAuB,KAAK,EAAE,QAAQ,QAAQ,OAAO,CAAC,CACxD;CACA,IAAI,iBAAiB,OACnB,SAAS,KAAK,oBAAoB,EAAE,QAAQ,iBAAiB,SAAS,CAAC,CAAC;CAG1E,OAAO;EACL;EACA,SAAS,qBAAqB,KAAK,EAAE,WAAW,QAAQ,UAAU,CAAC;EACnE,gBAAgB;GAEd,IAAI,yBAAyB,IAAI,6BAA6B,QAAQ,MAAM,CAAC;EAC/E;CACF;AACF;AAmDA,SAAgB,mBACd,KACA,SACA,SACkC;CAClC,MAAM,EAAE,SAAS,WAAW,cAAc,QAAQ,GAAG,iBAAiB;CAEtE,OAAO,UAAU,SAAS,SADb,4BAA4B,KAAK;EAAE;EAAW;EAAc;CAAO,CAC1C,GAAG,YAAY;AACvD"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../../src/platform/cloudflare/index.ts"],"sourcesContent":["import {\r\n createDurableObjectStorageAdapter,\r\n type StorageAdapter,\r\n type TCreateDurableObjectStorageOptions,\r\n} from \"@nice-code/util\";\r\nimport type { ActionDomain } from \"../../ActionDefinition/Domain/ActionDomain\";\r\nimport type { ActionRuntime } from \"../../ActionRuntime/ActionRuntime\";\r\nimport type { IActionChannel } from \"../../ActionRuntime/Channel/ActionChannel\";\r\nimport type {\r\n IChannelServer,\r\n IServeConnectionStateOptions,\r\n} from \"../../ActionRuntime/Channel/serveChannel\";\r\nimport {\r\n type IChannelHostAdapter,\r\n serveHost,\r\n type TServeHostOptions,\r\n} from \"../../ActionRuntime/Channel/serveHost\";\r\nimport type { ConnectionStateStore } from \"../../ActionRuntime/Handler/PeerLink/Acceptor/Hibernation/ConnectionStateStore\";\r\nimport type {\r\n IDuplexAcceptorCarrier,\r\n TAcceptorCarrier,\r\n} from \"../../ActionRuntime/Transport/Carrier/AcceptorCarrier.types\";\r\nimport { wsAcceptorCarrier } from \"../../ActionRuntime/Transport/Carrier/duplex/ws/wsAcceptorCarrier\";\r\nimport { httpAcceptorCarrier } from \"../../ActionRuntime/Transport/Carrier/exchange/http/httpAcceptorCarrier\";\r\n\r\n/**\r\n * Cloudflare-specific helpers for `@nice-code/action`, imported from `@nice-code/action/platform/cloudflare`.\r\n * They collapse the Durable Object boilerplate (the `WebSocketPair` upgrade, hibernation attachment wiring,\r\n * and DO-storage adapter) into one-liners you hand to `serveChannel`. The core library stays\r\n * platform-agnostic — nothing here is reachable from the main entry.\r\n *\r\n * The Workers runtime surface this module needs is declared *structurally* (and the two globals it\r\n * constructs are declared module-locally below) rather than pulled from `@cloudflare/workers-types`, so the\r\n * library's own DOM-lib build never clashes with that package's global `Response`/`WebSocket` redefinitions.\r\n * A real `DurableObjectState` and its hibernatable `WebSocket`s satisfy these shapes.\r\n */\r\n\r\ntype TDurableObjectStorage = TCreateDurableObjectStorageOptions[\"durableObjectStorage\"];\r\n\r\n/** The slice of a Durable Object's hibernatable WebSocket these helpers touch. */\r\nexport interface IDurableObjectWebSocket {\r\n send(data: string | ArrayBuffer | Uint8Array): void;\r\n serializeAttachment(value: unknown): void;\r\n // Mirrors the Workers runtime's `any` return so a typed connection binding round-trips without a cast.\r\n deserializeAttachment(): any;\r\n}\r\n\r\n/** The slice of a Durable Object's `state` (its `ctx`) these helpers touch. */\r\nexport interface IDurableObjectContext {\r\n storage: TDurableObjectStorage;\r\n getWebSockets(): IDurableObjectWebSocket[];\r\n acceptWebSocket(ws: IDurableObjectWebSocket): void;\r\n /** Register a runtime-answered keepalive so pings never wake the DO. */\r\n setWebSocketAutoResponse(pair: IWebSocketRequestResponsePair): void;\r\n}\r\n\r\n/** An `IChannelServer` whose connections are a Durable Object's hibernatable WebSockets — the type to\r\n * store the result of `serveChannel(...)` in when serving over {@link durableObjectWsCarrier}. `TApp` is the\r\n * per-connection app-state type when `connectionState` is used (defaults to `unknown` otherwise). */\r\nexport type TDurableObjectChannelServer<TApp = unknown> = IChannelServer<\r\n IDurableObjectWebSocket,\r\n TApp\r\n>;\r\n\r\n// Workers runtime globals, declared module-locally (the runtime provides them at deploy time). Declaring\r\n// them here keeps them out of the package's global scope, so the DOM lib's `Response`/`WebSocket` stand\r\n// elsewhere. The `Response` constructor type still returns the DOM `Response` — only its init gains the\r\n// Workers-only `webSocket` field.\r\ndeclare const WebSocketPair: {\r\n new (): { 0: IDurableObjectWebSocket; 1: IDurableObjectWebSocket };\r\n};\r\ndeclare const Response: {\r\n new (\r\n body: BodyInit | null,\r\n init?: {\r\n status?: number;\r\n statusText?: string;\r\n headers?: HeadersInit;\r\n webSocket?: IDurableObjectWebSocket;\r\n },\r\n ): Response;\r\n};\r\n/** The keepalive pair shape — just the fields the Workers runtime's `WebSocketRequestResponsePair` exposes. */\r\ninterface IWebSocketRequestResponsePair {\r\n readonly request: string;\r\n readonly response: string;\r\n}\r\ndeclare const WebSocketRequestResponsePair: {\r\n new (request: string, response: string): IWebSocketRequestResponsePair;\r\n};\r\n\r\nexport interface IDurableObjectWsCarrierOptions {\r\n /**\r\n * Whether each socket runs the secure handshake (default `true`). Pass `false` for a plain WS endpoint —\r\n * then `serveChannel` needs no `storage` for this carrier.\r\n */\r\n secure?: boolean;\r\n}\r\n\r\n/**\r\n * Build a hibernatable-WebSocket acceptor carrier for a Durable Object in one call — the `send`, the\r\n * `WebSocketPair` upgrade, and the hibernation attachment hooks all derived from the DO's `ctx`. Hand it\r\n * straight to `serveChannel`'s `carriers`, and forward the DO's socket events to the returned handle:\r\n * ```ts\r\n * const ws = durableObjectWsCarrier(this.ctx);\r\n * const server = serveChannel(runtime, channel, {\r\n * clientEnv,\r\n * storage: durableObjectStorage(this.ctx, { keyPrefix: \"ws:\" }),\r\n * handlers: [localHandler],\r\n * carriers: [ws, httpAcceptorCarrier()],\r\n * });\r\n * // webSocketMessage(c, m) => ws.receive(c, m);\r\n * // webSocketClose/Error(c) => ws.drop(c);\r\n * ```\r\n *\r\n * The carrier exposes the DO's socket attachment to `serveChannel`, which persists the routing binding\r\n * there and replays it on wake — and, when `connectionState` is requested, co-stores per-connection app\r\n * state in the *same* attachment, so both survive eviction without the DO wiring any of it by hand.\r\n */\r\nexport function durableObjectWsCarrier(\r\n ctx: IDurableObjectContext,\r\n options: IDurableObjectWsCarrierOptions = {},\r\n): IDuplexAcceptorCarrier<IDurableObjectWebSocket> {\r\n return wsAcceptorCarrier<IDurableObjectWebSocket>({\r\n secure: options.secure,\r\n send: (ws, frame) => ws.send(frame),\r\n upgrade: () => {\r\n const pair = new WebSocketPair();\r\n const client = pair[0];\r\n const server = pair[1];\r\n // Hibernatable WebSocket — the DO can sleep between messages.\r\n ctx.acceptWebSocket(server);\r\n return new Response(null, { status: 101, webSocket: client });\r\n },\r\n // Raw access to each socket's attachment; `serveChannel` owns the composite (binding + app) layout.\r\n attachmentStore: {\r\n getConnections: () => ctx.getWebSockets(),\r\n read: (ws) => ws.deserializeAttachment(),\r\n write: (ws, value) => ws.serializeAttachment(value),\r\n },\r\n });\r\n}\r\n\r\nexport interface IDurableObjectStorageOptions {\r\n /** Namespace prefix for every key (e.g. `\"demo-ws:\"`), so several adapters can share one DO storage. */\r\n keyPrefix?: string;\r\n}\r\n\r\n/**\r\n * Wrap a Durable Object's storage as a {@link StorageAdapter} for `serveChannel`'s `storage` — sugar over\r\n * `createDurableObjectStorageAdapter({ durableObjectStorage: ctx.storage, … })` so a DO needs one import.\r\n */\r\nexport function durableObjectStorage(\r\n ctx: IDurableObjectContext,\r\n options: IDurableObjectStorageOptions = {},\r\n): StorageAdapter {\r\n return createDurableObjectStorageAdapter({\r\n durableObjectStorage: ctx.storage,\r\n keyPrefix: options.keyPrefix,\r\n });\r\n}\r\n\r\nexport interface ICloudflareDurableObjectHostOptions {\r\n /** Namespace prefix for the DO-storage crypto identity keys (e.g. `\"lobby-ws:\"`). */\r\n keyPrefix?: string;\r\n /**\r\n * The HTTP fallback that sits beside the WebSocket: `\"plain\"` (default — POSTs the raw action wire, the\r\n * usual fallback for a public client), `\"secure\"` (the full handshake-protected exchange, sharing the WS\r\n * identity), or `false` (WebSocket only).\r\n */\r\n httpFallback?: \"plain\" | \"secure\" | false;\r\n /** Whether the WebSocket runs the secure handshake (default `true`). `false` = a plain WS endpoint. */\r\n secure?: boolean;\r\n}\r\n\r\n/**\r\n * Build the {@link IChannelHostAdapter} for a Durable Object in one call — the entire repeated transport\r\n * stack a DO would otherwise assemble by hand: a hibernatable secure WebSocket carrier, an HTTP fallback,\r\n * the DO-storage-backed crypto identity, and a runtime-answered `ping`/`pong` keepalive (so pings never\r\n * wake the DO). Hand it to {@link serveHost}, or use {@link serveDurableObject} which composes both.\r\n */\r\nexport function cloudflareDurableObjectHost(\r\n ctx: IDurableObjectContext,\r\n options: ICloudflareDurableObjectHostOptions = {},\r\n): IChannelHostAdapter<IDurableObjectWebSocket> {\r\n const httpFallback = options.httpFallback ?? \"plain\";\r\n const carriers: TAcceptorCarrier<IDurableObjectWebSocket>[] = [\r\n durableObjectWsCarrier(ctx, { secure: options.secure }),\r\n ];\r\n if (httpFallback !== false) {\r\n carriers.push(httpAcceptorCarrier({ secure: httpFallback === \"secure\" }));\r\n }\r\n\r\n return {\r\n carriers,\r\n storage: durableObjectStorage(ctx, { keyPrefix: options.keyPrefix }),\r\n onServed: () => {\r\n // Keepalive answered by the runtime itself — pings never wake the DO.\r\n ctx.setWebSocketAutoResponse(new WebSocketRequestResponsePair(\"ping\", \"pong\"));\r\n },\r\n };\r\n}\r\n\r\n/** {@link serveDurableObject}'s options: the `serveHost` surface + the DO runtime + the host knobs. */\r\nexport type TServeDurableObjectOptions<\r\n TO_ACCEPTOR extends readonly ActionDomain<any>[],\r\n TApp = unknown,\r\n> = TServeHostOptions<TO_ACCEPTOR, IDurableObjectWebSocket, TApp> &\r\n ICloudflareDurableObjectHostOptions & {\r\n /** This DO's runtime (e.g. `new ActionRuntime(coord.withPersistentId(ctx.id.toString()))`). */\r\n runtime: ActionRuntime;\r\n };\r\n\r\n/**\r\n * Serve a secure channel from a Durable Object in one call — the whole transport stack\r\n * ({@link cloudflareDurableObjectHost}: hibernatable secure WebSocket + HTTP fallback + DO-storage crypto\r\n * identity + keepalive) folded in, leaving the DO to forward its four socket lifecycle methods to the\r\n * returned server's `fetch` / `receive` / `drop`:\r\n * ```ts\r\n * const server = serveDurableObject(this.ctx, lobbyChannel, {\r\n * runtime, clientEnv,\r\n * connectionState: { schema: vs_player }, // optional, survives hibernation\r\n * channelCases: { join: (action, conn) => { conn.setState(action.input); conn.broadcast(…); } },\r\n * });\r\n * // fetch(req) => server.fetch(req)\r\n * // webSocketMessage(ws, m) => server.receive(ws, m)\r\n * // webSocketClose/Error(ws)=> server.drop(ws)\r\n * ```\r\n * Passing `connectionState` narrows the return so `server.connections` is non-optional.\r\n */\r\nexport function serveDurableObject<\r\n TO_ACCEPTOR extends readonly ActionDomain<any>[],\r\n TO_CONNECTOR extends readonly ActionDomain<any>[],\r\n TApp,\r\n>(\r\n ctx: IDurableObjectContext,\r\n channel: IActionChannel<TO_ACCEPTOR, TO_CONNECTOR>,\r\n options: TServeDurableObjectOptions<TO_ACCEPTOR, TApp> & {\r\n connectionState: IServeConnectionStateOptions<TApp>;\r\n },\r\n): TDurableObjectChannelServer<TApp> & {\r\n connections: ConnectionStateStore<IDurableObjectWebSocket, TApp>;\r\n};\r\nexport function serveDurableObject<\r\n TO_ACCEPTOR extends readonly ActionDomain<any>[] = readonly ActionDomain<any>[],\r\n TO_CONNECTOR extends readonly ActionDomain<any>[] = readonly ActionDomain<any>[],\r\n TApp = unknown,\r\n>(\r\n ctx: IDurableObjectContext,\r\n channel: IActionChannel<TO_ACCEPTOR, TO_CONNECTOR>,\r\n options: TServeDurableObjectOptions<TO_ACCEPTOR, TApp>,\r\n): TDurableObjectChannelServer<TApp>;\r\nexport function serveDurableObject(\r\n ctx: IDurableObjectContext,\r\n channel: IActionChannel<readonly ActionDomain<any>[], readonly ActionDomain<any>[]>,\r\n options: TServeDurableObjectOptions<readonly ActionDomain<any>[], any>,\r\n): TDurableObjectChannelServer<any> {\r\n const { runtime, keyPrefix, httpFallback, secure, ...serveOptions } = options;\r\n const host = cloudflareDurableObjectHost(ctx, { keyPrefix, httpFallback, secure });\r\n return serveHost(runtime, channel, host, serveOptions);\r\n}\r\n\r\n/** The slice of a Durable Object stub {@link forwardExchangeToDurableObject} calls — `env.NS.get(id)`. */\r\nexport interface IDurableObjectStub {\r\n fetch(request: Request): Promise<Response>;\r\n}\r\n\r\nexport interface IForwardExchangeToDurableObjectOptions {\r\n /**\r\n * CORS headers for the edge-answered `OPTIONS` preflight (defaults to the permissive `*` set; `false`\r\n * attaches none). Only the preflight is answered here — every other response comes straight from the DO\r\n * (whose `serveDurableObject` applies its own CORS), so the two should agree.\r\n */\r\n cors?: Record<string, string> | false;\r\n}\r\n\r\n/** Permissive defaults, matching `serveDurableObject`'s HTTP fallback. */\r\nconst DEFAULT_FORWARD_CORS: Record<string, string> = {\r\n \"Access-Control-Allow-Origin\": \"*\",\r\n \"Access-Control-Allow-Methods\": \"GET, POST, OPTIONS\",\r\n \"Access-Control-Allow-Headers\": \"Content-Type\",\r\n \"Access-Control-Max-Age\": \"86400\",\r\n};\r\n\r\n/**\r\n * Fan a Worker's incoming action request out to a *per-id* Durable Object that serves the exchange itself.\r\n *\r\n * A secure exchange body is opaque to the Worker (handshake / encrypted frames), so the DO it belongs to\r\n * has to be chosen from the **URL**, not the body. This picks the stub with your `pickStub` and forwards\r\n * the request to its `fetch` (where `serveDurableObject` serves the exchange), while answering the CORS\r\n * `OPTIONS` preflight *at the edge* so a per-id DO is never woken (or billed) just to reply to a preflight:\r\n * ```ts\r\n * // wrangler: a Durable Object namespace `BRIDGE`, each instance one bridge serving `bridgeChannel`.\r\n * export default {\r\n * fetch(request: Request, env: Env) {\r\n * return forwardExchangeToDurableObject(request, (req, url) => {\r\n * const bridgeId = url.pathname.split(\"/\")[2]; // e.g. /bridge/:id/action\r\n * return env.BRIDGE.get(env.BRIDGE.idFromName(bridgeId));\r\n * });\r\n * },\r\n * };\r\n *\r\n * // …and in the Durable Object, serve the secure exchange (+ a WS upgrade) as usual:\r\n * // fetch(request) { return this.server.fetch(request); }\r\n * // where this.server = serveDurableObject(this.ctx, bridgeChannel, { runtime, httpFallback: \"secure\" });\r\n * ```\r\n * The matching connector points its `httpCarrier` at `/bridge/:id/action`. `pickStub` may be async (e.g.\r\n * to look an id up first); it receives the parsed {@link URL} alongside the request.\r\n */\r\nexport function forwardExchangeToDurableObject(\r\n request: Request,\r\n pickStub: (request: Request, url: URL) => IDurableObjectStub | Promise<IDurableObjectStub>,\r\n options: IForwardExchangeToDurableObjectOptions = {},\r\n): Promise<Response> {\r\n if (request.method === \"OPTIONS\") {\r\n const headers = options.cors === false ? undefined : (options.cors ?? DEFAULT_FORWARD_CORS);\r\n return Promise.resolve(new Response(null, { status: 204, headers }));\r\n }\r\n const url = new URL(request.url);\r\n return Promise.resolve(pickStub(request, url)).then((stub) => stub.fetch(request));\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAuHA,SAAgB,uBACd,KACA,UAA0C,CAAC,GACM;CACjD,OAAO,kBAA2C;EAChD,QAAQ,QAAQ;EAChB,OAAO,IAAI,UAAU,GAAG,KAAK,KAAK;EAClC,eAAe;GACb,MAAM,OAAO,IAAI,cAAc;GAC/B,MAAM,SAAS,KAAK;GACpB,MAAM,SAAS,KAAK;GAEpB,IAAI,gBAAgB,MAAM;GAC1B,OAAO,IAAI,SAAS,MAAM;IAAE,QAAQ;IAAK,WAAW;GAAO,CAAC;EAC9D;EAEA,iBAAiB;GACf,sBAAsB,IAAI,cAAc;GACxC,OAAO,OAAO,GAAG,sBAAsB;GACvC,QAAQ,IAAI,UAAU,GAAG,oBAAoB,KAAK;EACpD;CACF,CAAC;AACH;;;;;AAWA,SAAgB,qBACd,KACA,UAAwC,CAAC,GACzB;CAChB,OAAO,kCAAkC;EACvC,sBAAsB,IAAI;EAC1B,WAAW,QAAQ;CACrB,CAAC;AACH;;;;;;;AAqBA,SAAgB,4BACd,KACA,UAA+C,CAAC,GACF;CAC9C,MAAM,eAAe,QAAQ,gBAAgB;CAC7C,MAAM,WAAwD,CAC5D,uBAAuB,KAAK,EAAE,QAAQ,QAAQ,OAAO,CAAC,CACxD;CACA,IAAI,iBAAiB,OACnB,SAAS,KAAK,oBAAoB,EAAE,QAAQ,iBAAiB,SAAS,CAAC,CAAC;CAG1E,OAAO;EACL;EACA,SAAS,qBAAqB,KAAK,EAAE,WAAW,QAAQ,UAAU,CAAC;EACnE,gBAAgB;GAEd,IAAI,yBAAyB,IAAI,6BAA6B,QAAQ,MAAM,CAAC;EAC/E;CACF;AACF;AAmDA,SAAgB,mBACd,KACA,SACA,SACkC;CAClC,MAAM,EAAE,SAAS,WAAW,cAAc,QAAQ,GAAG,iBAAiB;CAEtE,OAAO,UAAU,SAAS,SADb,4BAA4B,KAAK;EAAE;EAAW;EAAc;CAAO,CAC1C,GAAG,YAAY;AACvD;;AAiBA,MAAM,uBAA+C;CACnD,+BAA+B;CAC/B,gCAAgC;CAChC,gCAAgC;CAChC,0BAA0B;AAC5B;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,SAAgB,+BACd,SACA,UACA,UAAkD,CAAC,GAChC;CACnB,IAAI,QAAQ,WAAW,WAAW;EAChC,MAAM,UAAU,QAAQ,SAAS,QAAQ,KAAA,IAAa,QAAQ,QAAQ;EACtE,OAAO,QAAQ,QAAQ,IAAI,SAAS,MAAM;GAAE,QAAQ;GAAK;EAAQ,CAAC,CAAC;CACrE;CACA,MAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;CAC/B,OAAO,QAAQ,QAAQ,SAAS,SAAS,GAAG,CAAC,CAAC,CAAC,MAAM,SAAS,KAAK,MAAM,OAAO,CAAC;AACnF"}
@@ -1,4 +1,4 @@
1
- import { Ar as TInferOutputFromSchema, Cr as IActionDomain, Fr as TInferActionError, Nr as ActionSchema, d as ActionCore, kr as TInferInputFromSchema } from "../AcceptorHandler-CxD0c1BE.cjs";
1
+ import { Lr as TInferInputFromSchema, Rr as TInferOutputFromSchema, Ur as TInferActionError, Vr as ActionSchema, d as ActionCore, jr as IActionDomain } from "../AcceptorHandler-CxPfZtIl.cjs";
2
2
  import { QueryKey, UseMutationOptions, UseMutationResult, UseQueryOptions, UseQueryResult } from "@tanstack/react-query";
3
3
 
4
4
  //#region src/react-query/hooks/useActionMutation.d.ts
@@ -1,4 +1,4 @@
1
- import { Ar as TInferOutputFromSchema, Cr as IActionDomain, Fr as TInferActionError, Nr as ActionSchema, d as ActionCore, kr as TInferInputFromSchema } from "../AcceptorHandler-11-QMdx2.mjs";
1
+ import { Lr as TInferInputFromSchema, Rr as TInferOutputFromSchema, Ur as TInferActionError, Vr as ActionSchema, d as ActionCore, jr as IActionDomain } from "../AcceptorHandler-BizUtq4u.mjs";
2
2
  import { QueryKey, UseMutationOptions, UseMutationResult, UseQueryOptions, UseQueryResult } from "@tanstack/react-query";
3
3
 
4
4
  //#region src/react-query/hooks/useActionMutation.d.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nice-code/action",
3
- "version": "0.24.0",
3
+ "version": "0.25.0",
4
4
  "main": "./build/index.cjs",
5
5
  "module": "./build/index.mjs",
6
6
  "types": "./build/index.d.cts",
@@ -97,9 +97,9 @@
97
97
  },
98
98
  "type": "module",
99
99
  "dependencies": {
100
- "@nice-code/common-errors": "0.24.0",
101
- "@nice-code/error": "0.24.0",
102
- "@nice-code/util": "0.24.0",
100
+ "@nice-code/common-errors": "0.25.0",
101
+ "@nice-code/error": "0.25.0",
102
+ "@nice-code/util": "0.25.0",
103
103
  "@standard-schema/spec": "^1.1.0",
104
104
  "@tanstack/react-virtual": "^3.13.26",
105
105
  "http-status-codes": "^2.3.0",