@firtoz/websocket-do 13.0.0 → 13.0.2

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.
@@ -9,6 +9,14 @@ type BaseWebSocketDOOptions<TSession extends BaseSession<any, any, any, any>, TE
9
9
  createSession: (ctx: Context<{
10
10
  Bindings: TEnv;
11
11
  }> | undefined, websocket: WebSocket) => TSession | Promise<TSession>;
12
+ /**
13
+ * If set, called on the WebSocketPair **server** socket before
14
+ * {@link DurableObjectState#acceptWebSocket}. Use `{ allowHalfOpen: true }` when you need
15
+ * to coordinate close independently (e.g. proxying). Omit for the usual hibernation-only path
16
+ * (no `WebSocket#accept` before `acceptWebSocket`), which matches
17
+ * [Durable Object WebSocket examples](https://developers.cloudflare.com/durable-objects/best-practices/websockets/).
18
+ */
19
+ pairServerWebSocketAcceptOptions?: WebSocketAcceptOptions;
12
20
  };
13
21
  declare abstract class BaseWebSocketDO<TSession extends BaseSession<any, any, any, any> = BaseSession<any, any, any, any>, TEnv extends SessionEnv<TSession> = SessionEnv<TSession>> extends DurableObject<TEnv> implements DOWithHonoApp {
14
22
  #private;
@@ -34,7 +42,7 @@ declare abstract class BaseWebSocketDO<TSession extends BaseSession<any, any, an
34
42
  Bindings: TEnv;
35
43
  }>, ws: WebSocket): Promise<void>;
36
44
  webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): Promise<void>;
37
- webSocketClose(ws: WebSocket, _code: number, _reason: string, _wasClean: boolean): Promise<void>;
45
+ webSocketClose(ws: WebSocket, code: number, reason: string, _wasClean: boolean): Promise<void>;
38
46
  webSocketError(ws: WebSocket, error: unknown): Promise<void>;
39
47
  fetch(request: Request): Response | Promise<Response>;
40
48
  }
@@ -1,4 +1,4 @@
1
- export { BaseWebSocketDO } from './chunk-XFB6C3NZ.js';
1
+ export { BaseWebSocketDO } from './chunk-E4576ZE6.js';
2
2
  import './chunk-NOUFNU2O.js';
3
3
  //# sourceMappingURL=BaseWebSocketDO.js.map
4
4
  //# sourceMappingURL=BaseWebSocketDO.js.map
@@ -16,6 +16,11 @@ type StandardSchemaWebSocketDOOptions<TSession extends StandardSchemaSession<any
16
16
  createStandardSchemaSession: (ctx: Context<{
17
17
  Bindings: TEnv;
18
18
  }> | undefined, websocket: WebSocket, options: StandardSchemaSessionOptions<TClientMessage, TServerMessage>) => TSession | Promise<TSession>;
19
+ /**
20
+ * Optional `WebSocket#accept` options for the server side of the `WebSocketPair` (see
21
+ * `pairServerWebSocketAcceptOptions` on `BaseWebSocketDO` constructor options).
22
+ */
23
+ pairServerWebSocketAcceptOptions?: WebSocketAcceptOptions;
19
24
  };
20
25
  declare abstract class StandardSchemaWebSocketDO<TSession extends StandardSchemaSession<any, any, any, any>, TClientMessage extends SessionClientMessage<TSession> = SessionClientMessage<TSession>, TServerMessage extends SessionServerMessage<TSession> = SessionServerMessage<TSession>, TEnv extends SessionEnv<TSession> = SessionEnv<TSession>> extends BaseWebSocketDO<TSession, TEnv> {
21
26
  protected readonly standardSchemaSessionOptions: StandardSchemaSessionOptionsOrFactory<TClientMessage, TServerMessage, TEnv>;
@@ -1,5 +1,5 @@
1
- export { StandardSchemaWebSocketDO } from './chunk-ULGH6X42.js';
2
- import './chunk-XFB6C3NZ.js';
1
+ export { StandardSchemaWebSocketDO } from './chunk-J275SVBA.js';
2
+ import './chunk-E4576ZE6.js';
3
3
  import './chunk-NOUFNU2O.js';
4
4
  //# sourceMappingURL=StandardSchemaWebSocketDO.js.map
5
5
  //# sourceMappingURL=StandardSchemaWebSocketDO.js.map
@@ -53,6 +53,10 @@ var BaseWebSocketDO = class extends DurableObject {
53
53
  );
54
54
  }
55
55
  async handleSession(ctx, ws) {
56
+ const acceptOpts = this.options.pairServerWebSocketAcceptOptions;
57
+ if (acceptOpts !== void 0) {
58
+ ws.accept(acceptOpts);
59
+ }
56
60
  this.ctx.acceptWebSocket(ws);
57
61
  try {
58
62
  const session = await this.options.createSession(ctx, ws);
@@ -82,7 +86,7 @@ var BaseWebSocketDO = class extends DurableObject {
82
86
  console.error(`Error during session message: ${error}`);
83
87
  }
84
88
  }
85
- async webSocketClose(ws, _code, _reason, _wasClean) {
89
+ async webSocketClose(ws, code, reason, _wasClean) {
86
90
  const session = this.sessions.get(ws);
87
91
  if (!session) return;
88
92
  try {
@@ -90,17 +94,13 @@ var BaseWebSocketDO = class extends DurableObject {
90
94
  } catch (error) {
91
95
  console.error(`Error during session close: ${error}`);
92
96
  } finally {
93
- if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CLOSING) {
94
- ws.close(1e3, "Normal closure");
95
- }
97
+ ws.close(code, reason);
96
98
  }
97
99
  }
98
100
  async webSocketError(ws, error) {
99
101
  const session = this.sessions.get(ws);
100
102
  if (!session) {
101
- if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CLOSING) {
102
- ws.close(1011, "Error during session setup.");
103
- }
103
+ ws.close(1011, "Error during session setup.");
104
104
  return;
105
105
  }
106
106
  console.error(`Error for session: ${error}`);
@@ -109,9 +109,7 @@ var BaseWebSocketDO = class extends DurableObject {
109
109
  } catch (error2) {
110
110
  console.error(`Error during session close: ${error2}`);
111
111
  } finally {
112
- if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CLOSING) {
113
- ws.close(1011, "Error during session.");
114
- }
112
+ ws.close(1011, "Error during session.");
115
113
  }
116
114
  }
