@firtoz/websocket-do 10.0.0 → 13.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 +68 -61
- package/dist/BaseSession.d.ts +41 -0
- package/dist/BaseSession.js +5 -0
- package/dist/BaseSession.js.map +1 -0
- package/dist/BaseWebSocketDO.d.ts +42 -0
- package/dist/BaseWebSocketDO.js +4 -0
- package/dist/BaseWebSocketDO.js.map +1 -0
- package/dist/StandardSchemaSession.d.ts +41 -0
- package/dist/StandardSchemaSession.js +8 -0
- package/dist/StandardSchemaSession.js.map +1 -0
- package/dist/StandardSchemaWebSocketClient.d.ts +45 -0
- package/dist/StandardSchemaWebSocketClient.js +5 -0
- package/dist/StandardSchemaWebSocketClient.js.map +1 -0
- package/dist/StandardSchemaWebSocketDO.d.ts +28 -0
- package/dist/StandardSchemaWebSocketDO.js +5 -0
- package/dist/StandardSchemaWebSocketDO.js.map +1 -0
- package/dist/WebsocketWrapper.d.ts +9 -0
- package/dist/WebsocketWrapper.js +4 -0
- package/dist/WebsocketWrapper.js.map +1 -0
- package/dist/chunk-3C77OSOD.js +54 -0
- package/dist/chunk-3C77OSOD.js.map +1 -0
- package/dist/chunk-3LWVEY3R.js +130 -0
- package/dist/chunk-3LWVEY3R.js.map +1 -0
- package/dist/chunk-53MFRNQS.js +153 -0
- package/dist/chunk-53MFRNQS.js.map +1 -0
- package/dist/chunk-CAX4POIL.js +13 -0
- package/dist/chunk-CAX4POIL.js.map +1 -0
- package/dist/chunk-KCPOB32E.js +20 -0
- package/dist/chunk-KCPOB32E.js.map +1 -0
- package/dist/chunk-NOUFNU2O.js +10 -0
- package/dist/chunk-NOUFNU2O.js.map +1 -0
- package/dist/chunk-QMGIRIHJ.js +18 -0
- package/dist/chunk-QMGIRIHJ.js.map +1 -0
- package/dist/chunk-ULGH6X42.js +23 -0
- package/dist/chunk-ULGH6X42.js.map +1 -0
- package/dist/chunk-WJIQBI6I.js +35 -0
- package/dist/chunk-WJIQBI6I.js.map +1 -0
- package/dist/chunk-XFB6C3NZ.js +134 -0
- package/dist/chunk-XFB6C3NZ.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/parseStandardSchema.d.ts +9 -0
- package/dist/parseStandardSchema.js +4 -0
- package/dist/parseStandardSchema.js.map +1 -0
- package/dist/standardSchemaMsgpack.d.ts +8 -0
- package/dist/standardSchemaMsgpack.js +5 -0
- package/dist/standardSchemaMsgpack.js.map +1 -0
- package/dist/standardSchemaRpc.d.ts +30 -0
- package/dist/standardSchemaRpc.js +6 -0
- package/dist/standardSchemaRpc.js.map +1 -0
- package/dist/standardSchemaRpcReact.d.ts +27 -0
- package/dist/standardSchemaRpcReact.js +54 -0
- package/dist/standardSchemaRpcReact.js.map +1 -0
- package/package.json +36 -18
- package/src/BaseSession.ts +15 -5
- package/src/{ZodSession.ts → StandardSchemaSession.ts} +71 -70
- package/src/{ZodWebSocketClient.ts → StandardSchemaWebSocketClient.ts} +30 -50
- package/src/{ZodWebSocketDO.ts → StandardSchemaWebSocketDO.ts} +29 -22
- package/src/index.ts +15 -12
- package/src/parseStandardSchema.ts +17 -0
- package/src/standardSchemaMsgpack.ts +17 -0
- package/src/standardSchemaRpc.ts +83 -0
- package/src/standardSchemaRpcReact.ts +107 -0
- package/src/zodMsgpack.ts +0 -13
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
1
2
|
import type { Context } from "hono";
|
|
2
|
-
import type { ZodType } from "zod";
|
|
3
3
|
import { BaseSession } from "./BaseSession";
|
|
4
|
-
import {
|
|
4
|
+
import { parseStandardSchema } from "./parseStandardSchema";
|
|
5
|
+
import { standardSchemaMsgpack } from "./standardSchemaMsgpack";
|
|
5
6
|
|
|
6
|
-
export type
|
|
7
|
-
clientSchema:
|
|
8
|
-
serverSchema:
|
|
7
|
+
export type StandardSchemaSessionOptions<TClientMessage, TServerMessage> = {
|
|
8
|
+
clientSchema: StandardSchemaV1<unknown, TClientMessage>;
|
|
9
|
+
serverSchema: StandardSchemaV1<unknown, TServerMessage>;
|
|
9
10
|
serializeJson?: (value: unknown) => string;
|
|
10
11
|
deserializeJson?: (raw: string) => unknown;
|
|
11
12
|
enableBufferMessages?: boolean;
|
|
@@ -15,9 +16,9 @@ export type ZodSessionOptions<TClientMessage, TServerMessage> = {
|
|
|
15
16
|
) => Promise<void>;
|
|
16
17
|
};
|
|
17
18
|
|
|
18
|
-
export type
|
|
19
|
+
export type StandardSchemaSessionHandlers<
|
|
19
20
|
TData,
|
|
20
|
-
|
|
21
|
+
TServerMessage,
|
|
21
22
|
TClientMessage,
|
|
22
23
|
TEnv extends object,
|
|
23
24
|
> = {
|
|
@@ -27,27 +28,36 @@ export type ZodSessionHandlers<
|
|
|
27
28
|
error: unknown,
|
|
28
29
|
originalMessage: unknown,
|
|
29
30
|
) => Promise<void>;
|
|
30
|
-
handleClose: (
|
|
31
|
+
handleClose: (
|
|
32
|
+
session: StandardSchemaSession<TData, TServerMessage, TClientMessage, TEnv>,
|
|
33
|
+
) => Promise<void>;
|
|
31
34
|
};
|
|
32
35
|
|
|
33
|
-
export class
|
|
36
|
+
export class StandardSchemaSession<
|
|
34
37
|
TData,
|
|
35
38
|
TServerMessage,
|
|
36
39
|
TClientMessage,
|
|
37
40
|
TEnv extends object = Cloudflare.Env,
|
|
38
41
|
> extends BaseSession<TData, TServerMessage, TClientMessage, TEnv> {
|
|
39
|
-
private readonly clientCodec: ReturnType<
|
|
40
|
-
|
|
42
|
+
private readonly clientCodec: ReturnType<
|
|
43
|
+
typeof standardSchemaMsgpack<TClientMessage>
|
|
44
|
+
>;
|
|
45
|
+
private readonly serverCodec: ReturnType<
|
|
46
|
+
typeof standardSchemaMsgpack<TServerMessage>
|
|
47
|
+
>;
|
|
41
48
|
protected readonly enableBufferMessages: boolean;
|
|
42
49
|
|
|
43
50
|
constructor(
|
|
44
51
|
websocket: WebSocket,
|
|
45
52
|
sessions: Map<
|
|
46
53
|
WebSocket,
|
|
47
|
-
|
|
54
|
+
StandardSchemaSession<TData, TServerMessage, TClientMessage, TEnv>
|
|
55
|
+
>,
|
|
56
|
+
private readonly options: StandardSchemaSessionOptions<
|
|
57
|
+
TClientMessage,
|
|
58
|
+
TServerMessage
|
|
48
59
|
>,
|
|
49
|
-
private readonly
|
|
50
|
-
private readonly zodHandlers: ZodSessionHandlers<
|
|
60
|
+
private readonly schemaHandlers: StandardSchemaSessionHandlers<
|
|
51
61
|
TData,
|
|
52
62
|
TServerMessage,
|
|
53
63
|
TClientMessage,
|
|
@@ -55,25 +65,33 @@ export class ZodSession<
|
|
|
55
65
|
>,
|
|
56
66
|
) {
|
|
57
67
|
super(websocket, sessions, {
|
|
58
|
-
createData:
|
|
68
|
+
createData: schemaHandlers.createData,
|
|
59
69
|
handleMessage: async (message) => {
|
|
60
70
|
return this._internalHandleMessage(message);
|
|
61
71
|
},
|
|
62
72
|
handleBufferMessage: async (message) => {
|
|
63
73
|
return this._internalHandleBufferMessage(message);
|
|
64
74
|
},
|
|
65
|
-
handleClose: async (
|
|
66
|
-
|
|
75
|
+
handleClose: async (
|
|
76
|
+
session: BaseSession<TData, TServerMessage, TClientMessage, TEnv>,
|
|
77
|
+
) => {
|
|
78
|
+
return schemaHandlers.handleClose(
|
|
79
|
+
session as StandardSchemaSession<
|
|
80
|
+
TData,
|
|
81
|
+
TServerMessage,
|
|
82
|
+
TClientMessage,
|
|
83
|
+
TEnv
|
|
84
|
+
>,
|
|
85
|
+
);
|
|
67
86
|
},
|
|
68
87
|
});
|
|
69
88
|
|
|
70
|
-
this.clientCodec =
|
|
71
|
-
this.serverCodec =
|
|
89
|
+
this.clientCodec = standardSchemaMsgpack(options.clientSchema);
|
|
90
|
+
this.serverCodec = standardSchemaMsgpack(options.serverSchema);
|
|
72
91
|
this.enableBufferMessages = options.enableBufferMessages ?? false;
|
|
73
92
|
}
|
|
74
93
|
|
|
75
94
|
public async handleRawMessage(rawMessage: string): Promise<void> {
|
|
76
|
-
// If buffer messages are enabled, reject string messages
|
|
77
95
|
if (this.enableBufferMessages) {
|
|
78
96
|
console.error(
|
|
79
97
|
"String messages not allowed when buffer messages are enabled",
|
|
@@ -86,17 +104,18 @@ export class ZodSession<
|
|
|
86
104
|
|
|
87
105
|
try {
|
|
88
106
|
const parsed = this.deserializeJson(rawMessage);
|
|
89
|
-
const validatedMessage =
|
|
90
|
-
|
|
107
|
+
const validatedMessage = await parseStandardSchema(
|
|
108
|
+
this.options.clientSchema,
|
|
109
|
+
parsed,
|
|
110
|
+
);
|
|
111
|
+
await this.schemaHandlers.handleValidatedMessage(validatedMessage);
|
|
91
112
|
} catch (error) {
|
|
92
113
|
console.error("Invalid client message received:", error);
|
|
93
114
|
await this._internalHandleValidationError(error, rawMessage);
|
|
94
115
|
}
|
|
95
116
|
}
|
|
96
117
|
|
|
97
|
-
// Internal method used by the base class handlers
|
|
98
118
|
private async _internalHandleMessage(message: TClientMessage): Promise<void> {
|
|
99
|
-
// If buffer messages are enabled, reject JSON messages
|
|
100
119
|
if (this.enableBufferMessages) {
|
|
101
120
|
console.error(
|
|
102
121
|
"String messages not allowed when buffer messages are enabled",
|
|
@@ -108,48 +127,44 @@ export class ZodSession<
|
|
|
108
127
|
}
|
|
109
128
|
|
|
110
129
|
try {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
130
|
+
const validatedMessage = await parseStandardSchema(
|
|
131
|
+
this.options.clientSchema,
|
|
132
|
+
message,
|
|
133
|
+
);
|
|
134
|
+
await this.schemaHandlers.handleValidatedMessage(validatedMessage);
|
|
114
135
|
} catch (error) {
|
|
115
136
|
console.error("Invalid client message received:", error);
|
|
116
137
|
await this._internalHandleValidationError(error, message);
|
|
117
138
|
}
|
|
118
139
|
}
|
|
119
140
|
|
|
120
|
-
// Internal method used by the base class handlers
|
|
121
141
|
private async _internalHandleBufferMessage(
|
|
122
142
|
buffer: ArrayBuffer,
|
|
123
143
|
): Promise<void> {
|
|
124
|
-
// If buffer messages are disabled, reject buffer messages
|
|
125
144
|
if (!this.enableBufferMessages) {
|
|
126
145
|
console.error(
|
|
127
146
|
"Buffer messages not allowed when buffer messages are disabled",
|
|
128
147
|
);
|
|
129
|
-
// We can't use sendProtocolError here because it would send JSON
|
|
130
|
-
// Just close the connection or ignore
|
|
131
148
|
return;
|
|
132
149
|
}
|
|
133
150
|
|
|
134
151
|
try {
|
|
135
152
|
const bytes = new Uint8Array(buffer);
|
|
136
|
-
const decodedMessage = this.clientCodec.decode(bytes);
|
|
137
|
-
await this.
|
|
153
|
+
const decodedMessage = await this.clientCodec.decode(bytes);
|
|
154
|
+
await this.schemaHandlers.handleValidatedMessage(decodedMessage);
|
|
138
155
|
} catch (error) {
|
|
139
156
|
console.error("Failed to decode buffer message:", error);
|
|
140
157
|
await this._internalHandleValidationError(error, buffer);
|
|
141
158
|
}
|
|
142
159
|
}
|
|
143
160
|
|
|
144
|
-
// Internal validation error handler
|
|
145
161
|
private async _internalHandleValidationError(
|
|
146
162
|
error: unknown,
|
|
147
163
|
originalMessage: unknown,
|
|
148
164
|
): Promise<void> {
|
|
149
|
-
if (this.
|
|
150
|
-
await this.
|
|
165
|
+
if (this.schemaHandlers.handleValidationError) {
|
|
166
|
+
await this.schemaHandlers.handleValidationError(error, originalMessage);
|
|
151
167
|
} else {
|
|
152
|
-
// Default implementation logs and continues
|
|
153
168
|
console.error(
|
|
154
169
|
"Validation error:",
|
|
155
170
|
error,
|
|
@@ -159,50 +174,38 @@ export class ZodSession<
|
|
|
159
174
|
}
|
|
160
175
|
}
|
|
161
176
|
|
|
162
|
-
// Type-safe send method that automatically uses the correct format
|
|
163
177
|
public send(message: TServerMessage): void {
|
|
164
178
|
if (this.enableBufferMessages) {
|
|
165
|
-
this.
|
|
179
|
+
void this.sendBufferAsync(message).catch((error: unknown) => {
|
|
180
|
+
console.error("Failed to encode buffer message:", error);
|
|
181
|
+
});
|
|
166
182
|
} else {
|
|
167
|
-
this.
|
|
183
|
+
void this.sendJsonAsync(message).catch((error: unknown) => {
|
|
184
|
+
console.error("Invalid server message to send:", error);
|
|
185
|
+
});
|
|
168
186
|
}
|
|
169
187
|
}
|
|
170
188
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
this.websocket.send(this.serializeJson(validatedMessage));
|
|
180
|
-
} catch (error) {
|
|
181
|
-
console.error("Invalid server message to send:", error);
|
|
182
|
-
}
|
|
189
|
+
private async sendJsonAsync(message: TServerMessage): Promise<void> {
|
|
190
|
+
const validatedMessage = await parseStandardSchema(
|
|
191
|
+
this.options.serverSchema,
|
|
192
|
+
message,
|
|
193
|
+
);
|
|
194
|
+
if (this.websocket.readyState !== WebSocket.OPEN) return;
|
|
195
|
+
this.websocket.send(this.serializeJson(validatedMessage));
|
|
183
196
|
}
|
|
184
197
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if (this.websocket.readyState !== WebSocket.OPEN) return;
|
|
191
|
-
|
|
192
|
-
this.websocket.send(encodedMessage);
|
|
193
|
-
} catch (error) {
|
|
194
|
-
console.error("Failed to encode buffer message:", error);
|
|
195
|
-
}
|
|
198
|
+
private async sendBufferAsync(message: TServerMessage): Promise<void> {
|
|
199
|
+
const encodedMessage = await this.serverCodec.encode(message);
|
|
200
|
+
if (this.websocket.readyState !== WebSocket.OPEN) return;
|
|
201
|
+
this.websocket.send(encodedMessage);
|
|
196
202
|
}
|
|
197
203
|
|
|
198
|
-
// Send a protocol error message (always as JSON for compatibility by default)
|
|
199
204
|
private async sendProtocolError(errorMessage: string): Promise<void> {
|
|
200
205
|
try {
|
|
201
|
-
// Use custom handler if provided, otherwise use default
|
|
202
206
|
if (this.options.sendProtocolError) {
|
|
203
207
|
await this.options.sendProtocolError(this.websocket, errorMessage);
|
|
204
208
|
} else {
|
|
205
|
-
// Default implementation: send a simple error object - no schema validation needed
|
|
206
209
|
if (this.websocket.readyState !== WebSocket.OPEN) return;
|
|
207
210
|
this.websocket.send(this.serializeJson({ error: errorMessage }));
|
|
208
211
|
}
|
|
@@ -223,13 +226,11 @@ export class ZodSession<
|
|
|
223
226
|
: JSON.parse(raw);
|
|
224
227
|
}
|
|
225
228
|
|
|
226
|
-
// Type-safe broadcast that validates server messages
|
|
227
|
-
// Automatically uses the correct format based on session configuration
|
|
228
229
|
public broadcast(message: TServerMessage, excludeSelf = false): void {
|
|
229
230
|
for (const session of this.sessions.values()) {
|
|
230
231
|
if (excludeSelf && session === this) continue;
|
|
231
|
-
if (session instanceof
|
|
232
|
-
session.send(message);
|
|
232
|
+
if (session instanceof StandardSchemaSession) {
|
|
233
|
+
session.send(message);
|
|
233
234
|
}
|
|
234
235
|
}
|
|
235
236
|
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
1
2
|
import { pack, unpack } from "msgpackr";
|
|
2
|
-
import
|
|
3
|
+
import { parseStandardSchema } from "./parseStandardSchema";
|
|
3
4
|
|
|
4
|
-
export interface
|
|
5
|
+
export interface StandardSchemaWebSocketClientOptions<
|
|
6
|
+
TClientMessage,
|
|
7
|
+
TServerMessage,
|
|
8
|
+
> {
|
|
5
9
|
/**
|
|
6
10
|
* URL to connect to (required if webSocket not provided)
|
|
7
11
|
*/
|
|
@@ -11,8 +15,8 @@ export interface ZodWebSocketClientOptions<TClientMessage, TServerMessage> {
|
|
|
11
15
|
* Useful when getting a WebSocket from honoDoFetcher
|
|
12
16
|
*/
|
|
13
17
|
webSocket?: WebSocket;
|
|
14
|
-
clientSchema:
|
|
15
|
-
serverSchema:
|
|
18
|
+
clientSchema: StandardSchemaV1<unknown, TClientMessage>;
|
|
19
|
+
serverSchema: StandardSchemaV1<unknown, TServerMessage>;
|
|
16
20
|
serializeJson?: (value: unknown) => string;
|
|
17
21
|
deserializeJson?: (raw: string) => unknown;
|
|
18
22
|
enableBufferMessages?: boolean;
|
|
@@ -23,10 +27,10 @@ export interface ZodWebSocketClientOptions<TClientMessage, TServerMessage> {
|
|
|
23
27
|
onValidationError?: (error: Error, rawMessage: unknown) => void;
|
|
24
28
|
}
|
|
25
29
|
|
|
26
|
-
export class
|
|
30
|
+
export class StandardSchemaWebSocketClient<TClientMessage, TServerMessage> {
|
|
27
31
|
private ws: WebSocket;
|
|
28
|
-
private readonly clientSchema:
|
|
29
|
-
private readonly serverSchema:
|
|
32
|
+
private readonly clientSchema: StandardSchemaV1<unknown, TClientMessage>;
|
|
33
|
+
private readonly serverSchema: StandardSchemaV1<unknown, TServerMessage>;
|
|
30
34
|
private readonly serializeJson: (value: unknown) => string;
|
|
31
35
|
private readonly deserializeJson: (raw: string) => unknown;
|
|
32
36
|
private readonly enableBufferMessages: boolean;
|
|
@@ -37,7 +41,10 @@ export class ZodWebSocketClient<TClientMessage, TServerMessage> {
|
|
|
37
41
|
) => void;
|
|
38
42
|
|
|
39
43
|
constructor(
|
|
40
|
-
options:
|
|
44
|
+
options: StandardSchemaWebSocketClientOptions<
|
|
45
|
+
TClientMessage,
|
|
46
|
+
TServerMessage
|
|
47
|
+
>,
|
|
41
48
|
) {
|
|
42
49
|
this.clientSchema = options.clientSchema;
|
|
43
50
|
this.serverSchema = options.serverSchema;
|
|
@@ -47,29 +54,24 @@ export class ZodWebSocketClient<TClientMessage, TServerMessage> {
|
|
|
47
54
|
this.onMessageCallback = options.onMessage;
|
|
48
55
|
this.onValidationError = options.onValidationError;
|
|
49
56
|
|
|
50
|
-
// Use provided WebSocket or create new one from URL
|
|
51
57
|
if (options.webSocket) {
|
|
52
|
-
// Use existing WebSocket (e.g., from honoDoFetcher)
|
|
53
58
|
this.ws = options.webSocket;
|
|
54
59
|
} else if (options.url) {
|
|
55
|
-
// Create new WebSocket from URL
|
|
56
60
|
this.ws = new WebSocket(options.url);
|
|
57
61
|
} else {
|
|
58
62
|
throw new Error("Either 'url' or 'webSocket' must be provided");
|
|
59
63
|
}
|
|
60
64
|
|
|
61
|
-
// Set binary type for buffer messages
|
|
62
65
|
if (this.enableBufferMessages) {
|
|
63
66
|
this.ws.binaryType = "arraybuffer";
|
|
64
67
|
}
|
|
65
68
|
|
|
66
|
-
// Setup event handlers
|
|
67
69
|
this.ws.addEventListener("open", (event) => {
|
|
68
70
|
options.onOpen?.(event);
|
|
69
71
|
});
|
|
70
72
|
|
|
71
73
|
this.ws.addEventListener("message", (event) => {
|
|
72
|
-
this.
|
|
74
|
+
void this.handleMessageEvent(event);
|
|
73
75
|
});
|
|
74
76
|
|
|
75
77
|
this.ws.addEventListener("close", (event) => {
|
|
@@ -81,12 +83,11 @@ export class ZodWebSocketClient<TClientMessage, TServerMessage> {
|
|
|
81
83
|
});
|
|
82
84
|
}
|
|
83
85
|
|
|
84
|
-
private
|
|
86
|
+
private async handleMessageEvent(event: MessageEvent): Promise<void> {
|
|
85
87
|
try {
|
|
86
88
|
let parsedMessage: TServerMessage;
|
|
87
89
|
|
|
88
90
|
if (this.enableBufferMessages) {
|
|
89
|
-
// Buffer mode: expect ArrayBuffer
|
|
90
91
|
if (!(event.data instanceof ArrayBuffer)) {
|
|
91
92
|
console.error(
|
|
92
93
|
"Expected ArrayBuffer but received:",
|
|
@@ -99,11 +100,9 @@ export class ZodWebSocketClient<TClientMessage, TServerMessage> {
|
|
|
99
100
|
return;
|
|
100
101
|
}
|
|
101
102
|
|
|
102
|
-
// Unpack and validate
|
|
103
103
|
const unpacked = unpack(new Uint8Array(event.data));
|
|
104
|
-
parsedMessage = this.serverSchema
|
|
104
|
+
parsedMessage = await parseStandardSchema(this.serverSchema, unpacked);
|
|
105
105
|
} else {
|
|
106
|
-
// JSON mode: expect string
|
|
107
106
|
if (typeof event.data !== "string") {
|
|
108
107
|
console.error("Expected string but received:", typeof event.data);
|
|
109
108
|
this.onValidationError?.(
|
|
@@ -113,12 +112,10 @@ export class ZodWebSocketClient<TClientMessage, TServerMessage> {
|
|
|
113
112
|
return;
|
|
114
113
|
}
|
|
115
114
|
|
|
116
|
-
// Parse and validate
|
|
117
115
|
const parsed = this.deserializeJson(event.data);
|
|
118
|
-
parsedMessage = this.serverSchema
|
|
116
|
+
parsedMessage = await parseStandardSchema(this.serverSchema, parsed);
|
|
119
117
|
}
|
|
120
118
|
|
|
121
|
-
// Call message handler
|
|
122
119
|
this.onMessageCallback?.(parsedMessage);
|
|
123
120
|
} catch (error) {
|
|
124
121
|
console.error("Failed to process message:", error);
|
|
@@ -130,51 +127,34 @@ export class ZodWebSocketClient<TClientMessage, TServerMessage> {
|
|
|
130
127
|
}
|
|
131
128
|
|
|
132
129
|
/**
|
|
133
|
-
* Send a message (automatically encodes based on mode)
|
|
130
|
+
* Send a message (automatically encodes based on mode).
|
|
134
131
|
*/
|
|
135
|
-
send(message: TClientMessage): void {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
132
|
+
async send(message: TClientMessage): Promise<void> {
|
|
133
|
+
const validatedMessage = await parseStandardSchema(
|
|
134
|
+
this.clientSchema,
|
|
135
|
+
message,
|
|
136
|
+
);
|
|
139
137
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
// Encode as JSON
|
|
146
|
-
this.ws.send(this.serializeJson(validatedMessage));
|
|
147
|
-
}
|
|
148
|
-
} catch (error) {
|
|
149
|
-
console.error("Failed to send message:", error);
|
|
150
|
-
throw error;
|
|
138
|
+
if (this.enableBufferMessages) {
|
|
139
|
+
const packed = pack(validatedMessage);
|
|
140
|
+
this.ws.send(new Uint8Array(packed));
|
|
141
|
+
} else {
|
|
142
|
+
this.ws.send(this.serializeJson(validatedMessage));
|
|
151
143
|
}
|
|
152
144
|
}
|
|
153
145
|
|
|
154
|
-
/**
|
|
155
|
-
* Close the WebSocket connection
|
|
156
|
-
*/
|
|
157
146
|
close(code?: number, reason?: string): void {
|
|
158
147
|
this.ws.close(code, reason);
|
|
159
148
|
}
|
|
160
149
|
|
|
161
|
-
/**
|
|
162
|
-
* Get the current WebSocket ready state
|
|
163
|
-
*/
|
|
164
150
|
get readyState(): number {
|
|
165
151
|
return this.ws.readyState;
|
|
166
152
|
}
|
|
167
153
|
|
|
168
|
-
/**
|
|
169
|
-
* Get the underlying WebSocket instance (use with caution)
|
|
170
|
-
*/
|
|
171
154
|
get socket(): WebSocket {
|
|
172
155
|
return this.ws;
|
|
173
156
|
}
|
|
174
157
|
|
|
175
|
-
/**
|
|
176
|
-
* Wait for the connection to open
|
|
177
|
-
*/
|
|
178
158
|
async waitForOpen(): Promise<void> {
|
|
179
159
|
if (this.ws.readyState === WebSocket.OPEN) {
|
|
180
160
|
return;
|
|
@@ -5,62 +5,65 @@ import type {
|
|
|
5
5
|
SessionServerMessage,
|
|
6
6
|
} from "./BaseSession";
|
|
7
7
|
import { BaseWebSocketDO } from "./BaseWebSocketDO";
|
|
8
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
StandardSchemaSession,
|
|
10
|
+
StandardSchemaSessionOptions,
|
|
11
|
+
} from "./StandardSchemaSession";
|
|
9
12
|
|
|
10
|
-
export type
|
|
13
|
+
export type StandardSchemaSessionOptionsOrFactory<
|
|
11
14
|
TClientMessage,
|
|
12
15
|
TServerMessage,
|
|
13
16
|
TEnv extends Cloudflare.Env = Cloudflare.Env,
|
|
14
17
|
> =
|
|
15
|
-
|
|
|
18
|
+
| StandardSchemaSessionOptions<TClientMessage, TServerMessage>
|
|
16
19
|
| ((
|
|
17
20
|
ctx: Context<{ Bindings: TEnv }> | undefined,
|
|
18
21
|
websocket: WebSocket,
|
|
19
|
-
) =>
|
|
22
|
+
) => StandardSchemaSessionOptions<TClientMessage, TServerMessage>);
|
|
20
23
|
|
|
21
|
-
export type
|
|
24
|
+
export type StandardSchemaWebSocketDOOptions<
|
|
22
25
|
// biome-ignore lint/suspicious/noExplicitAny: We are using any on purpose to allow any type of session.
|
|
23
|
-
TSession extends
|
|
26
|
+
TSession extends StandardSchemaSession<any, any, any, any>,
|
|
24
27
|
TClientMessage,
|
|
25
28
|
TServerMessage,
|
|
26
29
|
TEnv extends SessionEnv<TSession>,
|
|
27
30
|
> = {
|
|
28
|
-
|
|
31
|
+
standardSchemaSessionOptions: StandardSchemaSessionOptionsOrFactory<
|
|
29
32
|
TClientMessage,
|
|
30
33
|
TServerMessage,
|
|
31
34
|
TEnv
|
|
32
35
|
>;
|
|
33
|
-
|
|
36
|
+
createStandardSchemaSession: (
|
|
34
37
|
ctx: Context<{ Bindings: TEnv }> | undefined,
|
|
35
38
|
websocket: WebSocket,
|
|
36
|
-
options:
|
|
39
|
+
options: StandardSchemaSessionOptions<TClientMessage, TServerMessage>,
|
|
37
40
|
) => TSession | Promise<TSession>;
|
|
38
41
|
};
|
|
39
42
|
|
|
40
|
-
export abstract class
|
|
43
|
+
export abstract class StandardSchemaWebSocketDO<
|
|
41
44
|
// biome-ignore lint/suspicious/noExplicitAny: We are using any on purpose to allow any type of session.
|
|
42
|
-
TSession extends
|
|
45
|
+
TSession extends StandardSchemaSession<any, any, any, any>,
|
|
43
46
|
TClientMessage extends
|
|
44
47
|
SessionClientMessage<TSession> = SessionClientMessage<TSession>,
|
|
45
48
|
TServerMessage extends
|
|
46
49
|
SessionServerMessage<TSession> = SessionServerMessage<TSession>,
|
|
47
50
|
TEnv extends SessionEnv<TSession> = SessionEnv<TSession>,
|
|
48
51
|
> extends BaseWebSocketDO<TSession, TEnv> {
|
|
49
|
-
protected readonly
|
|
52
|
+
protected readonly standardSchemaSessionOptions: StandardSchemaSessionOptionsOrFactory<
|
|
50
53
|
TClientMessage,
|
|
51
54
|
TServerMessage,
|
|
52
55
|
TEnv
|
|
53
56
|
>;
|
|
54
|
-
protected readonly
|
|
57
|
+
protected readonly createStandardSchemaSessionFn: (
|
|
55
58
|
ctx: Context<{ Bindings: TEnv }> | undefined,
|
|
56
59
|
websocket: WebSocket,
|
|
57
|
-
options:
|
|
60
|
+
options: StandardSchemaSessionOptions<TClientMessage, TServerMessage>,
|
|
58
61
|
) => TSession | Promise<TSession>;
|
|
59
62
|
|
|
60
63
|
constructor(
|
|
61
64
|
ctx: DurableObjectState,
|
|
62
65
|
env: TEnv,
|
|
63
|
-
options:
|
|
66
|
+
options: StandardSchemaWebSocketDOOptions<
|
|
64
67
|
TSession,
|
|
65
68
|
TClientMessage,
|
|
66
69
|
TServerMessage,
|
|
@@ -69,15 +72,19 @@ export abstract class ZodWebSocketDO<
|
|
|
69
72
|
) {
|
|
70
73
|
super(ctx, env, {
|
|
71
74
|
createSession: (ctx, websocket) => {
|
|
72
|
-
const
|
|
73
|
-
typeof options.
|
|
74
|
-
? options.
|
|
75
|
-
: options.
|
|
75
|
+
const schemaOptions =
|
|
76
|
+
typeof options.standardSchemaSessionOptions === "function"
|
|
77
|
+
? options.standardSchemaSessionOptions(ctx, websocket)
|
|
78
|
+
: options.standardSchemaSessionOptions;
|
|
76
79
|
|
|
77
|
-
return options.
|
|
80
|
+
return options.createStandardSchemaSession(
|
|
81
|
+
ctx,
|
|
82
|
+
websocket,
|
|
83
|
+
schemaOptions,
|
|
84
|
+
);
|
|
78
85
|
},
|
|
79
86
|
});
|
|
80
|
-
this.
|
|
81
|
-
this.
|
|
87
|
+
this.standardSchemaSessionOptions = options.standardSchemaSessionOptions;
|
|
88
|
+
this.createStandardSchemaSessionFn = options.createStandardSchemaSession;
|
|
82
89
|
}
|
|
83
90
|
}
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,8 @@ export {
|
|
|
2
2
|
BaseSession,
|
|
3
3
|
type BaseSessionHandlers,
|
|
4
4
|
type SessionClientMessage,
|
|
5
|
+
type SessionEnv,
|
|
6
|
+
type SessionServerMessage,
|
|
5
7
|
} from "./BaseSession";
|
|
6
8
|
export {
|
|
7
9
|
BaseWebSocketDO,
|
|
@@ -9,17 +11,18 @@ export {
|
|
|
9
11
|
} from "./BaseWebSocketDO";
|
|
10
12
|
export { WebsocketWrapper } from "./WebsocketWrapper";
|
|
11
13
|
export {
|
|
12
|
-
|
|
13
|
-
type
|
|
14
|
-
type
|
|
15
|
-
} from "./
|
|
14
|
+
StandardSchemaSession,
|
|
15
|
+
type StandardSchemaSessionHandlers,
|
|
16
|
+
type StandardSchemaSessionOptions,
|
|
17
|
+
} from "./StandardSchemaSession";
|
|
16
18
|
export {
|
|
17
|
-
|
|
18
|
-
type
|
|
19
|
-
} from "./
|
|
19
|
+
StandardSchemaWebSocketClient,
|
|
20
|
+
type StandardSchemaWebSocketClientOptions,
|
|
21
|
+
} from "./StandardSchemaWebSocketClient";
|
|
20
22
|
export {
|
|
21
|
-
type
|
|
22
|
-
|
|
23
|
-
type
|
|
24
|
-
} from "./
|
|
25
|
-
export {
|
|
23
|
+
type StandardSchemaSessionOptionsOrFactory,
|
|
24
|
+
StandardSchemaWebSocketDO,
|
|
25
|
+
type StandardSchemaWebSocketDOOptions,
|
|
26
|
+
} from "./StandardSchemaWebSocketDO";
|
|
27
|
+
export { parseStandardSchema } from "./parseStandardSchema";
|
|
28
|
+
export { standardSchemaMsgpack } from "./standardSchemaMsgpack";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Validates {@link value} with a Standard Schema v1 schema and returns the output,
|
|
5
|
+
* or throws an {@link Error} whose message aggregates issue messages.
|
|
6
|
+
*/
|
|
7
|
+
export async function parseStandardSchema<T>(
|
|
8
|
+
schema: StandardSchemaV1<unknown, T>,
|
|
9
|
+
value: unknown,
|
|
10
|
+
): Promise<T> {
|
|
11
|
+
const result = await schema["~standard"].validate(value);
|
|
12
|
+
if (result.issues) {
|
|
13
|
+
const messages = result.issues.map((issue) => issue.message).join("; ");
|
|
14
|
+
throw new Error(messages || "Validation failed");
|
|
15
|
+
}
|
|
16
|
+
return result.value;
|
|
17
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
2
|
+
import { pack, unpack } from "msgpackr";
|
|
3
|
+
import { parseStandardSchema } from "./parseStandardSchema";
|
|
4
|
+
|
|
5
|
+
export const standardSchemaMsgpack = <T>(
|
|
6
|
+
schema: StandardSchemaV1<unknown, T>,
|
|
7
|
+
) => ({
|
|
8
|
+
async encode(value: T): Promise<Uint8Array> {
|
|
9
|
+
const validated = await parseStandardSchema(schema, value);
|
|
10
|
+
const packed = pack(validated);
|
|
11
|
+
return new Uint8Array(packed);
|
|
12
|
+
},
|
|
13
|
+
async decode(bytes: Uint8Array): Promise<T> {
|
|
14
|
+
const unpacked = unpack(bytes);
|
|
15
|
+
return parseStandardSchema(schema, unpacked);
|
|
16
|
+
},
|
|
17
|
+
});
|