@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 +19 -0
- package/dist/BaseWebSocketDO.d.ts +18 -1
- package/dist/BaseWebSocketDO.js +1 -1
- package/dist/StandardSchemaWebSocketDO.d.ts +5 -0
- package/dist/StandardSchemaWebSocketDO.js +2 -2
- package/dist/{chunk-XFB6C3NZ.js → chunk-KFZPNOZL.js} +23 -12
- package/dist/chunk-KFZPNOZL.js.map +1 -0
- package/dist/{chunk-ULGH6X42.js → chunk-OA3RDNDW.js} +5 -4
- package/dist/chunk-OA3RDNDW.js.map +1 -0
- package/dist/index.js +2 -2
- package/package.json +6 -6
- package/src/BaseWebSocketDO.ts +39 -24
- package/src/StandardSchemaWebSocketDO.ts +7 -0
- package/dist/chunk-ULGH6X42.js.map +0 -1
- package/dist/chunk-XFB6C3NZ.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:
|
|
@@ -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,
|
|
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
|
}
|
package/dist/BaseWebSocketDO.js
CHANGED
|
@@ -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-
|
|
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,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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
134
|
-
//# 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":"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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-
|
|
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-
|
|
23
|
-
//# sourceMappingURL=chunk-
|
|
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-
|
|
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.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.
|
|
66
|
-
"@firtoz/hono-fetcher": "^2.
|
|
67
|
-
"hono": "^4.12.
|
|
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": "^
|
|
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.
|
|
89
|
+
"zod": "^4.4.2"
|
|
90
90
|
}
|
|
91
91
|
}
|
package/src/BaseWebSocketDO.ts
CHANGED
|
@@ -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
|
-
|
|
153
|
-
|
|
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
|
-
//
|
|
165
|
-
//
|
|
166
|
-
if
|
|
167
|
-
|
|
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
|
-
//
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
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":";;;;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"]}
|