117
115
  fetch(request) {
@@ -130,5 +128,5 @@ handleClose_fn = async function(session) {
130
128
  };
131
129
 
132
130
  export { BaseWebSocketDO };
133
- //# sourceMappingURL=chunk-XFB6C3NZ.js.map
134
- //# sourceMappingURL=chunk-XFB6C3NZ.js.map
131
+ //# sourceMappingURL=chunk-E4576ZE6.js.map
132
+ //# sourceMappingURL=chunk-E4576ZE6.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/BaseWebSocketDO.ts"],"names":["error"],"mappings":";;;;AAAA,IAAA,0BAAA,EAAA,cAAA;AA4BO,IAAe,eAAA,GAAf,cAcE,aAAA,CAET;AAAA,EAIC,WAAA,CACC,GAAA,EACA,GAAA,EACiB,OAAA,EAChB;AACD,IAAA,KAAA,CAAM,KAAK,GAAG,CAAA;AAFG,IAAA,IAAA,CAAA,OAAA,GAAA,OAAA;AAvBZ,IAAA,YAAA,CAAA,IAAA,EAAA,0BAAA,CAAA;AAiBN,IAAA,IAAA,CAAmB,QAAA,uBAAe,GAAA,EAAyB;AAU1D,IAAA,IAAA,CAAK,GAAA,CAAI,sBAAsB,YAAY;AAC1C,MAAA,MAAM,UAAA,GAAa,IAAA,CAAK,GAAA,CAAI,aAAA,EAAc;AAC1C,MAAA,MAAM,OAAA,CAAQ,GAAA;AAAA,QACb,UAAA,CAAW,GAAA,CAAI,OAAO,SAAA,KAAc;AACnC,UAAA,IAAI;AAGH,YAAA,MAAM,OAAA,GAAU,MAAM,OAAA,CAAQ,aAAA,CAAc,QAAW,SAAS,CAAA;AAChE,YAAA,OAAA,CAAQ,MAAA,EAAO;AACf,YAAA,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,SAAA,EAAW,OAAO,CAAA;AAAA,UACrC,SAAS,KAAA,EAAO;AACf,YAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,4BAAA,EAA+B,KAAK,CAAA,CAAE,CAAA;AACpD,YAAA,MAAM,IAAA,CAAK,cAAA,CAAe,SAAA,EAAW,KAAK,CAAA;AAAA,UAC3C;AAAA,QACD,CAAC;AAAA,OACF;AAAA,IACD,CAAC,CAAA;AAAA,EACF;AAAA,EAEU,UAAA,GAAa;AACtB,IAAA,OAAO,IAAI,MAAyB,CAAE,GAAA;AAAA,MACrC,YAAA;AAAA,MACA,OAAO,GAAA,KAA2B;AACjC,QAAA,MAAM,EAAE,KAAI,GAAI,GAAA;AAChB,QAAA,IAAI,GAAA,CAAI,MAAA,CAAO,SAAS,CAAA,KAAM,WAAA,EAAa;AAC1C,UAAA,OAAA,CAAQ,MAAM,oBAAoB,CAAA;AAClC,UAAA,OAAO,GAAA,CAAI,IAAA,CAAK,oBAAA,EAAsB,GAAG,CAAA;AAAA,QAC1C;AAEA,QAAA,MAAM,CAAC,QAAQ,MAAM,CAAA,GAAI,OAAO,MAAA,CAAO,IAAI,eAAe,CAAA;AAK1D,QAAA,IAAI;AACH,UAAA,MAAM,IAAA,CAAK,aAAA,CAAc,GAAA,EAAK,MAAM,CAAA;AACpC,UAAA,OAAO,IAAI,SAAS,IAAA,EAAM,EAAE,QAAQ,GAAA,EAAK,SAAA,EAAW,QAAQ,CAAA;AAAA,QAC7D,SAAS,KAAA,EAAO;AACf,UAAA,OAAA,CAAQ,MAAM,KAAK,CAAA;AACnB,UAAA,MAAA,CAAO,MAAA,EAAO;AACd,UAAA,MAAA,CAAO,IAAA;AAAA,YACN,KAAK,SAAA,CAAU;AAAA,cACd,KAAA,EAAO;AAAA,aACP;AAAA,WACF;AACA,UAAA,MAAA,CAAO,KAAA,CAAM,MAAM,0CAA0C,CAAA;AAC7D,UAAA,OAAO,IAAI,SAAS,IAAA,EAAM,EAAE,QAAQ,GAAA,EAAK,SAAA,EAAW,QAAQ,CAAA;AAAA,QAC7D;AAAA,MACD;AAAA,KACD;AAAA,EACD;AAAA,EAEA,MAAM,aAAA,CACL,GAAA,EACA,EAAA,EACgB;AAChB,IAAA,MAAM,UAAA,GAAa,KAAK,OAAA,CAAQ,gCAAA;AAChC,IAAA,IAAI,eAAe,MAAA,EAAW;AAC7B,MAAA,EAAA,CAAG,OAAO,UAAU,CAAA;AAAA,IACrB;AACA,IAAA,IAAA,CAAK,GAAA,CAAI,gBAAgB,EAAE,CAAA;AAC3B,IAAA,IAAI;AACH,MAAA,MAAM,UAAU,MAAM,IAAA,CAAK,OAAA,CAAQ,aAAA,CAAc,KAAK,EAAE,CAAA;AACxD,MAAA,OAAA,CAAQ,WAAW,GAAG,CAAA;AACtB,MAAA,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,EAAA,EAAI,OAAO,CAAA;AAAA,IAC9B,SAAS,KAAA,EAAO;AACf,MAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,4BAAA,EAA+B,KAAK,CAAA,CAAE,CAAA;AACpD,MAAA,MAAM,IAAA,CAAK,cAAA,CAAe,EAAA,EAAI,KAAK,CAAA;AAAA,IACpC;AAAA,EACD;AAAA,EAEA,MAAe,gBAAA,CACd,EAAA,EACA,OAAA,EACgB;AAChB,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,EAAE,CAAA;AACpC,IAAA,IAAI,CAAC,OAAA,EAAS;AAEd,IAAA,IAAI;AACH,MAAA,IAAI,mBAAmB,WAAA,EAAa;AACnC,QAAA,MAAM,OAAA,CAAQ,oBAAoB,OAAO,CAAA;AACzC,QAAA;AAAA,MACD;AAEA,MAAA,MAAM,iBAAA,GAAoB,OAAA;AAQ1B,MAAA,IAAI,kBAAkB,gBAAA,EAAkB;AACvC,QAAA,MAAM,iBAAA,CAAkB,iBAAiB,OAAO,CAAA;AAChD,QAAA;AAAA,MACD;AAEA,MAAA,MAAM,MAAA,GAAS,IAAA,CAAK,KAAA,CAAM,OAAO,CAAA;AACjC,MAAA,MAAM,OAAA,CAAQ,cAAc,MAAM,CAAA;AAAA,IACnC,SAAS,KAAA,EAAO;AACf,MAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,8BAAA,EAAiC,KAAK,CAAA,CAAE,CAAA;AAAA,IAGvD;AAAA,EACD;AAAA,EAEA,MAAe,cAAA,CACd,EAAA,EACA,IAAA,EACA,QACA,SAAA,EACC;AACD,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,EAAE,CAAA;AACpC,IAAA,IAAI,CAAC,OAAA,EAAS;AAEd,IAAA,IAAI;AACH,MAAA,MAAM,eAAA,CAAA,IAAA,EAAK,4CAAL,IAAA,CAAA,IAAA,EAAkB,OAAA,CAAA;AAAA,IACzB,SAAS,KAAA,EAAO;AACf,MAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,4BAAA,EAA+B,KAAK,CAAA,CAAE,CAAA;AAAA,IACrD,CAAA,SAAE;AAID,MAAA,EAAA,CAAG,KAAA,CAAM,MAAM,MAAM,CAAA;AAAA,IACtB;AAAA,EACD;AAAA,EAEA,MAAe,cAAA,CAAe,EAAA,EAAe,KAAA,EAAgB;AAC5D,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,EAAE,CAAA;AACpC,IAAA,IAAI,CAAC,OAAA,EAAS;AAGb,MAAA,EAAA,CAAG,KAAA,CAAM,MAAM,6BAA6B,CAAA;AAC5C,MAAA;AAAA,IACD;AAEA,IAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,mBAAA,EAAsB,KAAK,CAAA,CAAE,CAAA;AAC3C,IAAA,IAAI;AACH,MAAA,MAAM,eAAA,CAAA,IAAA,EAAK,4CAAL,IAAA,CAAA,IAAA,EAAkB,OAAA,CAAA;AAAA,IACzB,SAASA,MAAAA,EAAO;AACf,MAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,4BAAA,EAA+BA,MAAK,CAAA,CAAE,CAAA;AAAA,IACrD,CAAA,SAAE;AACD,MAAA,EAAA,CAAG,KAAA,CAAM,MAAM,uBAAuB,CAAA;AAAA,IACvC;AAAA,EACD;AAAA,EAYS,MAAM,OAAA,EAAgD;AAC9D,IAAA,OAAO,IAAA,CAAK,GAAA,CAAI,KAAA,CAAM,OAAA,EAAS,KAAK,GAAG,CAAA;AAAA,EACxC;AACD;AA1LO,0BAAA,GAAA,IAAA,OAAA,EAAA;AA6KA,cAAA,GAAY,eAAC,OAAA,EAAmB;AACrC,EAAA,IAAI;AACH,IAAA,MAAM,QAAQ,WAAA,EAAY;AAAA,EAC3B,SAAS,KAAA,EAAO;AACf,IAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,4BAAA,EAA+B,KAAK,CAAA,CAAE,CAAA;AAAA,EACrD,CAAA,SAAE;AACD,IAAA,IAAA,CAAK,QAAA,CAAS,MAAA,CAAO,OAAA,CAAQ,SAAS,CAAA;AAAA,EACvC;AACD,CAAA","file":"chunk-E4576ZE6.js","sourcesContent":["import { DurableObject } from \"cloudflare:workers\";\nimport type { DOWithHonoApp } from \"@firtoz/hono-fetcher/honoDoFetcher\";\nimport { type Context, Hono } from \"hono\";\nimport type {\n\tBaseSession,\n\tSessionClientMessage,\n\tSessionEnv,\n} from \"./BaseSession\";\n\nexport type BaseWebSocketDOOptions<\n\t// biome-ignore lint/suspicious/noExplicitAny: We are using any on purpose to allow any type of session.\n\tTSession extends BaseSession<any, any, any, any>,\n\tTEnv extends SessionEnv<TSession>,\n> = {\n\tcreateSession: (\n\t\tctx: Context<{ Bindings: TEnv }> | undefined,\n\t\twebsocket: WebSocket,\n\t) => TSession | Promise<TSession>;\n\t/**\n\t * If set, called on the WebSocketPair **server** socket before\n\t * {@link DurableObjectState#acceptWebSocket}. Use `{ allowHalfOpen: true }` when you need\n\t * to coordinate close independently (e.g. proxying). Omit for the usual hibernation-only path\n\t * (no `WebSocket#accept` before `acceptWebSocket`), which matches\n\t * [Durable Object WebSocket examples](https://developers.cloudflare.com/durable-objects/best-practices/websockets/).\n\t */\n\tpairServerWebSocketAcceptOptions?: WebSocketAcceptOptions;\n};\n\nexport abstract class BaseWebSocketDO<\n\t\t// biome-ignore lint/suspicious/noExplicitAny: We are using any on purpose to allow any type of session.\n\t\tTSession extends BaseSession<any, any, any, any> = BaseSession<\n\t\t\t// biome-ignore lint/suspicious/noExplicitAny: We are using any on purpose to allow any type of session.\n\t\t\tany,\n\t\t\t// biome-ignore lint/suspicious/noExplicitAny: We are using any on purpose to allow any type of session.\n\t\t\tany,\n\t\t\t// biome-ignore lint/suspicious/noExplicitAny: We are using any on purpose to allow any type of session.\n\t\t\tany,\n\t\t\t// biome-ignore lint/suspicious/noExplicitAny: We are using any on purpose to allow any type of session.\n\t\t\tany\n\t\t>,\n\t\tTEnv extends SessionEnv<TSession> = SessionEnv<TSession>,\n\t>\n\textends DurableObject<TEnv>\n\timplements DOWithHonoApp\n{\n\tprotected readonly sessions = new Map<WebSocket, TSession>();\n\tabstract readonly app: Hono<{ Bindings: TEnv }>;\n\n\tconstructor(\n\t\tctx: DurableObjectState,\n\t\tenv: TEnv,\n\t\tprivate readonly options: BaseWebSocketDOOptions<TSession, TEnv>,\n\t) {\n\t\tsuper(ctx, env);\n\n\t\tthis.ctx.blockConcurrencyWhile(async () => {\n\t\t\tconst websockets = this.ctx.getWebSockets();\n\t\t\tawait Promise.all(\n\t\t\t\twebsockets.map(async (websocket) => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\t// For resumed sessions, we don't have a Hono context\n\t\t\t\t\t\t// Pass undefined and let implementers handle it\n\t\t\t\t\t\tconst session = await options.createSession(undefined, websocket);\n\t\t\t\t\t\tsession.resume();\n\t\t\t\t\t\tthis.sessions.set(websocket, session);\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tconsole.error(`Error during session setup: ${error}`);\n\t\t\t\t\t\tawait this.webSocketError(websocket, error);\n\t\t\t\t\t}\n\t\t\t\t}),\n\t\t\t);\n\t\t});\n\t}\n\n\tprotected getBaseApp() {\n\t\treturn new Hono<{ Bindings: TEnv }>().get(\n\t\t\t\"/websocket\",\n\t\t\tasync (ctx): Promise<Response> => {\n\t\t\t\tconst { req } = ctx;\n\t\t\t\tif (req.header(\"Upgrade\") !== \"websocket\") {\n\t\t\t\t\tconsole.error(\"Expected websocket\");\n\t\t\t\t\treturn ctx.text(\"Expected websocket\", 400);\n\t\t\t\t}\n\n\t\t\t\tconst [client, server] = Object.values(new WebSocketPair()) as [\n\t\t\t\t\tWebSocket,\n\t\t\t\t\tWebSocket,\n\t\t\t\t];\n\n\t\t\t\ttry {\n\t\t\t\t\tawait this.handleSession(ctx, server);\n\t\t\t\t\treturn new Response(null, { status: 101, webSocket: client });\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconsole.error(error);\n\t\t\t\t\tclient.accept();\n\t\t\t\t\tclient.send(\n\t\t\t\t\t\tJSON.stringify({\n\t\t\t\t\t\t\terror: \"Uncaught exception during session setup.\",\n\t\t\t\t\t\t}),\n\t\t\t\t\t);\n\t\t\t\t\tclient.close(1011, \"Uncaught exception during session setup.\");\n\t\t\t\t\treturn new Response(null, { status: 101, webSocket: client });\n\t\t\t\t}\n\t\t\t},\n\t\t);\n\t}\n\n\tasync handleSession(\n\t\tctx: Context<{ Bindings: TEnv }>,\n\t\tws: WebSocket,\n\t): Promise<void> {\n\t\tconst acceptOpts = this.options.pairServerWebSocketAcceptOptions;\n\t\tif (acceptOpts !== undefined) {\n\t\t\tws.accept(acceptOpts);\n\t\t}\n\t\tthis.ctx.acceptWebSocket(ws);\n\t\ttry {\n\t\t\tconst session = await this.options.createSession(ctx, ws);\n\t\t\tsession.startFresh(ctx);\n\t\t\tthis.sessions.set(ws, session);\n\t\t} catch (error) {\n\t\t\tconsole.error(`Error during session setup: ${error}`);\n\t\t\tawait this.webSocketError(ws, error);\n\t\t}\n\t}\n\n\toverride async webSocketMessage(\n\t\tws: WebSocket,\n\t\tmessage: string | ArrayBuffer,\n\t): Promise<void> {\n\t\tconst session = this.sessions.get(ws);\n\t\tif (!session) return;\n\n\t\ttry {\n\t\t\tif (message instanceof ArrayBuffer) {\n\t\t\t\tawait session.handleBufferMessage(message);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst rawMessageSession = session as BaseSession<\n\t\t\t\tunknown,\n\t\t\t\tunknown,\n\t\t\t\tunknown,\n\t\t\t\tTEnv\n\t\t\t> & {\n\t\t\t\thandleRawMessage?: (rawMessage: string) => Promise<void>;\n\t\t\t};\n\t\t\tif (rawMessageSession.handleRawMessage) {\n\t\t\t\tawait rawMessageSession.handleRawMessage(message);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst parsed = JSON.parse(message) as SessionClientMessage<TSession>;\n\t\t\tawait session.handleMessage(parsed);\n\t\t} catch (error) {\n\t\t\tconsole.error(`Error during session message: ${error}`);\n\t\t\t// Let the implementer decide how to handle errors in their session implementation\n\t\t\t// The session can optionally implement error handling that closes the connection if needed\n\t\t}\n\t}\n\n\toverride async webSocketClose(\n\t\tws: WebSocket,\n\t\tcode: number,\n\t\treason: string,\n\t\t_wasClean: boolean,\n\t) {\n\t\tconst session = this.sessions.get(ws);\n\t\tif (!session) return;\n\n\t\ttry {\n\t\t\tawait this.#handleClose(session);\n\t\t} catch (error) {\n\t\t\tconsole.error(`Error during session close: ${error}`);\n\t\t} finally {\n\t\t\t// Pre–2026-04-07 (manual close reply): required to complete the Close handshake and avoid\n\t\t\t// abnormal client closure. With web_socket_auto_reply_to_close (default on 2026-04-07+),\n\t\t\t// the runtime already replied; `close()` is a no-op if already CLOSED.\n\t\t\tws.close(code, reason);\n\t\t}\n\t}\n\n\toverride async webSocketError(ws: WebSocket, error: unknown) {\n\t\tconst session = this.sessions.get(ws);\n\t\tif (!session) {\n\t\t\t// Idempotent: safe when the socket is already closed (auto close reply) or in CLOSING\n\t\t\t// (legacy manual reply to complete the handshake).\n\t\t\tws.close(1011, \"Error during session setup.\");\n\t\t\treturn;\n\t\t}\n\n\t\tconsole.error(`Error for session: ${error}`);\n\t\ttry {\n\t\t\tawait this.#handleClose(session);\n\t\t} catch (error) {\n\t\t\tconsole.error(`Error during session close: ${error}`);\n\t\t} finally {\n\t\t\tws.close(1011, \"Error during session.\");\n\t\t}\n\t}\n\n\tasync #handleClose(session: TSession) {\n\t\ttry {\n\t\t\tawait session.handleClose();\n\t\t} catch (error) {\n\t\t\tconsole.error(`Error during session close: ${error}`);\n\t\t} finally {\n\t\t\tthis.sessions.delete(session.websocket);\n\t\t}\n\t}\n\n\toverride fetch(request: Request): Response | Promise<Response> {\n\t\treturn this.app.fetch(request, this.env);\n\t}\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { BaseWebSocketDO } from './chunk-XFB6C3NZ.js';
1
+ import { BaseWebSocketDO } from './chunk-E4576ZE6.js';
2
2
 
3
3
  // src/StandardSchemaWebSocketDO.ts
4
4
  var StandardSchemaWebSocketDO = class extends BaseWebSocketDO {
@@ -11,7 +11,8 @@ var StandardSchemaWebSocketDO = class extends BaseWebSocketDO {
11
11
  websocket,
12
12
  schemaOptions
13
13
  );
14
- }
14
+ },
15
+ pairServerWebSocketAcceptOptions: options.pairServerWebSocketAcceptOptions
15
16
  });
