@firtoz/websocket-do 13.0.1 → 14.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -574,6 +574,25 @@ Low-level wrapper for typed WebSocket operations.
574
574
 
575
575
  ## Advanced Usage
576
576
 
577
+ ### Pre-upgrade auth (`beforeWebSocket`)
578
+
579
+ Override **`beforeWebSocket(ctx)`** on your DO subclass to reject the WebSocket handshake **before** `101 Switching Protocols` (HTTP `401` / `403`). Runs after the `Upgrade: websocket` check and before `WebSocketPair` is created:
580
+
581
+ ```typescript
582
+ export class ChatRoomDO extends BaseWebSocketDO<ChatSession, Env> {
583
+ app = this.getBaseApp();
584
+
585
+ protected beforeWebSocket(ctx: Context<{ Bindings: Env }>): Response | undefined {
586
+ const token = ctx.req.header('X-Room-Auth');
587
+ if (token !== this.env.ROOM_SECRET) {
588
+ return ctx.text('Unauthorized', 401);
589
+ }
590
+ }
591
+ }
592
+ ```
593
+
594
+ Use this when a web worker forwards attested identity via headers. Auth inside **`createData`** runs **after** upgrade — see socka **[Authentication](./auth.md)** for when to use each.
595
+
577
596
  ### Custom Routes
578
597
 
579
598
  You can extend the base app with custom routes:
@@ -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;
@@ -30,11 +38,20 @@ declare abstract class BaseWebSocketDO<TSession extends BaseSession<any, any, an
30
38
  };
31
39
  };
32
40
  }, "/", "/websocket">;
41
+ /**
42
+ * Optional gate before the WebSocket upgrade creates a {@link WebSocketPair}.
43
+ * Return an HTTP {@link Response} (e.g. `401` / `403`) to reject the upgrade;
44
+ * return `undefined` / `void` to proceed. Override on your DO subclass or use
45
+ * Hono middleware on a chained `app` for route-level checks.
46
+ */
47
+ protected beforeWebSocket(_ctx: Context<{
48
+ Bindings: TEnv;
49
+ }>): Response | undefined | Promise<Response | undefined>;
33
50
  handleSession(ctx: Context<{
34
51
  Bindings: TEnv;
35
52
  }>, ws: WebSocket): Promise<void>;
36
53
  webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): Promise<void>;
37
- webSocketClose(ws: WebSocket, _code: number, _reason: string, _wasClean: boolean): Promise<void>;
54
+ webSocketClose(ws: WebSocket, code: number, reason: string, _wasClean: boolean): Promise<void>;
38
55
  webSocketError(ws: WebSocket, error: unknown): Promise<void>;
39
56
  fetch(request: Request): Response | Promise<Response>;
40
57
  }
@@ -1,4 +1,4 @@
1
- export { BaseWebSocketDO } from './chunk-XFB6C3NZ.js';
1
+ export { BaseWebSocketDO } from './chunk-KFZPNOZL.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-OA3RDNDW.js';
2
+ import './chunk-KFZPNOZL.js';
3
3
  import './chunk-NOUFNU2O.js';
4
4
  //# sourceMappingURL=StandardSchemaWebSocketDO.js.map
5
5
  //# sourceMappingURL=StandardSchemaWebSocketDO.js.map
@@ -34,6 +34,10 @@ var BaseWebSocketDO = class extends DurableObject {
34
34
  console.error("Expected websocket");
35
35
  return ctx.text("Expected websocket", 400);
36
36
  }
37
+ const gate = await this.beforeWebSocket(ctx);
38
+ if (gate instanceof Response) {
39
+ return gate;
40
+ }
37
41
  const [client, server] = Object.values(new WebSocketPair());
38
42
  try {
39
43
  await this.handleSession(ctx, server);
@@ -52,7 +56,20 @@ var BaseWebSocketDO = class extends DurableObject {
52
56
  }
53
57
  );
54
58
  }
