@firtoz/websocket-do 9.0.0 → 12.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,7 +11,7 @@ Type-safe WebSocket session management for Cloudflare Durable Objects with Hono
11
11
  ## Features
12
12
 
13
13
  - 🔒 **Type-safe** - Full TypeScript support with generic types for messages and session data
14
- - ✨ **Zod Validation** - Runtime message validation with `ZodWebSocketClient` and `ZodSession`
14
+ - ✨ **Standard Schema validation** - Runtime message validation with `StandardSchemaWebSocketClient` and `StandardSchemaSession` (Zod, Valibot, ArkType, …)
15
15
  - 🌐 **WebSocket Management** - Built on Cloudflare Durable Objects for stateful WebSocket connections
16
16
  - 🎯 **Session-based** - Abstract session class for easy implementation of custom WebSocket logic
17
17
  - 🔄 **State Persistence** - Automatic serialization/deserialization of session data
@@ -34,10 +34,7 @@ This package requires the following peer dependencies:
34
34
  bun add hono @firtoz/hono-fetcher
35
35
  ```
36
36
 
37
- **For Zod validation features** (ZodWebSocketClient, ZodSession):
38
- ```bash
39
- bun add zod msgpackr
40
- ```
37
+ **For schemas:** use any [Standard Schema v1](https://standardschema.dev/) library (e.g. Zod, Valibot). **`msgpackr`** is a normal dependency of this package (buffer / msgpack mode); you do not add it separately unless your bundler needs it hoisted.
41
38
 
42
39
  For TypeScript support, use `wrangler types` to generate accurate types from your `wrangler.jsonc`:
43
40
 
@@ -101,8 +98,8 @@ const chatSessionHandlers: BaseSessionHandlers<
101
98
  // Handle binary messages if needed
102
99
  },
103
100
 
104
- handleClose: async () => {
105
- console.log('Session closed');
101
+ handleClose: async (session) => {
102
+ console.log('Session closed', session.data);
106
103
  },
107
104
  };
108
105
 
@@ -140,8 +137,8 @@ class ChatSession extends BaseSession<
140
137
  handleBufferMessage: async (message) => {
141
138
  // Handle binary messages if needed
142
139
  },
143
- handleClose: async () => {
144
- console.log(`Session closed for user ${this.data.userId}`);
140
+ handleClose: async (session) => {
141
+ console.log(`Session closed for user ${session.data.userId}`);
145
142
  },
146
143
  });
147
144
  }
@@ -214,13 +211,13 @@ export default {
214
211
  };
215
212
  ```
216
213
 
217
- ## ZodWebSocketClient (Type-Safe Client)
214
+ ## StandardSchemaWebSocketClient (Type-Safe Client)
218
215
 
219
- `ZodWebSocketClient` provides a type-safe WebSocket client with automatic Zod validation for both incoming and outgoing messages.
216
+ `StandardSchemaWebSocketClient` provides a type-safe WebSocket client with automatic validation (Standard Schema v1) for both incoming and outgoing messages.
220
217
 
221
218
  ### Features
222
219
 
223
- - ✅ **Automatic validation** - All messages validated with Zod schemas
220
+ - ✅ **Automatic validation** - All messages validated with your Standard Schema–compatible schemas
224
221
  - 🎯 **Full type inference** - TypeScript types automatically inferred from schemas
225
222
  - 📦 **Dual mode** - Supports both JSON and msgpack (buffer) serialization
226
223
  - 🔗 **DO Integration** - Works seamlessly with `honoDoFetcher` WebSocket connections