16
17
  this.standardSchemaSessionOptions = options.standardSchemaSessionOptions;
17
18
  this.createStandardSchemaSessionFn = options.createStandardSchemaSession;
@@ -19,5 +20,5 @@ var StandardSchemaWebSocketDO = class extends BaseWebSocketDO {
19
20
  };
20
21
 
21
22
  export { StandardSchemaWebSocketDO };
22
- //# sourceMappingURL=chunk-ULGH6X42.js.map
23
- //# sourceMappingURL=chunk-ULGH6X42.js.map
23
+ //# sourceMappingURL=chunk-J275SVBA.js.map
24
+ //# sourceMappingURL=chunk-J275SVBA.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/StandardSchemaWebSocketDO.ts"],"names":["ctx"],"mappings":";;;AA+CO,IAAe,yBAAA,GAAf,cAQG,eAAA,CAAgC;AAAA,EAYzC,WAAA,CACC,GAAA,EACA,GAAA,EACA,OAAA,EAMC;AACD,IAAA,KAAA,CAAM,KAAK,GAAA,EAAK;AAAA,MACf,aAAA,EAAe,CAACA,IAAAA,EAAK,SAAA,KAAc;AAClC,QAAA,MAAM,aAAA,GACL,OAAO,OAAA,CAAQ,4BAAA,KAAiC,UAAA,GAC7C,QAAQ,4BAAA,CAA6BA,IAAAA,EAAK,SAAS,CAAA,GACnD,OAAA,CAAQ,4BAAA;AAEZ,QAAA,OAAO,OAAA,CAAQ,2BAAA;AAAA,UACdA,IAAAA;AAAA,UACA,SAAA;AAAA,UACA;AAAA,SACD;AAAA,MACD,CAAA;AAAA,MACA,kCACC,OAAA,CAAQ;AAAA,KACT,CAAA;AACD,IAAA,IAAA,CAAK,+BAA+B,OAAA,CAAQ,4BAAA;AAC5C,IAAA,IAAA,CAAK,gCAAgC,OAAA,CAAQ,2BAAA;AAAA,EAC9C;AACD","file":"chunk-J275SVBA.js","sourcesContent":["import type { Context } from \"hono\";\nimport type {\n\tSessionClientMessage,\n\tSessionEnv,\n\tSessionServerMessage,\n} from \"./BaseSession\";\nimport { BaseWebSocketDO } from \"./BaseWebSocketDO\";\nimport type {\n\tStandardSchemaSession,\n\tStandardSchemaSessionOptions,\n} from \"./StandardSchemaSession\";\n\nexport type StandardSchemaSessionOptionsOrFactory<\n\tTClientMessage,\n\tTServerMessage,\n\tTEnv extends Cloudflare.Env = Cloudflare.Env,\n> =\n\t| StandardSchemaSessionOptions<TClientMessage, TServerMessage>\n\t| ((\n\t\t\tctx: Context<{ Bindings: TEnv }> | undefined,\n\t\t\twebsocket: WebSocket,\n\t ) => StandardSchemaSessionOptions<TClientMessage, TServerMessage>);\n\nexport type StandardSchemaWebSocketDOOptions<\n\t// biome-ignore lint/suspicious/noExplicitAny: We are using any on purpose to allow any type of session.\n\tTSession extends StandardSchemaSession<any, any, any, any>,\n\tTClientMessage,\n\tTServerMessage,\n\tTEnv extends SessionEnv<TSession>,\n> = {\n\tstandardSchemaSessionOptions: StandardSchemaSessionOptionsOrFactory<\n\t\tTClientMessage,\n\t\tTServerMessage,\n\t\tTEnv\n\t>;\n\tcreateStandardSchemaSession: (\n\t\tctx: Context<{ Bindings: TEnv }> | undefined,\n\t\twebsocket: WebSocket,\n\t\toptions: StandardSchemaSessionOptions<TClientMessage, TServerMessage>,\n\t) => TSession | Promise<TSession>;\n\t/**\n\t * Optional `WebSocket#accept` options for the server side of the `WebSocketPair` (see\n\t * `pairServerWebSocketAcceptOptions` on `BaseWebSocketDO` constructor options).\n\t */\n\tpairServerWebSocketAcceptOptions?: WebSocketAcceptOptions;\n};\n\nexport abstract class StandardSchemaWebSocketDO<\n\t// biome-ignore lint/suspicious/noExplicitAny: We are using any on purpose to allow any type of session.\n\tTSession extends StandardSchemaSession<any, any, any, any>,\n\tTClientMessage extends\n\t\tSessionClientMessage<TSession> = SessionClientMessage<TSession>,\n\tTServerMessage extends\n\t\tSessionServerMessage<TSession> = SessionServerMessage<TSession>,\n\tTEnv extends SessionEnv<TSession> = SessionEnv<TSession>,\n> extends BaseWebSocketDO<TSession, TEnv> {\n\tprotected readonly standardSchemaSessionOptions: StandardSchemaSessionOptionsOrFactory<\n\t\tTClientMessage,\n\t\tTServerMessage,\n\t\tTEnv\n\t>;\n\tprotected readonly createStandardSchemaSessionFn: (\n\t\tctx: Context<{ Bindings: TEnv }> | undefined,\n\t\twebsocket: WebSocket,\n\t\toptions: StandardSchemaSessionOptions<TClientMessage, TServerMessage>,\n\t) => TSession | Promise<TSession>;\n\n\tconstructor(\n\t\tctx: DurableObjectState,\n\t\tenv: TEnv,\n\t\toptions: StandardSchemaWebSocketDOOptions<\n\t\t\tTSession,\n\t\t\tTClientMessage,\n\t\t\tTServerMessage,\n\t\t\tTEnv\n\t\t>,\n\t) {\n\t\tsuper(ctx, env, {\n\t\t\tcreateSession: (ctx, websocket) => {\n\t\t\t\tconst schemaOptions =\n\t\t\t\t\ttypeof options.standardSchemaSessionOptions === \"function\"\n\t\t\t\t\t\t? options.standardSchemaSessionOptions(ctx, websocket)\n\t\t\t\t\t\t: options.standardSchemaSessionOptions;\n\n\t\t\t\treturn options.createStandardSchemaSession(\n\t\t\t\t\tctx,\n\t\t\t\t\twebsocket,\n\t\t\t\t\tschemaOptions,\n\t\t\t\t);\n\t\t\t},\n\t\t\tpairServerWebSocketAcceptOptions:\n\t\t\t\toptions.pairServerWebSocketAcceptOptions,\n\t\t});\n\t\tthis.standardSchemaSessionOptions = options.standardSchemaSessionOptions;\n\t\tthis.createStandardSchemaSessionFn = options.createStandardSchemaSession;\n\t}\n}\n"]}
package/dist/index.js CHANGED
@@ -2,8 +2,8 @@ export { StandardSchemaSession } from './chunk-53MFRNQS.js';
2
2
  export { standardSchemaMsgpack } from './chunk-QMGIRIHJ.js';