59
+ /**
60
+ * Optional gate before the WebSocket upgrade creates a {@link WebSocketPair}.
61
+ * Return an HTTP {@link Response} (e.g. `401` / `403`) to reject the upgrade;
62
+ * return `undefined` / `void` to proceed. Override on your DO subclass or use
63
+ * Hono middleware on a chained `app` for route-level checks.
64
+ */
65
+ beforeWebSocket(_ctx) {
66
+ return;
67
+ }
55
68
  async handleSession(ctx, ws) {
69
+ const acceptOpts = this.options.pairServerWebSocketAcceptOptions;
70
+ if (acceptOpts !== void 0) {
71
+ ws.accept(acceptOpts);
72
+ }
56
73
  this.ctx.acceptWebSocket(ws);
57
74
  try {
58
75
  const session = await this.options.createSession(ctx, ws);
@@ -82,7 +99,7 @@ var BaseWebSocketDO = class extends DurableObject {
82
99
  console.error(`Error during session message: ${error}`);
83
100
  }
84
101
  }
85
- async webSocketClose(ws, _code, _reason, _wasClean) {
102
+ async webSocketClose(ws, code, reason, _wasClean) {
86
103
  const session = this.sessions.get(ws);
87
104
  if (!session) return;
88
105
  try {
@@ -90,17 +107,13 @@ var BaseWebSocketDO = class extends DurableObject {
90
107
  } catch (error) {
91
108
  console.error(`Error during session close: ${error}`);
92
109
  } finally {
93
- if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CLOSING) {
94
- ws.close(1e3, "Normal closure");
95
- }
110
+ ws.close(code, reason);
96
111
  }
97
112
  }
98
113
  async webSocketError(ws, error) {
99
114
  const session = this.sessions.get(ws);
100
115
  if (!session) {
101
- if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CLOSING) {
102
- ws.close(1011, "Error during session setup.");
103
- }
116
+ ws.close(1011, "Error during session setup.");
104
117
  return;
105
118
  }
106
119
  console.error(`Error for session: ${error}`);
@@ -109,9 +122,7 @@ var BaseWebSocketDO = class extends DurableObject {
109
122
  } catch (error2) {
110
123
  console.error(`Error during session close: ${error2}`);
111
124
  } finally {
112
- if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CLOSING) {
113
- ws.close(1011, "Error during session.");
114
- }
125
+ ws.close(1011, "Error during session.");
115
126
  }
116
127
  }
117
128
  fetch(request) {
@@ -130,5 +141,5 @@ handleClose_fn = async function(session) {
130
141
  };
131
142
 
132
143
  export { BaseWebSocketDO };
133
- //# sourceMappingURL=chunk-XFB6C3NZ.js.map
134
- //# sourceMappingURL=chunk-XFB6C3NZ.js.map
144
+ //# sourceMappingURL=chunk-KFZPNOZL.js.map
145
+ //# sourceMappingURL=chunk-KFZPNOZL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/BaseWebSocketDO.ts"],"names":["error"],"mappings":"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","file":"chunk-KFZPNOZL.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 gate = await this.beforeWebSocket(ctx);\n\t\t\t\tif (gate instanceof Response) {\n\t\t\t\t\treturn gate;\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\t/**\n\t * Optional gate before the WebSocket upgrade creates a {@link WebSocketPair}.\n\t * Return an HTTP {@link Response} (e.g. `401` / `403`) to reject the upgrade;\n\t * return `undefined` / `void` to proceed. Override on your DO subclass or use\n\t * Hono middleware on a chained `app` for route-level checks.\n\t */\n\tprotected beforeWebSocket(\n\t\t_ctx: Context<{ Bindings: TEnv }>,\n\t): Response | undefined | Promise<Response | undefined> {\n\t\treturn;\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-KFZPNOZL.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-OA3RDNDW.js.map
24
+ //# sourceMappingURL=chunk-OA3RDNDW.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-OA3RDNDW.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-OA3RDNDW.js';
6
+ export { BaseWebSocketDO } from './chunk-KFZPNOZL.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.1",
3
+ "version": "14.0.0",
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.20260422.1",
66
- "@firtoz/hono-fetcher": "^2.7.1",
67
- "hono": "^4.12.14",
65
+ "@cloudflare/workers-types": "^4.20260503.1",
66
+ "@firtoz/hono-fetcher": "^2.8.0",
67
+ "hono": "^4.12.16",
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.10"
77
+ "msgpackr": "^2.0.1"
78
78
  },