@@ -229,7 +226,7 @@ export default {
229
226
  ### Basic Usage
230
227
 
231
228
  ```typescript
232
- import { ZodWebSocketClient } from '@firtoz/websocket-do';
229
+ import { StandardSchemaWebSocketClient } from '@firtoz/websocket-do';
233
230
  import { z } from 'zod';
234
231
 
235
232
  // Define your message schemas
@@ -249,8 +246,8 @@ type ServerMessage = z.infer<typeof ServerMessageSchema>;
249
246
  // Create WebSocket connection (regular or via honoDoFetcher)
250
247
  const ws = new WebSocket('wss://example.com/chat');
251
248
 
252
- // Wrap with ZodWebSocketClient
253
- const client = new ZodWebSocketClient({
249
+ // Wrap with StandardSchemaWebSocketClient
250
+ const client = new StandardSchemaWebSocketClient({
254
251
  webSocket: ws, // Can also use 'url' instead
255
252
  clientSchema: ClientMessageSchema,
256
253
  serverSchema: ServerMessageSchema,
@@ -272,7 +269,7 @@ Perfect for connecting to Durable Objects:
272
269
 
273
270
  ```typescript
274
271
  import { honoDoFetcherWithName } from '@firtoz/hono-fetcher';
275
- import { ZodWebSocketClient } from '@firtoz/websocket-do';
272
+ import { StandardSchemaWebSocketClient } from '@firtoz/websocket-do';
276
273
 
277
274
  // 1. Connect to DO via honoDoFetcher
278
275
  const api = honoDoFetcherWithName(env.CHAT_ROOM, 'room-1');
@@ -281,8 +278,8 @@ const wsResp = await api.websocket({
281
278
  config: { autoAccept: false }, // Let client control acceptance
282
279
  });
283
280
 
284
- // 2. Wrap with ZodWebSocketClient for type safety!
285
- const client = new ZodWebSocketClient({
281
+ // 2. Wrap with StandardSchemaWebSocketClient for type safety!
282
+ const client = new StandardSchemaWebSocketClient({
286
283
  webSocket: wsResp.webSocket,
287
284
  clientSchema: ClientMessageSchema,
288
285
  serverSchema: ServerMessageSchema,
@@ -307,7 +304,7 @@ client.send({ type: 'chat', text: 'Hello from typed client!' });
307
304
  For better performance and smaller payloads, use buffer mode with msgpack:
308
305
 
309
306
  ```typescript
310
- const client = new ZodWebSocketClient({
307
+ const client = new StandardSchemaWebSocketClient({
311
308
  webSocket: ws,
312
309
  clientSchema: ClientMessageSchema,
313
310
  serverSchema: ServerMessageSchema,
@@ -327,14 +324,16 @@ client.send({ type: 'chat', text: 'Efficient binary message!' });
327
324
  #### Constructor Options
328
325
 
329
326
  ```typescript
330
- interface ZodWebSocketClientOptions<TClientMessage, TServerMessage> {
327
+ import type { StandardSchemaV1 } from "@standard-schema/spec";
328
+
329
+ interface StandardSchemaWebSocketClientOptions<TClientMessage, TServerMessage> {
331
330
  // Connection (provide one)
332
331
  url?: string; // Create new WebSocket
333
332
  webSocket?: WebSocket; // Use existing WebSocket (e.g., from honoDoFetcher)
334
333
 
335
334
  // Schemas (required)
336
- clientSchema: z.ZodType<TClientMessage>;
337
- serverSchema: z.ZodType<TServerMessage>;
335
+ clientSchema: StandardSchemaV1<unknown, TClientMessage>;
336
+ serverSchema: StandardSchemaV1<unknown, TServerMessage>;
338
337
 
339
338
  // Serialization
340
339
  enableBufferMessages?: boolean; // Use msgpack instead of JSON (default: false)
@@ -350,18 +349,18 @@ interface ZodWebSocketClientOptions<TClientMessage, TServerMessage> {
350
349
 
351
350
  #### Methods
352
351
 
353
- - `send(message: TClientMessage): void` - Send a validated message
352
+ - `send(message: TClientMessage): Promise<void>` - Send a validated message (async Standard Schema validation)
354
353
  - `close(code?: number, reason?: string): void` - Close the connection
355
354
  - `waitForOpen(): Promise<void>` - Wait for connection to open
356
355
 
357
- ## ZodSession (Validated Sessions)
356
+ ## StandardSchemaSession (Validated Sessions)
358
357
 
359
- `ZodSession` extends `BaseSession` with automatic Zod validation for incoming messages.
358
+ `StandardSchemaSession` extends `BaseSession` with automatic validation for incoming messages.
360
359
 
361
360
  ### Basic Usage
362
361
 
363
362
  ```typescript
364
- import { ZodSession } from '@firtoz/websocket-do';
363
+ import { StandardSchemaSession } from '@firtoz/websocket-do';
365
364
  import { z } from 'zod';
366
365
 
367
366
  // Define schemas
@@ -384,7 +383,7 @@ interface SessionData {
384
383
  }
385
384
 
386
385
  // Implement validated session
387
- class ChatSession extends ZodSession<
386
+ class ChatSession extends StandardSchemaSession<
388
387
  SessionData,
389
388
  ServerMessage,
390
389
  ClientMessage,
@@ -393,7 +392,7 @@ class ChatSession extends ZodSession<
393
392
  constructor(
394
393
  websocket: WebSocket,
395
394
  sessions: Map<WebSocket, ChatSession>,
396
- options: ZodSessionOptions<ClientMessage, ServerMessage>
395
+ options: StandardSchemaSessionOptions<ClientMessage, ServerMessage>
397
396
  ) {
398
397
  super(websocket, sessions, options, {
399
398
  createData: (ctx) => ({ name: 'Anonymous' }),
@@ -417,18 +416,18 @@ class ChatSession extends ZodSession<
417
416
  }
418
417
  },
419
418
 
420
- handleClose: async () => {
421
- console.log(`${this.data.name} disconnected`);
419
+ handleClose: async (session) => {
420
+ console.log(`${session.data.name} disconnected`);
422
421
  },
423
422
  });
424
423
  }
425
424
  }
426
425
  ```
427
426
 
428
- ### Buffer Mode with ZodSession
427
+ ### Buffer Mode with StandardSchemaSession
429
428
 
430
429
  ```typescript
431
- class ChatSession extends ZodSession<...> {
430
+ class ChatSession extends StandardSchemaSession<...> {
432
431
  constructor(
433
432
  websocket: WebSocket,
434
433
  sessions: Map<WebSocket, ChatSession>
@@ -443,8 +442,8 @@ class ChatSession extends ZodSession<...> {
443
442
  // Messages automatically decoded from msgpack
444
443
  // Handle validated message
445
444
  },
446
- handleClose: async () => {
447
- console.log('Session closed');
445
+ handleClose: async (session) => {
446
+ console.log('Session closed', session.data);
448
447
  },
449
448
  });
450
449
  }
@@ -521,13 +520,17 @@ constructor(
521
520
 
522
521
  ```typescript
523
522
  type BaseSessionHandlers<TData, TServerMessage, TClientMessage, TEnv> = {
524
- createData: (ctx: Context<{ Bindings: TEnv }>) => TData;
523
+ createData?: (ctx: Context<{ Bindings: TEnv }>) => TData;
525
524
  handleMessage: (message: TClientMessage) => Promise<void>;
526
525
  handleBufferMessage: (message: ArrayBuffer) => Promise<void>;
527
- handleClose: () => Promise<void>;
526
+ handleClose: (
527
+ session: BaseSession<TData, TServerMessage, TClientMessage, TEnv>,
528
+ ) => Promise<void>;
528
529
  };
529
530
  ```
530
531
 
532
+ If `createData` is omitted, `startFresh` sets `data` to `{}` (use an empty `TData` such as `Record<string, never>`).
533
+
531
534
  #### Methods
532
535
 
533
536
  - `send(message: TServerMessage): void`
@@ -537,7 +540,7 @@ type BaseSessionHandlers<TData, TServerMessage, TClientMessage, TEnv> = {
537
540
  - Send message to all connected sessions
538
541
 
539
542
  - `startFresh(ctx: Context): void`
540
- - Initialize new session (called automatically)
543
+ - Initialize new session (called automatically). Uses `createData` when provided; otherwise `{}`.
541
544
 
542
545
  - `resume(): void`
543
546
  - Resume existing session after hibernation (called automatically)
@@ -635,8 +638,8 @@ class GameSession extends BaseSession<GameData, ServerMsg, ClientMsg, Env> {
635
638
  // Handle buffer messages if needed
636
639
  },
637
640
 
638
- handleClose: async () => {
639
- console.log('Game session closed');
641
+ handleClose: async (session) => {
642
+ console.log('Game session closed', session.data);
640
643
  },
641
644
  });
642
645
  }
@@ -678,8 +681,8 @@ class MySession extends BaseSession<...> {
678
681
  // Handle buffer messages
679
682
  },
680
683
 
681
- handleClose: async () => {
682
- console.log('Session closed');
684
+ handleClose: async (session) => {
685
+ console.log('Session closed', session.data);
683
686
  },
684
687
  });
685
688
  }
@@ -693,22 +696,22 @@ This package exports the following:
693
696
  ### Classes
694
697
  - `BaseWebSocketDO` - Base class for WebSocket Durable Objects (composition-based)
695
698
  - `BaseSession` - Concrete session class with handler injection
696
- - `ZodWebSocketClient` - Type-safe WebSocket client with Zod validation
697
- - `ZodSession` - Concrete session class with Zod validation and handler injection
698
- - `ZodWebSocketDO` - Base class for WebSocket DOs with Zod validation
699
+ - `StandardSchemaWebSocketClient` - Type-safe WebSocket client with Standard Schema validation
700
+ - `StandardSchemaSession` - Concrete session class with validation and handler injection
701
+ - `StandardSchemaWebSocketDO` - Base class for WebSocket DOs with Standard Schema sessions
699
702
  - `WebsocketWrapper` - Low-level WebSocket wrapper with typed attachments
700
703
 
701
704
  ### Types
702
705
  - `BaseSessionHandlers` - Handler interface for `BaseSession`
703
706
  - `BaseWebSocketDOOptions` - Options interface for `BaseWebSocketDO`
704
- - `ZodSessionHandlers` - Handler interface for `ZodSession`
705
- - `ZodSessionOptions` - Options interface for `ZodSession`
706
- - `ZodSessionOptionsOrFactory` - Options or factory function for `ZodSession`
707
- - `ZodWebSocketDOOptions` - Options interface for `ZodWebSocketDO`
708
- - `ZodWebSocketClientOptions` - Options interface for `ZodWebSocketClient`
707
+ - `StandardSchemaSessionHandlers` - Handler interface for `StandardSchemaSession`
708
+ - `StandardSchemaSessionOptions` - Options interface for `StandardSchemaSession`
709
+ - `StandardSchemaSessionOptionsOrFactory` - Options or factory function for `StandardSchemaSession`
710
+ - `StandardSchemaWebSocketDOOptions` - Options interface for `StandardSchemaWebSocketDO`
711
+ - `StandardSchemaWebSocketClientOptions` - Options interface for `StandardSchemaWebSocketClient`
709
712
 
710
713
  ### Utilities
711
- - `zodMsgpack` - Msgpack encode/decode with Zod validation
714
+ - `standardSchemaMsgpack` - Msgpack encode/decode with Standard Schema validation
712
715
 
713
716
  ## Complete Example
714
717
 
@@ -734,7 +737,7 @@ export type ClientMessage = z.infer<typeof ClientMessageSchema>;
734
737
  export type ServerMessage = z.infer<typeof ServerMessageSchema>;
735
738
 
736
739
  // do.ts - Server-side (Durable Object)
737
- import { BaseWebSocketDO, ZodSession, type ZodSessionOptions } from '@firtoz/websocket-do';
740
+ import { BaseWebSocketDO, StandardSchemaSession, type StandardSchemaSessionOptions } from '@firtoz/websocket-do';
738
741
  import { ClientMessageSchema, ServerMessageSchema } from './schemas';
739
742
 
740
743
  interface SessionData {
@@ -742,11 +745,11 @@ interface SessionData {
742
745
  joinedAt: number;
743
746
  }
744
747
 
745
- class ChatSession extends ZodSession<SessionData, ServerMessage, ClientMessage, Env> {
748
+ class ChatSession extends StandardSchemaSession<SessionData, ServerMessage, ClientMessage, Env> {
746
749
  constructor(
747
750
  websocket: WebSocket,
748
751
  sessions: Map<WebSocket, ChatSession>,
749
- options: ZodSessionOptions<ClientMessage, ServerMessage>
752
+ options: StandardSchemaSessionOptions<ClientMessage, ServerMessage>
750
753
  ) {
751
754
  super(websocket, sessions, options, {
752
755
  createData: () => ({
@@ -775,8 +778,8 @@ class ChatSession extends ZodSession<SessionData, ServerMessage, ClientMessage,
775
778
  }
776
779
  },
777
780
 
778
- handleClose: async () => {
779
- console.log(`${this.data.name} disconnected`);
781
+ handleClose: async (session) => {
782
+ console.log(`${session.data.name} disconnected`);
780
783
  },
781
784
  });
782
785
  }
@@ -806,7 +809,7 @@ export class ChatRoomDO extends BaseWebSocketDO<ChatSession, Env> {
806
809
  }
807
810
 
808
811
  // client.ts - Client-side
809
- import { ZodWebSocketClient } from '@firtoz/websocket-do';
812
+ import { StandardSchemaWebSocketClient } from '@firtoz/websocket-do';
810
813
  import { honoDoFetcherWithName } from '@firtoz/hono-fetcher';
811
814
  import { ClientMessageSchema, ServerMessageSchema } from './schemas';
812
815
 
@@ -818,8 +821,8 @@ async function connectToChat(env: Env, roomName: string) {
818
821
  config: { autoAccept: false },
819
822
  });
820
823
 
821
- // 2. Wrap with ZodWebSocketClient
822
- const client = new ZodWebSocketClient({
824
+ // 2. Wrap with StandardSchemaWebSocketClient
825
+ const client = new StandardSchemaWebSocketClient({
823
826
  webSocket: wsResp.webSocket,
824
827
  clientSchema: ClientMessageSchema,
825
828
  serverSchema: ServerMessageSchema,
@@ -868,8 +871,8 @@ This package includes comprehensive integration tests in a separate test package
868
871
  - ✅ Real-time WebSocket message exchange
869
872
  - ✅ WebSocket session management
870
873
  - ✅ Type-safe DO client integration
871
- - ✅ Zod validation in both JSON and msgpack modes
872
- - ✅ Integration between honoDoFetcher and ZodWebSocketClient
874
+ - ✅ Standard Schema validation in both JSON and msgpack modes
875
+ - ✅ Integration between honoDoFetcher and StandardSchemaWebSocketClient
873
876
 
874
877
  For detailed information about testing capabilities, example implementations, comprehensive test coverage, and setup instructions, see the [websocket-do-test](../../tests/websocket-do-test/) package.
875
878
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@firtoz/websocket-do",
3
- "version": "9.0.0",
3
+ "version": "12.0.0",
4
4
  "description": "Type-safe WebSocket session management for Cloudflare Durable Objects with Hono integration",
5
5
  "main": "./src/index.ts",
6
6
  "module": "./src/index.ts",
@@ -11,10 +11,20 @@
11
11
  "import": "./src/index.ts",
12
12
  "require": "./src/index.ts"
13
13
  },
14
- "./zod-client": {
15
- "types": "./src/ZodWebSocketClient.ts",
16
- "import": "./src/ZodWebSocketClient.ts",
17
- "require": "./src/ZodWebSocketClient.ts"
14
+ "./schema-client": {
15
+ "types": "./src/StandardSchemaWebSocketClient.ts",
16
+ "import": "./src/StandardSchemaWebSocketClient.ts",
17
+ "require": "./src/StandardSchemaWebSocketClient.ts"
18
+ },
19
+ "./rpc": {
20
+ "types": "./src/standardSchemaRpc.ts",
21
+ "import": "./src/standardSchemaRpc.ts",
22
+ "require": "./src/standardSchemaRpc.ts"
23
+ },
24
+ "./rpc-react": {
25
+ "types": "./src/standardSchemaRpcReact.ts",
26
+ "import": "./src/standardSchemaRpcReact.ts",
27
+ "require": "./src/standardSchemaRpcReact.ts"
18
28
  }
19
29
  },
20
30
  "files": [
@@ -51,13 +61,18 @@
51
61
  },
52
62
  "peerDependencies": {
53
63
  "@cloudflare/workers-types": "^4.20260329.1",
54
- "@firtoz/hono-fetcher": "^2.5.0",
55
- "hono": "^4.12.9"
64
+ "@firtoz/hono-fetcher": "^2.6.0",
65
+ "hono": "^4.12.9",
66
+ "react": ">=18.0.0"
56
67
  },
57
- "optionalDependencies": {
58
- "msgpackr": "^1.11.9",
59
- "react": "^19.2.4",
60
- "zod": "^4.3.6"
68
+ "peerDependenciesMeta": {
69
+ "react": {
70
+ "optional": true
71
+ }
72
+ },
73
+ "dependencies": {
74
+ "@standard-schema/spec": "^1.1.0",
75
+ "msgpackr": "^1.11.9"
61
76
  },
62
77
  "engines": {
63
78
  "node": ">=18.0.0"
@@ -68,6 +83,7 @@
68
83
  "devDependencies": {
69
84
  "@types/react": "^19.2.14",
70
85
  "bun-types": "^1.3.11",
71
- "typescript": "^6.0.2"
86
+ "typescript": "^6.0.2",
87
+ "zod": "^4.3.6"
72
88
  }
73
89
  }
@@ -53,14 +53,21 @@ export type SessionEnv<
53
53
 
54
54
  export type BaseSessionHandlers<
55
55
  TData,
56
- _TServerMessage,
56
+ TServerMessage,
57
57
  TClientMessage,
58
58
  TEnv extends object = Cloudflare.Env,
59
59
  > = {
60
- createData: (ctx: Context<{ Bindings: TEnv }>) => TData;
60
+ /**
61
+ * Per-connection state. If omitted, `startFresh` initializes `data` as `{}`
62
+ * (use empty `TData`, e.g. `Record<string, never>`).
63
+ */
64
+ createData?: (ctx: Context<{ Bindings: TEnv }>) => TData;
61
65
  handleMessage: (message: TClientMessage) => Promise<void>;
62
66
  handleBufferMessage: (message: ArrayBuffer) => Promise<void>;
63
- handleClose: () => Promise<void>;
67
+ /** Called when this connection closes; `session` is this {@link BaseSession} instance. */
68
+ handleClose: (
69
+ session: BaseSession<TData, TServerMessage, TClientMessage, TEnv>,
70
+ ) => Promise<void>;
64
71
  };
65
72
 
66
73
  export class BaseSession<
@@ -100,7 +107,10 @@ export class BaseSession<
100
107
  }
101
108
 
102
109
  public startFresh(ctx: Context<{ Bindings: TEnv }>) {
103
- this.data = this.handlers.createData(ctx);
110
+ this.data =
111
+ this.handlers.createData !== undefined
112
+ ? this.handlers.createData(ctx)
113
+ : ({} as TData);
104
114
  this.wrapper.serializeAttachment(this.data);
105
115
  }
106
116
 
@@ -137,6 +147,6 @@ export class BaseSession<
137
147
  }
138
148
 
139
149
  async handleClose(): Promise<void> {
140
- return this.handlers.handleClose();
150
+ return this.handlers.handleClose(this);
141
151
  }
142
152
  }
@@ -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 { zodMsgpack } from "./zodMsgpack";
4
+ import { parseStandardSchema } from "./parseStandardSchema";
5
+ import { standardSchemaMsgpack } from "./standardSchemaMsgpack";
5
6
 
6
- export type ZodSessionOptions<TClientMessage, TServerMessage> = {
7
- clientSchema: ZodType<TClientMessage>;
8
- serverSchema: ZodType<TServerMessage>;
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 ZodSessionHandlers<
19
+ export type StandardSchemaSessionHandlers<
19
20
  TData,
20
- _TServerMessage,
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: () => Promise<void>;
31
+ handleClose: (
32
+ session: StandardSchemaSession<TData, TServerMessage, TClientMessage, TEnv>,
33
+ ) => Promise<void>;
31
34
  };
32
35
 
33
- export class ZodSession<
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<typeof zodMsgpack<TClientMessage>>;
40
- private readonly serverCodec: ReturnType<typeof zodMsgpack<TServerMessage>>;
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
- ZodSession<TData, TServerMessage, TClientMessage, TEnv>
54
+ StandardSchemaSession<TData, TServerMessage, TClientMessage, TEnv>
55
+ >,
56
+ private readonly options: StandardSchemaSessionOptions<
57
+ TClientMessage,
58
+ TServerMessage
48
59
  >,
49
- private readonly options: ZodSessionOptions<TClientMessage, TServerMessage>,
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: zodHandlers.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
- return zodHandlers.handleClose();
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 = zodMsgpack(options.clientSchema);
71
- this.serverCodec = zodMsgpack(options.serverSchema);
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 = this.options.clientSchema.parse(parsed);
90
- await this.zodHandlers.handleValidatedMessage(validatedMessage);
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
- // Validate the message using the client schema
112
- const validatedMessage = this.options.clientSchema.parse(message);
113
- await this.zodHandlers.handleValidatedMessage(validatedMessage);
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.zodHandlers.handleValidatedMessage(decodedMessage);
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.zodHandlers.handleValidationError) {
150
- await this.zodHandlers.handleValidationError(error, originalMessage);
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.sendBuffer(message);
179
+ void this.sendBufferAsync(message).catch((error: unknown) => {
180
+ console.error("Failed to encode buffer message:", error);
181
+ });
166
182
  } else {
167
- this.sendJson(message);
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
- // Explicitly send as JSON
172
- private sendJson(message: TServerMessage): void {
173
- try {
174
- // Validate the message using the server schema
175
- const validatedMessage = this.options.serverSchema.parse(message);
176
-
177
- if (this.websocket.readyState !== WebSocket.OPEN) return;
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
- // Explicitly send as buffer using msgpack
186
- private sendBuffer(message: TServerMessage): void {
187
- try {
188
- const encodedMessage = this.serverCodec.encode(message);
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 ZodSession) {
232
- session.send(message); // send() automatically uses correct format
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 type { ZodType } from "zod";
3
+ import { parseStandardSchema } from "./parseStandardSchema";
3
4
 
4
- export interface ZodWebSocketClientOptions<TClientMessage, TServerMessage> {
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: ZodType<TClientMessage>;
15
- serverSchema: ZodType<TServerMessage>;
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 ZodWebSocketClient<TClientMessage, TServerMessage> {
30
+ export class StandardSchemaWebSocketClient<TClientMessage, TServerMessage> {
27
31
  private ws: WebSocket;
28
- private readonly clientSchema: ZodType<TClientMessage>;
29
- private readonly serverSchema: ZodType<TServerMessage>;
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: ZodWebSocketClientOptions<TClientMessage, TServerMessage>,
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.handleMessage(event);
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 handleMessage(event: MessageEvent): void {
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.parse(unpacked);
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.parse(parsed);
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
- try {
137
- // Validate message
138
- const validatedMessage = this.clientSchema.parse(message);
132
+ async send(message: TClientMessage): Promise<void> {
133
+ const validatedMessage = await parseStandardSchema(
134
+ this.clientSchema,
135
+ message,
136
+ );
139
137
 
140
- if (this.enableBufferMessages) {
141
- // Encode as msgpack (ensure ArrayBufferView for WebSocket.send)
142
- const packed = pack(validatedMessage);
143
- this.ws.send(new Uint8Array(packed));
144
- } else {
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 { ZodSession, ZodSessionOptions } from "./ZodSession";
8
+ import type {
9
+ StandardSchemaSession,
10
+ StandardSchemaSessionOptions,
11
+ } from "./StandardSchemaSession";
9
12
 
10
- export type ZodSessionOptionsOrFactory<
13
+ export type StandardSchemaSessionOptionsOrFactory<
11
14
  TClientMessage,
12
15
  TServerMessage,
13
16
  TEnv extends Cloudflare.Env = Cloudflare.Env,
14
17
  > =
15
- | ZodSessionOptions<TClientMessage, TServerMessage>
18
+ | StandardSchemaSessionOptions<TClientMessage, TServerMessage>
16
19
  | ((
17
20
  ctx: Context<{ Bindings: TEnv }> | undefined,
18
21
  websocket: WebSocket,
19
- ) => ZodSessionOptions<TClientMessage, TServerMessage>);
22
+ ) => StandardSchemaSessionOptions<TClientMessage, TServerMessage>);
20
23
 
21
- export type ZodWebSocketDOOptions<
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 ZodSession<any, any, any, any>,
26
+ TSession extends StandardSchemaSession<any, any, any, any>,
24
27
  TClientMessage,
25
28
  TServerMessage,
26
29
  TEnv extends SessionEnv<TSession>,
27
30
  > = {
28
- zodSessionOptions: ZodSessionOptionsOrFactory<
31
+ standardSchemaSessionOptions: StandardSchemaSessionOptionsOrFactory<
29
32
  TClientMessage,
30
33
  TServerMessage,
31
34
  TEnv
32
35
  >;
33
- createZodSession: (
36
+ createStandardSchemaSession: (
34
37
  ctx: Context<{ Bindings: TEnv }> | undefined,
35
38
  websocket: WebSocket,
36
- options: ZodSessionOptions<TClientMessage, TServerMessage>,
39
+ options: StandardSchemaSessionOptions<TClientMessage, TServerMessage>,
37
40
  ) => TSession | Promise<TSession>;
38
41
  };
39
42
 
40
- export abstract class ZodWebSocketDO<
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 ZodSession<any, any, any, any>,
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 zodSessionOptions: ZodSessionOptionsOrFactory<
52
+ protected readonly standardSchemaSessionOptions: StandardSchemaSessionOptionsOrFactory<
50
53
  TClientMessage,
51
54
  TServerMessage,
52
55
  TEnv
53
56
  >;
54
- protected readonly createZodSessionFn: (
57
+ protected readonly createStandardSchemaSessionFn: (
55
58
  ctx: Context<{ Bindings: TEnv }> | undefined,
56
59
  websocket: WebSocket,
57
- options: ZodSessionOptions<TClientMessage, TServerMessage>,
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: ZodWebSocketDOOptions<
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 zodOptions =
73
- typeof options.zodSessionOptions === "function"
74
- ? options.zodSessionOptions(ctx, websocket)
75
- : options.zodSessionOptions;
75
+ const schemaOptions =
76
+ typeof options.standardSchemaSessionOptions === "function"
77
+ ? options.standardSchemaSessionOptions(ctx, websocket)
78
+ : options.standardSchemaSessionOptions;
76
79
 
77
- return options.createZodSession(ctx, websocket, zodOptions);
80
+ return options.createStandardSchemaSession(
81
+ ctx,
82
+ websocket,
83
+ schemaOptions,
84
+ );
78
85
  },
79
86
  });
80
- this.zodSessionOptions = options.zodSessionOptions;
81
- this.createZodSessionFn = options.createZodSession;
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
- ZodSession,
13
- type ZodSessionHandlers,
14
- type ZodSessionOptions,
15
- } from "./ZodSession";
14
+ StandardSchemaSession,
15
+ type StandardSchemaSessionHandlers,
16
+ type StandardSchemaSessionOptions,
17
+ } from "./StandardSchemaSession";
16
18
  export {
17
- ZodWebSocketClient,
18
- type ZodWebSocketClientOptions,
19
- } from "./ZodWebSocketClient";
19
+ StandardSchemaWebSocketClient,
20
+ type StandardSchemaWebSocketClientOptions,
21
+ } from "./StandardSchemaWebSocketClient";
20
22
  export {
21
- type ZodSessionOptionsOrFactory,
22
- ZodWebSocketDO,
23
- type ZodWebSocketDOOptions,
24
- } from "./ZodWebSocketDO";
25
- export { zodMsgpack } from "./zodMsgpack";
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
+ });
@@ -0,0 +1,83 @@
1
+ import {
2
+ type StandardSchemaWebSocketClientOptions,
3
+ StandardSchemaWebSocketClient,
4
+ } from "./StandardSchemaWebSocketClient";
5
+
6
+ export type StandardSchemaWebSocketRpcSessionConstructorOptions<
7
+ TClientMsg,
8
+ TServerMsg,
9
+ TPending extends { reject: (error: Error) => void },
10
+ > = Omit<
11
+ StandardSchemaWebSocketClientOptions<TClientMsg, TServerMsg>,
12
+ "onMessage"
13
+ > & {
14
+ onMessage: (
15
+ message: TServerMsg,
16
+ session: StandardSchemaWebSocketRpcSession<
17
+ TClientMsg,
18
+ TServerMsg,
19
+ TPending
20
+ >,
21
+ ) => void;
22
+ };
23
+
24
+ /**
25
+ * WebSocket client session with a pending map for request/response RPC and a
26
+ * monotonic id helper. Wire formats stay in your Standard Schema schemas; you dispatch
27
+ * {@link TServerMsg} in `onMessage` (typically with `switch` + `exhaustiveGuard`)
28
+ * and resolve/reject entries in {@link pending}.
29
+ */
30
+ export class StandardSchemaWebSocketRpcSession<
31
+ TClientMsg,
32
+ TServerMsg,
33
+ TPending extends { reject: (error: Error) => void },
34
+ > {
35
+ readonly pending = new Map<string, TPending>();
36
+ readonly client: StandardSchemaWebSocketClient<TClientMsg, TServerMsg>;
37
+ private idSeq = 0;
38
+
39
+ constructor(
40
+ options: StandardSchemaWebSocketRpcSessionConstructorOptions<
41
+ TClientMsg,
42
+ TServerMsg,
43
+ TPending
44
+ >,
45
+ ) {
46
+ const { onMessage, ...clientOptions } = options;
47
+ this.client = new StandardSchemaWebSocketClient({
48
+ ...clientOptions,
49
+ onMessage: (message) => {
50
+ onMessage(message, this);
51
+ },
52
+ });
53
+ }
54
+
55
+ nextId(prefix: string): string {
56
+ return `${prefix}-${++this.idSeq}`;
57
+ }
58
+
59
+ rejectAllPending(reason: Error): void {
60
+ for (const [, pending] of this.pending) {
61
+ pending.reject(reason);
62
+ }
63
+ this.pending.clear();
64
+ }
65
+
66
+ close(code?: number, reason?: string): void {
67
+ this.client.close(code, reason);
68
+ }
69
+ }
70
+
71
+ export function createStandardSchemaWebSocketRpcSession<
72
+ TClientMsg,
73
+ TServerMsg,
74
+ TPending extends { reject: (error: Error) => void },
75
+ >(
76
+ options: StandardSchemaWebSocketRpcSessionConstructorOptions<
77
+ TClientMsg,
78
+ TServerMsg,
79
+ TPending
80
+ >,
81
+ ): StandardSchemaWebSocketRpcSession<TClientMsg, TServerMsg, TPending> {
82
+ return new StandardSchemaWebSocketRpcSession(options);
83
+ }
@@ -0,0 +1,107 @@
1
+ import type { DependencyList, RefObject } from "react";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import {
4
+ type StandardSchemaWebSocketRpcSession,
5
+ type StandardSchemaWebSocketRpcSessionConstructorOptions,
6
+ createStandardSchemaWebSocketRpcSession,
7
+ } from "./standardSchemaRpc";
8
+
9
+ /** Options for {@link useStandardSchemaWebSocketRpc} (same as constructor options for {@link StandardSchemaWebSocketRpcSession}). */
10
+ export type UseStandardSchemaWebSocketRpcOptions<
11
+ TClientMsg,
12
+ TServerMsg,
13
+ TPending extends { reject: (error: Error) => void },
14
+ > = StandardSchemaWebSocketRpcSessionConstructorOptions<
15
+ TClientMsg,
16
+ TServerMsg,
17
+ TPending
18
+ >;
19
+
20
+ /**
21
+ * Connects a {@link StandardSchemaWebSocketRpcSession} in an effect: rejects all pending
22
+ * RPCs and closes the socket on cleanup or when `deps` change.
23
+ *
24
+ * Callback refs keep the latest `onMessage` / `onOpen` / `onClose` without
25
+ * listing them in `deps`, so inline handlers do not reconnect every render.
26
+ *
27
+ * Pass `deps` as the second argument; keep it aligned with values used to
28
+ * build `url` / `webSocket`.
29
+ */
30
+ export function useStandardSchemaWebSocketRpc<
31
+ TClientMsg,
32
+ TServerMsg,
33
+ TPending extends { reject: (error: Error) => void },
34
+ >(
35
+ options: UseStandardSchemaWebSocketRpcOptions<
36
+ TClientMsg,
37
+ TServerMsg,
38
+ TPending
39
+ >,
40
+ deps: DependencyList,
41
+ ): {
42
+ ready: boolean;
43
+ sessionRef: RefObject<StandardSchemaWebSocketRpcSession<
44
+ TClientMsg,
45
+ TServerMsg,
46
+ TPending
47
+ > | null>;
48
+ } {
49
+ const { onMessage, onOpen, onClose, ...clientOptions } = options;
50
+
51
+ const onMessageRef = useRef(onMessage);
52
+ onMessageRef.current = onMessage;
53
+ const onOpenRef = useRef(onOpen);
54
+ onOpenRef.current = onOpen;
55
+ const onCloseRef = useRef(onClose);
56
+ onCloseRef.current = onClose;
57
+
58
+ const [ready, setReady] = useState(false);
59
+ const sessionRef = useRef<StandardSchemaWebSocketRpcSession<
60
+ TClientMsg,
61
+ TServerMsg,
62
+ TPending
63
+ > | null>(null);
64
+
65
+ useEffect(() => {
66
+ let cancelled = false;
67
+ setReady(false);
68
+
69
+ const session = createStandardSchemaWebSocketRpcSession<
70
+ TClientMsg,
71
+ TServerMsg,
72
+ TPending
73
+ >({
74
+ ...clientOptions,
75
+ onMessage: (msg, s) => {
76
+ onMessageRef.current(msg, s);
77
+ },
78
+ onOpen: (event) => {
79
+ if (!cancelled) {
80
+ setReady(true);
81
+ }
82
+ onOpenRef.current?.(event);
83
+ },
84
+ onClose: (event) => {
85
+ if (!cancelled) {
86
+ setReady(false);
87
+ }
88
+ onCloseRef.current?.(event);
89
+ },
90
+ });
91
+
92
+ sessionRef.current = session;
93
+ if (session.client.socket.readyState === WebSocket.OPEN) {
94
+ setReady(true);
95
+ }
96
+
97
+ return () => {
98
+ cancelled = true;
99
+ sessionRef.current = null;
100
+ session.rejectAllPending(new Error("WebSocket closed"));
101
+ session.close();
102
+ };
103
+ // biome-ignore lint/correctness/useExhaustiveDependencies: deps is the explicit contract
104
+ }, deps);
105
+
106
+ return { ready, sessionRef };
107
+ }
package/src/zodMsgpack.ts DELETED
@@ -1,13 +0,0 @@
1
- import { pack, unpack } from "msgpackr";
2
- import type { ZodType } from "zod";
3
-
4
- export const zodMsgpack = <T>(schema: ZodType<T>) => ({
5
- encode(value: T): Uint8Array {
6
- const validated = schema.parse(value);
7
- const packed = pack(validated);
8
- return new Uint8Array(packed);
9
- },
10
- decode(bytes: Uint8Array): T {
11
- return schema.parse(unpack(bytes));
12
- },
13
- });