3
3
  export { BaseSession } from './chunk-3C77OSOD.js';
4
4
  export { StandardSchemaWebSocketClient } from './chunk-3LWVEY3R.js';
5
- export { StandardSchemaWebSocketDO } from './chunk-ULGH6X42.js';
6
- export { BaseWebSocketDO } from './chunk-XFB6C3NZ.js';
5
+ export { StandardSchemaWebSocketDO } from './chunk-J275SVBA.js';
6
+ export { BaseWebSocketDO } from './chunk-E4576ZE6.js';
7
7
  export { WebsocketWrapper } from './chunk-KCPOB32E.js';
8
8
  export { parseStandardSchema } from './chunk-CAX4POIL.js';
9
9
  import './chunk-NOUFNU2O.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@firtoz/websocket-do",
3
- "version": "13.0.0",
3
+ "version": "13.0.2",
4
4
  "description": "Type-safe WebSocket session management for Cloudflare Durable Objects with Hono integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -62,9 +62,9 @@
62
62
  "url": "https://github.com/firtoz/fullstack-toolkit/issues"
63
63
  },
64
64
  "peerDependencies": {
65
- "@cloudflare/workers-types": "^4.20260329.1",
66
- "@firtoz/hono-fetcher": "^2.7.0",
67
- "hono": "^4.12.9",
65
+ "@cloudflare/workers-types": "^4.20260423.1",
66
+ "@firtoz/hono-fetcher": "^2.7.2",
67
+ "hono": "^4.12.14",
68
68
  "react": ">=18.0.0"
69
69
  },