79
79
  "engines": {
80
80
  "node": ">=18.0.0"
@@ -86,6 +86,6 @@
86
86
  "@types/react": "^19.2.14",
87
87
  "bun-types": "^1.3.13",
88
88
  "typescript": "^6.0.3",
89
- "zod": "^4.3.6"
89
+ "zod": "^4.4.2"
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<
@@ -74,6 +82,11 @@ export abstract class BaseWebSocketDO<
74
82
  return ctx.text("Expected websocket", 400);
75
83
  }
76
84
 
85
+ const gate = await this.beforeWebSocket(ctx);
86
+ if (gate instanceof Response) {
87
+ return gate;
88
+ }
89
+
77
90
  const [client, server] = Object.values(new WebSocketPair()) as [
78
91
  WebSocket,
79
92
  WebSocket,
@@ -97,10 +110,26 @@ export abstract class BaseWebSocketDO<
97
110
  );
98
111
  }
99
112
 
113
+ /**
114
+ * Optional gate before the WebSocket upgrade creates a {@link WebSocketPair}.
115
+ * Return an HTTP {@link Response} (e.g. `401` / `403`) to reject the upgrade;
116
+ * return `undefined` / `void` to proceed. Override on your DO subclass or use
117
+ * Hono middleware on a chained `app` for route-level checks.
118
+ */
119
+ protected beforeWebSocket(
120
+ _ctx: Context<{ Bindings: TEnv }>,
121
+ ): Response | undefined | Promise<Response | undefined> {
122
+ return;
123
+ }
124
+
100
125
  async handleSession(
101
126
  ctx: Context<{ Bindings: TEnv }>,
102
127
  ws: WebSocket,
103
128
  ): Promise<void> {
129
+ const acceptOpts = this.options.pairServerWebSocketAcceptOptions;
130
+ if (acceptOpts !== undefined) {
131
+ ws.accept(acceptOpts);
132
+ }
104
133
  this.ctx.acceptWebSocket(ws);
105
134
  try {
106
135
  const session = await this.options.createSession(ctx, ws);
@@ -149,8 +178,8 @@ export abstract class BaseWebSocketDO<
149
178
 
150
179
  override async webSocketClose(
151
180
  ws: WebSocket,
152
- _code: number,
153
- _reason: string,
181
+ code: number,
182
+ reason: string,
154
183
  _wasClean: boolean,
155
184
  ) {
156
185
  const session = this.sessions.get(ws);
@@ -161,27 +190,19 @@ export abstract class BaseWebSocketDO<
161
190
  } catch (error) {
162
191
  console.error(`Error during session close: ${error}`);
163
192
  } 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
- }
193
+ // Pre–2026-04-07 (manual close reply): required to complete the Close handshake and avoid
194
+ // abnormal client closure. With web_socket_auto_reply_to_close (default on 2026-04-07+),
195
+ // the runtime already replied; `close()` is a no-op if already CLOSED.
196
+ ws.close(code, reason);
172
197
  }
173
198
  }
174
199
 
175
200
  override async webSocketError(ws: WebSocket, error: unknown) {
176
201
  const session = this.sessions.get(ws);
177
202
  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
- }
203
+ // Idempotent: safe when the socket is already closed (auto close reply) or in CLOSING
204
+ // (legacy manual reply to complete the handshake).
205
+ ws.close(1011, "Error during session setup.");
185
206
  return;
186
207
  }
187
208
 
@@ -191,13 +212,7 @@ export abstract class BaseWebSocketDO<
191
212
  } catch (error) {
192
213
  console.error(`Error during session close: ${error}`);
193
214
  } 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
- }
215
+ ws.close(1011, "Error during session.");
201
216
  }
202
217
  }
203
218
 
@@ -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":"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","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"]}