@firtoz/websocket-do 13.0.2 → 14.0.1
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 +19 -0
- package/dist/BaseWebSocketDO.d.ts +9 -0
- package/dist/BaseWebSocketDO.js +1 -1
- package/dist/StandardSchemaWebSocketDO.js +2 -2
- package/dist/{chunk-E4576ZE6.js → chunk-KFZPNOZL.js} +15 -2
- package/dist/chunk-KFZPNOZL.js.map +1 -0
- package/dist/{chunk-J275SVBA.js → chunk-OA3RDNDW.js} +3 -3
- package/dist/{chunk-J275SVBA.js.map → chunk-OA3RDNDW.js.map} +1 -1
- package/dist/index.js +2 -2
- package/package.json +8 -8
- package/src/BaseWebSocketDO.ts +17 -0
- package/dist/chunk-E4576ZE6.js.map +0 -1
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:
|
|
@@ -38,6 +38,15 @@ declare abstract class BaseWebSocketDO<TSession extends BaseSession<any, any, an
|
|
|
38
38
|
};
|
|
39
39
|
};
|
|
40
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>;
|
|
41
50
|
handleSession(ctx: Context<{
|
|
42
51
|
Bindings: TEnv;
|
|
43
52
|
}>, ws: WebSocket): Promise<void>;
|
package/dist/BaseWebSocketDO.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { StandardSchemaWebSocketDO } from './chunk-
|
|
2
|
-
import './chunk-
|
|
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,6 +56,15 @@ 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) {
|
|
56
69
|
const acceptOpts = this.options.pairServerWebSocketAcceptOptions;
|
|
57
70
|
if (acceptOpts !== void 0) {
|
|
@@ -128,5 +141,5 @@ handleClose_fn = async function(session) {
|
|
|
128
141
|
};
|
|
129
142
|
|
|
130
143
|
export { BaseWebSocketDO };
|
|
131
|
-
//# sourceMappingURL=chunk-
|
|
132
|
-
//# sourceMappingURL=chunk-
|
|
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":";;;;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,IAAA,GAAO,MAAM,IAAA,CAAK,eAAA,CAAgB,GAAG,CAAA;AAC3C,QAAA,IAAI,gBAAgB,QAAA,EAAU;AAC7B,UAAA,OAAO,IAAA;AAAA,QACR;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;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQU,gBACT,IAAA,EACuD;AACvD,IAAA;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;AA3MO,0BAAA,GAAA,IAAA,OAAA,EAAA;AA8LA,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-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-
|
|
1
|
+
import { BaseWebSocketDO } from './chunk-KFZPNOZL.js';
|
|
2
2
|
|
|
3
3
|
// src/StandardSchemaWebSocketDO.ts
|
|
4
4
|
var StandardSchemaWebSocketDO = class extends BaseWebSocketDO {
|
|
@@ -20,5 +20,5 @@ var StandardSchemaWebSocketDO = class extends BaseWebSocketDO {
|
|
|
20
20
|
};
|
|
21
21
|
|
|
22
22
|
export { StandardSchemaWebSocketDO };
|
|
23
|
-
//# sourceMappingURL=chunk-
|
|
24
|
-
//# sourceMappingURL=chunk-
|
|
23
|
+
//# sourceMappingURL=chunk-OA3RDNDW.js.map
|
|
24
|
+
//# sourceMappingURL=chunk-OA3RDNDW.js.map
|
|
@@ -1 +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-
|
|
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-
|
|
6
|
-
export { BaseWebSocketDO } from './chunk-
|
|
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": "
|
|
3
|
+
"version": "14.0.1",
|
|
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.
|
|
66
|
-
"@firtoz/hono-fetcher": "^2.
|
|
67
|
-
"hono": "^4.12.
|
|
65
|
+
"@cloudflare/workers-types": "^4.20260529.1",
|
|
66
|
+
"@firtoz/hono-fetcher": "^2.8.1",
|
|
67
|
+
"hono": "^4.12.23",
|
|
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": "^
|
|
77
|
+
"msgpackr": "^2.0.2"
|
|
78
78
|
},
|
|
79
79
|
"engines": {
|
|
80
80
|
"node": ">=18.0.0"
|
|
@@ -83,9 +83,9 @@
|
|
|
83
83
|
"access": "public"
|
|
84
84
|
},
|
|
85
85
|
"devDependencies": {
|
|
86
|
-
"@types/react": "^19.2.
|
|
87
|
-
"bun-types": "^1.3.
|
|
86
|
+
"@types/react": "^19.2.15",
|
|
87
|
+
"bun-types": "^1.3.14",
|
|
88
88
|
"typescript": "^6.0.3",
|
|
89
|
-
"zod": "^4.3
|
|
89
|
+
"zod": "^4.4.3"
|
|
90
90
|
}
|
|
91
91
|
}
|
package/src/BaseWebSocketDO.ts
CHANGED
|
@@ -82,6 +82,11 @@ export abstract class BaseWebSocketDO<
|
|
|
82
82
|
return ctx.text("Expected websocket", 400);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
const gate = await this.beforeWebSocket(ctx);
|
|
86
|
+
if (gate instanceof Response) {
|
|
87
|
+
return gate;
|
|
88
|
+
}
|
|
89
|
+
|
|
85
90
|
const [client, server] = Object.values(new WebSocketPair()) as [
|
|
86
91
|
WebSocket,
|
|
87
92
|
WebSocket,
|
|
@@ -105,6 +110,18 @@ export abstract class BaseWebSocketDO<
|
|
|
105
110
|
);
|
|
106
111
|
}
|
|
107
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
|
+
|
|
108
125
|
async handleSession(
|
|
109
126
|
ctx: Context<{ Bindings: TEnv }>,
|
|
110
127
|
ws: WebSocket,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/BaseWebSocketDO.ts"],"names":["error"],"mappings":"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","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"]}
|