70
70
  "peerDependenciesMeta": {
@@ -74,7 +74,7 @@
74
74
  },
75
75
  "dependencies": {
76
76
  "@standard-schema/spec": "^1.1.0",
77
- "msgpackr": "^1.11.9"
77
+ "msgpackr": "^1.11.10"
78
78
  },
79
79
  "engines": {
80
80
  "node": ">=18.0.0"
@@ -84,8 +84,8 @@
84
84
  },
85
85
  "devDependencies": {
86
86
  "@types/react": "^19.2.14",
87
- "bun-types": "^1.3.11",
88
- "typescript": "^6.0.2",
87
+ "bun-types": "^1.3.13",
88
+ "typescript": "^6.0.3",
89
89
  "zod": "^4.3.6"
90
90
  }
91
91
  }
@@ -16,6 +16,14 @@ export type BaseWebSocketDOOptions<
16
16
  ctx: Context<{ Bindings: TEnv }> | undefined,
17
17
  websocket: WebSocket,
18
18
  ) => TSession | Promise<TSession>;
19
+ /**
20
+ * If set, called on the WebSocketPair **server** socket before
21
+ * {@link DurableObjectState#acceptWebSocket}. Use `{ allowHalfOpen: true }` when you need
22
+ * to coordinate close independently (e.g. proxying). Omit for the usual hibernation-only path
23
+ * (no `WebSocket#accept` before `acceptWebSocket`), which matches
24
+ * [Durable Object WebSocket examples](https://developers.cloudflare.com/durable-objects/best-practices/websockets/).
25
+ */
26
+ pairServerWebSocketAcceptOptions?: WebSocketAcceptOptions;
19
27
  };
20
28
 
21
29
  export abstract class BaseWebSocketDO<
@@ -101,6 +109,10 @@ export abstract class BaseWebSocketDO<
101
109
  ctx: Context<{ Bindings: TEnv }>,
102
110
  ws: WebSocket,
103
111
  ): Promise<void> {
112
+ const acceptOpts = this.options.pairServerWebSocketAcceptOptions;
113
+ if (acceptOpts !== undefined) {
114
+ ws.accept(acceptOpts);
115
+ }
104
116
  this.ctx.acceptWebSocket(ws);
105
117
  try {
106
118
  const session = await this.options.createSession(ctx, ws);
@@ -149,8 +161,8 @@ export abstract class BaseWebSocketDO<
149
161
 
150
162
  override async webSocketClose(
151
163
  ws: WebSocket,
152
- _code: number,
153
- _reason: string,
164
+ code: number,
165
+ reason: string,
154
166
  _wasClean: boolean,
155
167
  ) {
156
168
  const session = this.sessions.get(ws);
@@ -161,27 +173,19 @@ export abstract class BaseWebSocketDO<
161
173
  } catch (error) {
162
174
  console.error(`Error during session close: ${error}`);
163
175
  } finally {
164
- // Call close() for both OPEN and CLOSING states
165
- // For CLOSING, this can help ensure the WebSocket fully transitions to CLOSED
166
- if (
167
- ws.readyState === WebSocket.OPEN ||
168
- ws.readyState === WebSocket.CLOSING
169
- ) {
170
- ws.close(1000, "Normal closure");
171
- }
176
+ // Pre–2026-04-07 (manual close reply): required to complete the Close handshake and avoid
177
+ // abnormal client closure. With web_socket_auto_reply_to_close (default on 2026-04-07+),
178
+ // the runtime already replied; `close()` is a no-op if already CLOSED.
179
+ ws.close(code, reason);
172
180
  }
173
181
  }
174
182
 
175
183
  override async webSocketError(ws: WebSocket, error: unknown) {
176
184
  const session = this.sessions.get(ws);
177
185
  if (!session) {
178
- // Call close() for both OPEN and CLOSING states
179
- if (
180
- ws.readyState === WebSocket.OPEN ||
181
- ws.readyState === WebSocket.CLOSING
182
- ) {
183
- ws.close(1011, "Error during session setup.");
184
- }
186
+ // Idempotent: safe when the socket is already closed (auto close reply) or in CLOSING
187
+ // (legacy manual reply to complete the handshake).
188
+ ws.close(1011, "Error during session setup.");
185
189
  return;
186
190
  }
187
191
 
@@ -191,13 +195,7 @@ export abstract class BaseWebSocketDO<
191
195
  } catch (error) {
192
196
  console.error(`Error during session close: ${error}`);
193
197
  } finally {
194
- // Call close() for both OPEN and CLOSING states
195
- if (
196
- ws.readyState === WebSocket.OPEN ||
197
- ws.readyState === WebSocket.CLOSING
198
- ) {
199
- ws.close(1011, "Error during session.");
200
- }
198
+ ws.close(1011, "Error during session.");
201
199
  }
202
200
  }
203
201
 
@@ -38,6 +38,11 @@ export type StandardSchemaWebSocketDOOptions<
38
38
  websocket: WebSocket,
39
39
  options: StandardSchemaSessionOptions<TClientMessage, TServerMessage>,
40
40
  ) => TSession | Promise<TSession>;
41
+ /**
42
+ * Optional `WebSocket#accept` options for the server side of the `WebSocketPair` (see
43
+ * `pairServerWebSocketAcceptOptions` on `BaseWebSocketDO` constructor options).
44
+ */
45
+ pairServerWebSocketAcceptOptions?: WebSocketAcceptOptions;
41
46
  };
42
47
 
43
48
  export abstract class StandardSchemaWebSocketDO<
@@ -83,6 +88,8 @@ export abstract class StandardSchemaWebSocketDO<
83
88
  schemaOptions,
84
89
  );
85
90
  },
91
+ pairServerWebSocketAcceptOptions:
92
+ options.pairServerWebSocketAcceptOptions,
86
93
  });
87
94
  this.standardSchemaSessionOptions = options.standardSchemaSessionOptions;
88
95
  this.createStandardSchemaSessionFn = options.createStandardSchemaSession;
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/StandardSchemaWebSocketDO.ts"],"names":["ctx"],"mappings":";;;AA0CO,IAAe,yBAAA,GAAf,cAQG,eAAA,CAAgC;AAAA,EAYzC,WAAA,CACC,GAAA,EACA,GAAA,EACA,OAAA,EAMC;AACD,IAAA,KAAA,CAAM,KAAK,GAAA,EAAK;AAAA,MACf,aAAA,EAAe,CAACA,IAAAA,EAAK,SAAA,KAAc;AAClC,QAAA,MAAM,aAAA,GACL,OAAO,OAAA,CAAQ,4BAAA,KAAiC,UAAA,GAC7C,QAAQ,4BAAA,CAA6BA,IAAAA,EAAK,SAAS,CAAA,GACnD,OAAA,CAAQ,4BAAA;AAEZ,QAAA,OAAO,OAAA,CAAQ,2BAAA;AAAA,UACdA,IAAAA;AAAA,UACA,SAAA;AAAA,UACA;AAAA,SACD;AAAA,MACD;AAAA,KACA,CAAA;AACD,IAAA,IAAA,CAAK,+BAA+B,OAAA,CAAQ,4BAAA;AAC5C,IAAA,IAAA,CAAK,gCAAgC,OAAA,CAAQ,2BAAA;AAAA,EAC9C;AACD","file":"chunk-ULGH6X42.js","sourcesContent":["import type { Context } from \"hono\";\nimport type {\n\tSessionClientMessage,\n\tSessionEnv,\n\tSessionServerMessage,\n} from \"./BaseSession\";\nimport { BaseWebSocketDO } from \"./BaseWebSocketDO\";\nimport type {\n\tStandardSchemaSession,\n\tStandardSchemaSessionOptions,\n} from \"./StandardSchemaSession\";\n\nexport type StandardSchemaSessionOptionsOrFactory<\n\tTClientMessage,\n\tTServerMessage,\n\tTEnv extends Cloudflare.Env = Cloudflare.Env,\n> =\n\t| StandardSchemaSessionOptions<TClientMessage, TServerMessage>\n\t| ((\n\t\t\tctx: Context<{ Bindings: TEnv }> | undefined,\n\t\t\twebsocket: WebSocket,\n\t ) => StandardSchemaSessionOptions<TClientMessage, TServerMessage>);\n\nexport type StandardSchemaWebSocketDOOptions<\n\t// biome-ignore lint/suspicious/noExplicitAny: We are using any on purpose to allow any type of session.\n\tTSession extends StandardSchemaSession<any, any, any, any>,\n\tTClientMessage,\n\tTServerMessage,\n\tTEnv extends SessionEnv<TSession>,\n> = {\n\tstandardSchemaSessionOptions: StandardSchemaSessionOptionsOrFactory<\n\t\tTClientMessage,\n\t\tTServerMessage,\n\t\tTEnv\n\t>;\n\tcreateStandardSchemaSession: (\n\t\tctx: Context<{ Bindings: TEnv }> | undefined,\n\t\twebsocket: WebSocket,\n\t\toptions: StandardSchemaSessionOptions<TClientMessage, TServerMessage>,\n\t) => TSession | Promise<TSession>;\n};\n\nexport abstract class StandardSchemaWebSocketDO<\n\t// biome-ignore lint/suspicious/noExplicitAny: We are using any on purpose to allow any type of session.\n\tTSession extends StandardSchemaSession<any, any, any, any>,\n\tTClientMessage extends\n\t\tSessionClientMessage<TSession> = SessionClientMessage<TSession>,\n\tTServerMessage extends\n\t\tSessionServerMessage<TSession> = SessionServerMessage<TSession>,\n\tTEnv extends SessionEnv<TSession> = SessionEnv<TSession>,\n> extends BaseWebSocketDO<TSession, TEnv> {\n\tprotected readonly standardSchemaSessionOptions: StandardSchemaSessionOptionsOrFactory<\n\t\tTClientMessage,\n\t\tTServerMessage,\n\t\tTEnv\n\t>;\n\tprotected readonly createStandardSchemaSessionFn: (\n\t\tctx: Context<{ Bindings: TEnv }> | undefined,\n\t\twebsocket: WebSocket,\n\t\toptions: StandardSchemaSessionOptions<TClientMessage, TServerMessage>,\n\t) => TSession | Promise<TSession>;\n\n\tconstructor(\n\t\tctx: DurableObjectState,\n\t\tenv: TEnv,\n\t\toptions: StandardSchemaWebSocketDOOptions<\n\t\t\tTSession,\n\t\t\tTClientMessage,\n\t\t\tTServerMessage,\n\t\t\tTEnv\n\t\t>,\n\t) {\n\t\tsuper(ctx, env, {\n\t\t\tcreateSession: (ctx, websocket) => {\n\t\t\t\tconst schemaOptions =\n\t\t\t\t\ttypeof options.standardSchemaSessionOptions === \"function\"\n\t\t\t\t\t\t? options.standardSchemaSessionOptions(ctx, websocket)\n\t\t\t\t\t\t: options.standardSchemaSessionOptions;\n\n\t\t\t\treturn options.createStandardSchemaSession(\n\t\t\t\t\tctx,\n\t\t\t\t\twebsocket,\n\t\t\t\t\tschemaOptions,\n\t\t\t\t);\n\t\t\t},\n\t\t});\n\t\tthis.standardSchemaSessionOptions = options.standardSchemaSessionOptions;\n\t\tthis.createStandardSchemaSessionFn = options.createStandardSchemaSession;\n\t}\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/BaseWebSocketDO.ts"],"names":["error"],"mappings":";;;;AAAA,IAAA,0BAAA,EAAA,cAAA;AAoBO,IAAe,eAAA,GAAf,cAcE,aAAA,CAET;AAAA,EAIC,WAAA,CACC,GAAA,EACA,GAAA,EACiB,OAAA,EAChB;AACD,IAAA,KAAA,CAAM,KAAK,GAAG,CAAA;AAFG,IAAA,IAAA,CAAA,OAAA,GAAA,OAAA;AAvBZ,IAAA,YAAA,CAAA,IAAA,EAAA,0BAAA,CAAA;AAiBN,IAAA,IAAA,CAAmB,QAAA,uBAAe,GAAA,EAAyB;AAU1D,IAAA,IAAA,CAAK,GAAA,CAAI,sBAAsB,YAAY;AAC1C,MAAA,MAAM,UAAA,GAAa,IAAA,CAAK,GAAA,CAAI,aAAA,EAAc;AAC1C,MAAA,MAAM,OAAA,CAAQ,GAAA;AAAA,QACb,UAAA,CAAW,GAAA,CAAI,OAAO,SAAA,KAAc;AACnC,UAAA,IAAI;AAGH,YAAA,MAAM,OAAA,GAAU,MAAM,OAAA,CAAQ,aAAA,CAAc,QAAW,SAAS,CAAA;AAChE,YAAA,OAAA,CAAQ,MAAA,EAAO;AACf,YAAA,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,SAAA,EAAW,OAAO,CAAA;AAAA,UACrC,SAAS,KAAA,EAAO;AACf,YAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,4BAAA,EAA+B,KAAK,CAAA,CAAE,CAAA;AACpD,YAAA,MAAM,IAAA,CAAK,cAAA,CAAe,SAAA,EAAW,KAAK,CAAA;AAAA,UAC3C;AAAA,QACD,CAAC;AAAA,OACF;AAAA,IACD,CAAC,CAAA;AAAA,EACF;AAAA,EAEU,UAAA,GAAa;AACtB,IAAA,OAAO,IAAI,MAAyB,CAAE,GAAA;AAAA,MACrC,YAAA;AAAA,MACA,OAAO,GAAA,KAA2B;AACjC,QAAA,MAAM,EAAE,KAAI,GAAI,GAAA;AAChB,QAAA,IAAI,GAAA,CAAI,MAAA,CAAO,SAAS,CAAA,KAAM,WAAA,EAAa;AAC1C,UAAA,OAAA,CAAQ,MAAM,oBAAoB,CAAA;AAClC,UAAA,OAAO,GAAA,CAAI,IAAA,CAAK,oBAAA,EAAsB,GAAG,CAAA;AAAA,QAC1C;AAEA,QAAA,MAAM,CAAC,QAAQ,MAAM,CAAA,GAAI,OAAO,MAAA,CAAO,IAAI,eAAe,CAAA;AAK1D,QAAA,IAAI;AACH,UAAA,MAAM,IAAA,CAAK,aAAA,CAAc,GAAA,EAAK,MAAM,CAAA;AACpC,UAAA,OAAO,IAAI,SAAS,IAAA,EAAM,EAAE,QAAQ,GAAA,EAAK,SAAA,EAAW,QAAQ,CAAA;AAAA,QAC7D,SAAS,KAAA,EAAO;AACf,UAAA,OAAA,CAAQ,MAAM,KAAK,CAAA;AACnB,UAAA,MAAA,CAAO,MAAA,EAAO;AACd,UAAA,MAAA,CAAO,IAAA;AAAA,YACN,KAAK,SAAA,CAAU;AAAA,cACd,KAAA,EAAO;AAAA,aACP;AAAA,WACF;AACA,UAAA,MAAA,CAAO,KAAA,CAAM,MAAM,0CAA0C,CAAA;AAC7D,UAAA,OAAO,IAAI,SAAS,IAAA,EAAM,EAAE,QAAQ,GAAA,EAAK,SAAA,EAAW,QAAQ,CAAA;AAAA,QAC7D;AAAA,MACD;AAAA,KACD;AAAA,EACD;AAAA,EAEA,MAAM,aAAA,CACL,GAAA,EACA,EAAA,EACgB;AAChB,IAAA,IAAA,CAAK,GAAA,CAAI,gBAAgB,EAAE,CAAA;AAC3B,IAAA,IAAI;AACH,MAAA,MAAM,UAAU,MAAM,IAAA,CAAK,OAAA,CAAQ,aAAA,CAAc,KAAK,EAAE,CAAA;AACxD,MAAA,OAAA,CAAQ,WAAW,GAAG,CAAA;AACtB,MAAA,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,EAAA,EAAI,OAAO,CAAA;AAAA,IAC9B,SAAS,KAAA,EAAO;AACf,MAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,4BAAA,EAA+B,KAAK,CAAA,CAAE,CAAA;AACpD,MAAA,MAAM,IAAA,CAAK,cAAA,CAAe,EAAA,EAAI,KAAK,CAAA;AAAA,IACpC;AAAA,EACD;AAAA,EAEA,MAAe,gBAAA,CACd,EAAA,EACA,OAAA,EACgB;AAChB,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,EAAE,CAAA;AACpC,IAAA,IAAI,CAAC,OAAA,EAAS;AAEd,IAAA,IAAI;AACH,MAAA,IAAI,mBAAmB,WAAA,EAAa;AACnC,QAAA,MAAM,OAAA,CAAQ,oBAAoB,OAAO,CAAA;AACzC,QAAA;AAAA,MACD;AAEA,MAAA,MAAM,iBAAA,GAAoB,OAAA;AAQ1B,MAAA,IAAI,kBAAkB,gBAAA,EAAkB;AACvC,QAAA,MAAM,iBAAA,CAAkB,iBAAiB,OAAO,CAAA;AAChD,QAAA;AAAA,MACD;AAEA,MAAA,MAAM,MAAA,GAAS,IAAA,CAAK,KAAA,CAAM,OAAO,CAAA;AACjC,MAAA,MAAM,OAAA,CAAQ,cAAc,MAAM,CAAA;AAAA,IACnC,SAAS,KAAA,EAAO;AACf,MAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,8BAAA,EAAiC,KAAK,CAAA,CAAE,CAAA;AAAA,IAGvD;AAAA,EACD;AAAA,EAEA,MAAe,cAAA,CACd,EAAA,EACA,KAAA,EACA,SACA,SAAA,EACC;AACD,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,EAAE,CAAA;AACpC,IAAA,IAAI,CAAC,OAAA,EAAS;AAEd,IAAA,IAAI;AACH,MAAA,MAAM,eAAA,CAAA,IAAA,EAAK,4CAAL,IAAA,CAAA,IAAA,EAAkB,OAAA,CAAA;AAAA,IACzB,SAAS,KAAA,EAAO;AACf,MAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,4BAAA,EAA+B,KAAK,CAAA,CAAE,CAAA;AAAA,IACrD,CAAA,SAAE;AAGD,MAAA,IACC,GAAG,UAAA,KAAe,SAAA,CAAU,QAC5B,EAAA,CAAG,UAAA,KAAe,UAAU,OAAA,EAC3B;AACD,QAAA,EAAA,CAAG,KAAA,CAAM,KAAM,gBAAgB,CAAA;AAAA,MAChC;AAAA,IACD;AAAA,EACD;AAAA,EAEA,MAAe,cAAA,CAAe,EAAA,EAAe,KAAA,EAAgB;AAC5D,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,EAAE,CAAA;AACpC,IAAA,IAAI,CAAC,OAAA,EAAS;AAEb,MAAA,IACC,GAAG,UAAA,KAAe,SAAA,CAAU,QAC5B,EAAA,CAAG,UAAA,KAAe,UAAU,OAAA,EAC3B;AACD,QAAA,EAAA,CAAG,KAAA,CAAM,MAAM,6BAA6B,CAAA;AAAA,MAC7C;AACA,MAAA;AAAA,IACD;AAEA,IAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,mBAAA,EAAsB,KAAK,CAAA,CAAE,CAAA;AAC3C,IAAA,IAAI;AACH,MAAA,MAAM,eAAA,CAAA,IAAA,EAAK,4CAAL,IAAA,CAAA,IAAA,EAAkB,OAAA,CAAA;AAAA,IACzB,SAASA,MAAAA,EAAO;AACf,MAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,4BAAA,EAA+BA,MAAK,CAAA,CAAE,CAAA;AAAA,IACrD,CAAA,SAAE;AAED,MAAA,IACC,GAAG,UAAA,KAAe,SAAA,CAAU,QAC5B,EAAA,CAAG,UAAA,KAAe,UAAU,OAAA,EAC3B;AACD,QAAA,EAAA,CAAG,KAAA,CAAM,MAAM,uBAAuB,CAAA;AAAA,MACvC;AAAA,IACD;AAAA,EACD;AAAA,EAYS,MAAM,OAAA,EAAgD;AAC9D,IAAA,OAAO,IAAA,CAAK,GAAA,CAAI,KAAA,CAAM,OAAA,EAAS,KAAK,GAAG,CAAA;AAAA,EACxC;AACD;AApMO,0BAAA,GAAA,IAAA,OAAA,EAAA;AAuLA,cAAA,GAAY,eAAC,OAAA,EAAmB;AACrC,EAAA,IAAI;AACH,IAAA,MAAM,QAAQ,WAAA,EAAY;AAAA,EAC3B,SAAS,KAAA,EAAO;AACf,IAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,4BAAA,EAA+B,KAAK,CAAA,CAAE,CAAA;AAAA,EACrD,CAAA,SAAE;AACD,IAAA,IAAA,CAAK,QAAA,CAAS,MAAA,CAAO,OAAA,CAAQ,SAAS,CAAA;AAAA,EACvC;AACD,CAAA","file":"chunk-XFB6C3NZ.js","sourcesContent":["import { DurableObject } from \"cloudflare:workers\";\nimport type { DOWithHonoApp } from \"@firtoz/hono-fetcher/honoDoFetcher\";\nimport { type Context, Hono } from \"hono\";\nimport type {\n\tBaseSession,\n\tSessionClientMessage,\n\tSessionEnv,\n} from \"./BaseSession\";\n\nexport type BaseWebSocketDOOptions<\n\t// biome-ignore lint/suspicious/noExplicitAny: We are using any on purpose to allow any type of session.\n\tTSession extends BaseSession<any, any, any, any>,\n\tTEnv extends SessionEnv<TSession>,\n> = {\n\tcreateSession: (\n\t\tctx: Context<{ Bindings: TEnv }> | undefined,\n\t\twebsocket: WebSocket,\n\t) => TSession | Promise<TSession>;\n};\n\nexport abstract class BaseWebSocketDO<\n\t\t// biome-ignore lint/suspicious/noExplicitAny: We are using any on purpose to allow any type of session.\n\t\tTSession extends BaseSession<any, any, any, any> = BaseSession<\n\t\t\t// biome-ignore lint/suspicious/noExplicitAny: We are using any on purpose to allow any type of session.\n\t\t\tany,\n\t\t\t// biome-ignore lint/suspicious/noExplicitAny: We are using any on purpose to allow any type of session.\n\t\t\tany,\n\t\t\t// biome-ignore lint/suspicious/noExplicitAny: We are using any on purpose to allow any type of session.\n\t\t\tany,\n\t\t\t// biome-ignore lint/suspicious/noExplicitAny: We are using any on purpose to allow any type of session.\n\t\t\tany\n\t\t>,\n\t\tTEnv extends SessionEnv<TSession> = SessionEnv<TSession>,\n\t>\n\textends DurableObject<TEnv>\n\timplements DOWithHonoApp\n{\n\tprotected readonly sessions = new Map<WebSocket, TSession>();\n\tabstract readonly app: Hono<{ Bindings: TEnv }>;\n\n\tconstructor(\n\t\tctx: DurableObjectState,\n\t\tenv: TEnv,\n\t\tprivate readonly options: BaseWebSocketDOOptions<TSession, TEnv>,\n\t) {\n\t\tsuper(ctx, env);\n\n\t\tthis.ctx.blockConcurrencyWhile(async () => {\n\t\t\tconst websockets = this.ctx.getWebSockets();\n\t\t\tawait Promise.all(\n\t\t\t\twebsockets.map(async (websocket) => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\t// For resumed sessions, we don't have a Hono context\n\t\t\t\t\t\t// Pass undefined and let implementers handle it\n\t\t\t\t\t\tconst session = await options.createSession(undefined, websocket);\n\t\t\t\t\t\tsession.resume();\n\t\t\t\t\t\tthis.sessions.set(websocket, session);\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tconsole.error(`Error during session setup: ${error}`);\n\t\t\t\t\t\tawait this.webSocketError(websocket, error);\n\t\t\t\t\t}\n\t\t\t\t}),\n\t\t\t);\n\t\t});\n\t}\n\n\tprotected getBaseApp() {\n\t\treturn new Hono<{ Bindings: TEnv }>().get(\n\t\t\t\"/websocket\",\n\t\t\tasync (ctx): Promise<Response> => {\n\t\t\t\tconst { req } = ctx;\n\t\t\t\tif (req.header(\"Upgrade\") !== \"websocket\") {\n\t\t\t\t\tconsole.error(\"Expected websocket\");\n\t\t\t\t\treturn ctx.text(\"Expected websocket\", 400);\n\t\t\t\t}\n\n\t\t\t\tconst [client, server] = Object.values(new WebSocketPair()) as [\n\t\t\t\t\tWebSocket,\n\t\t\t\t\tWebSocket,\n\t\t\t\t];\n\n\t\t\t\ttry {\n\t\t\t\t\tawait this.handleSession(ctx, server);\n\t\t\t\t\treturn new Response(null, { status: 101, webSocket: client });\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconsole.error(error);\n\t\t\t\t\tclient.accept();\n\t\t\t\t\tclient.send(\n\t\t\t\t\t\tJSON.stringify({\n\t\t\t\t\t\t\terror: \"Uncaught exception during session setup.\",\n\t\t\t\t\t\t}),\n\t\t\t\t\t);\n\t\t\t\t\tclient.close(1011, \"Uncaught exception during session setup.\");\n\t\t\t\t\treturn new Response(null, { status: 101, webSocket: client });\n\t\t\t\t}\n\t\t\t},\n\t\t);\n\t}\n\n\tasync handleSession(\n\t\tctx: Context<{ Bindings: TEnv }>,\n\t\tws: WebSocket,\n\t): Promise<void> {\n\t\tthis.ctx.acceptWebSocket(ws);\n\t\ttry {\n\t\t\tconst session = await this.options.createSession(ctx, ws);\n\t\t\tsession.startFresh(ctx);\n\t\t\tthis.sessions.set(ws, session);\n\t\t} catch (error) {\n\t\t\tconsole.error(`Error during session setup: ${error}`);\n\t\t\tawait this.webSocketError(ws, error);\n\t\t}\n\t}\n\n\toverride async webSocketMessage(\n\t\tws: WebSocket,\n\t\tmessage: string | ArrayBuffer,\n\t): Promise<void> {\n\t\tconst session = this.sessions.get(ws);\n\t\tif (!session) return;\n\n\t\ttry {\n\t\t\tif (message instanceof ArrayBuffer) {\n\t\t\t\tawait session.handleBufferMessage(message);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst rawMessageSession = session as BaseSession<\n\t\t\t\tunknown,\n\t\t\t\tunknown,\n\t\t\t\tunknown,\n\t\t\t\tTEnv\n\t\t\t> & {\n\t\t\t\thandleRawMessage?: (rawMessage: string) => Promise<void>;\n\t\t\t};\n\t\t\tif (rawMessageSession.handleRawMessage) {\n\t\t\t\tawait rawMessageSession.handleRawMessage(message);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst parsed = JSON.parse(message) as SessionClientMessage<TSession>;\n\t\t\tawait session.handleMessage(parsed);\n\t\t} catch (error) {\n\t\t\tconsole.error(`Error during session message: ${error}`);\n\t\t\t// Let the implementer decide how to handle errors in their session implementation\n\t\t\t// The session can optionally implement error handling that closes the connection if needed\n\t\t}\n\t}\n\n\toverride async webSocketClose(\n\t\tws: WebSocket,\n\t\t_code: number,\n\t\t_reason: string,\n\t\t_wasClean: boolean,\n\t) {\n\t\tconst session = this.sessions.get(ws);\n\t\tif (!session) return;\n\n\t\ttry {\n\t\t\tawait this.#handleClose(session);\n\t\t} catch (error) {\n\t\t\tconsole.error(`Error during session close: ${error}`);\n\t\t} finally {\n\t\t\t// Call close() for both OPEN and CLOSING states\n\t\t\t// For CLOSING, this can help ensure the WebSocket fully transitions to CLOSED\n\t\t\tif (\n\t\t\t\tws.readyState === WebSocket.OPEN ||\n\t\t\t\tws.readyState === WebSocket.CLOSING\n\t\t\t) {\n\t\t\t\tws.close(1000, \"Normal closure\");\n\t\t\t}\n\t\t}\n\t}\n\n\toverride async webSocketError(ws: WebSocket, error: unknown) {\n\t\tconst session = this.sessions.get(ws);\n\t\tif (!session) {\n\t\t\t// Call close() for both OPEN and CLOSING states\n\t\t\tif (\n\t\t\t\tws.readyState === WebSocket.OPEN ||\n\t\t\t\tws.readyState === WebSocket.CLOSING\n\t\t\t) {\n\t\t\t\tws.close(1011, \"Error during session setup.\");\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tconsole.error(`Error for session: ${error}`);\n\t\ttry {\n\t\t\tawait this.#handleClose(session);\n\t\t} catch (error) {\n\t\t\tconsole.error(`Error during session close: ${error}`);\n\t\t} finally {\n\t\t\t// Call close() for both OPEN and CLOSING states\n\t\t\tif (\n\t\t\t\tws.readyState === WebSocket.OPEN ||\n\t\t\t\tws.readyState === WebSocket.CLOSING\n\t\t\t) {\n\t\t\t\tws.close(1011, \"Error during session.\");\n\t\t\t}\n\t\t}\n\t}\n\n\tasync #handleClose(session: TSession) {\n\t\ttry {\n\t\t\tawait session.handleClose();\n\t\t} catch (error) {\n\t\t\tconsole.error(`Error during session close: ${error}`);\n\t\t} finally {\n\t\t\tthis.sessions.delete(session.websocket);\n\t\t}\n\t}\n\n\toverride fetch(request: Request): Response | Promise<Response> {\n\t\treturn this.app.fetch(request, this.env);\n\t}\n}\n"]}