@firtoz/hono-fetcher 2.0.0 → 2.1.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
@@ -12,6 +12,7 @@ Type-safe Hono API client with full TypeScript inference for routes, params, and
12
12
  - 🎯 **Path Parameters** - Automatic extraction and validation of path parameters (`:id`, `:slug`, etc.)
13
13
  - 📝 **Request Bodies** - Type-safe JSON and form data support with automatic serialization
14
14
  - 🌐 **Cloudflare Workers** - First-class support for Durable Objects with `honoDoFetcher`
15
+ - 🔌 **WebSocket Support** - Type-safe WebSocket connections with automatic acceptance and configuration
15
16
  - 🚀 **Zero Runtime Overhead** - All type inference happens at compile time
16
17
  - 🔄 **Full HTTP Methods** - Support for GET, POST, PUT, DELETE, and PATCH
17
18
 
@@ -298,17 +299,132 @@ await api.get({
298
299
  });
299
300
  ```
300
301
 
302
+ ## WebSocket Support
303
+
304
+ `hono-fetcher` provides first-class support for WebSocket connections with full type safety.
305
+
306
+ ### Basic WebSocket Connection
307
+
308
+ ```typescript
309
+ import { honoFetcher } from '@firtoz/hono-fetcher';
310
+
311
+ const api = honoFetcher<typeof app>(fetcher);
312
+
313
+ // Connect to a WebSocket endpoint
314
+ const wsResponse = await api.websocket({
315
+ url: '/chat',
316
+ });
317
+
318
+ // Access the WebSocket
319
+ const ws = wsResponse.webSocket;
320
+ if (ws) {
321
+ ws.send(JSON.stringify({ type: 'hello' }));
322
+
323
+ ws.addEventListener('message', (event) => {
324
+ console.log('Received:', event.data);
325
+ });
326
+ }
327
+ ```
328
+
329
+ ### WebSocket with Auto-Accept
330
+
331
+ By default, WebSockets are **automatically accepted** for convenience:
332
+
333
+ ```typescript
334
+ // Default behavior - WebSocket is auto-accepted
335
+ const wsResp = await api.websocket({
336
+ url: '/websocket',
337
+ // config.autoAccept defaults to true
338
+ });
339
+
340
+ // WebSocket is ready to use immediately!
341
+ wsResp.webSocket?.send('Hello!');
342
+ ```
343
+
344
+ ### Manual WebSocket Acceptance
345
+
346
+ For advanced scenarios where you need control over when the WebSocket is accepted:
347
+
348
+ ```typescript
349
+ const wsResp = await api.websocket({
350
+ url: '/websocket',
351
+ config: { autoAccept: false }, // Disable auto-accept
352
+ });
353
+
354
+ const ws = wsResp.webSocket;
355
+ if (ws) {
356
+ // Set up your listeners first
357
+ ws.addEventListener('message', (event) => {
358
+ console.log('Message:', event.data);
359
+ });
360
+
361
+ // Then manually accept when ready
362
+ ws.accept();
363
+ }
364
+ ```
365
+
366
+ ### WebSocket with Path Parameters
367
+
368
+ ```typescript
369
+ const api = honoFetcher<typeof app>(fetcher);
370
+
371
+ // WebSocket endpoint with path parameters
372
+ const wsResp = await api.websocket({
373
+ url: '/rooms/:roomId/websocket',
374
+ params: { roomId: 'room-123' }, // Type-safe params!
375
+ });
376
+ ```
377
+
378
+ ### Integration with ZodWebSocketClient
379
+
380
+ For even better type safety, combine with `@firtoz/websocket-do`'s `ZodWebSocketClient`:
381
+
382
+ ```typescript
383
+ import { ZodWebSocketClient } from '@firtoz/websocket-do';
384
+ import { honoDoFetcherWithName } from '@firtoz/hono-fetcher';
385
+
386
+ // 1. Connect to DO WebSocket
387
+ const api = honoDoFetcherWithName(env.CHAT_ROOM, 'room-1');
388
+ const wsResp = await api.websocket({
389
+ url: '/websocket',
390
+ config: { autoAccept: false }, // Let ZodWebSocketClient handle acceptance
391
+ });
392
+
393
+ // 2. Wrap with type-safe client
394
+ const client = new ZodWebSocketClient({
395
+ webSocket: wsResp.webSocket,
396
+ clientSchema: ClientMessageSchema,
397
+ serverSchema: ServerMessageSchema,
398
+ onMessage: (message) => {
399
+ // Fully typed message!
400
+ console.log('Received:', message);
401
+ },
402
+ });
403
+
404
+ // 3. Now accept
405
+ wsResp.webSocket?.accept();
406
+
407
+ // 4. Send type-safe messages
408
+ client.send({ type: 'chat', text: 'Hello!' }); // Validated with Zod!
409
+ ```
410
+
411
+ See the [ZodWebSocketClient documentation](#) for more details on type-safe WebSocket communication.
412
+
301
413
  ## Durable Objects API
302
414
 
303
415
  ### `honoDoFetcher<T>(stub)`
304
416
 
305
- Creates a typed fetcher for a Durable Object stub.
417
+ Creates a typed fetcher for a Durable Object stub with support for both HTTP and WebSocket connections.
306
418
 
307
419
  ```typescript
308
420
  const stub = env.MY_DO.getByName('example');
309
421
  const api = honoDoFetcher(stub);
310
422
 
423
+ // HTTP requests
311
424
  await api.get({ url: '/status' });
425
+
426
+ // WebSocket connections
427
+ const wsResp = await api.websocket({ url: '/ws' });
312
428
  ```
313
429
 
314
430
  ### `honoDoFetcherWithName<T>(namespace, name)`
@@ -317,7 +433,12 @@ Convenience method to create a fetcher from a namespace and name.
317
433
 
318
434
  ```typescript
319
435
  const api = honoDoFetcherWithName(env.MY_DO, 'example');
436
+
437
+ // HTTP
320
438
  await api.get({ url: '/status' });
439
+
440
+ // WebSocket
441
+ await api.websocket({ url: '/chat' });
321
442
  ```
322
443
 
323
444
  ### `honoDoFetcherWithId<T>(namespace, id)`
@@ -354,6 +475,23 @@ const response: JsonResponse<{ id: string }> = await api.get({ url: '/user' });
354
475
  const data = await response.json(); // Type: { id: string }
355
476
  ```
356
477
 
478
+ ### `WebSocketConfig`
479
+
480
+ Configuration options for WebSocket connections.
481
+
482
+ ```typescript
483
+ import type { WebSocketConfig } from '@firtoz/hono-fetcher';
484
+
485
+ const config: WebSocketConfig = {
486
+ autoAccept: false, // Default: true
487
+ };
488
+
489
+ await api.websocket({ url: '/ws', config });
490
+ ```
491
+
492
+ **Options:**
493
+ - `autoAccept?: boolean` - Whether to automatically call `accept()` on the WebSocket. Defaults to `true` for convenience. Set to `false` if you need manual control over when the WebSocket is accepted (e.g., when using with `ZodWebSocketClient`).
494
+
357
495
  ### `ParsePathParams<T>`
358
496
 
359
497
  Utility type to extract path parameters from a route string.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@firtoz/hono-fetcher",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Type-safe Hono API client with full TypeScript inference for routes, params, and payloads",
5
5
  "main": "./src/index.ts",
6
6
  "module": "./src/index.ts",
@@ -97,9 +97,33 @@ type AvailableMethods<T extends Hono> = {
97
97
  [M in HttpMethod]: keyof HonoSchema<T>[M] extends never ? never : M;
98
98
  }[HttpMethod];
99
99
 
100
+ export interface WebSocketConfig {
101
+ /**
102
+ * Whether to automatically call accept() on the WebSocket before returning.
103
+ * Defaults to true for convenience.
104
+ *
105
+ * In Cloudflare Workers, you must call accept() before using a WebSocket.
106
+ * Setting this to false allows you to call accept() manually if needed.
107
+ *
108
+ * @default true
109
+ */
110
+ autoAccept?: boolean;
111
+ }
112
+
113
+ export type TypedWebSocketFetcher<T extends Hono> = <
114
+ SchemaPath extends string & keyof HonoSchema<T>["get"],
115
+ >(
116
+ request: {
117
+ url: SchemaPath;
118
+ config?: WebSocketConfig;
119
+ } & FetcherParams<SchemaPath>,
120
+ ) => Promise<Response>;
121
+
100
122
  export type BaseTypedHonoFetcher<T extends Hono> = {
101
123
  [M in AvailableMethods<T>]: TypedMethodFetcher<T, M>;
102
- };
124
+ } & (keyof HonoSchema<T>["get"] extends never
125
+ ? {}
126
+ : { websocket: TypedWebSocketFetcher<T> });
103
127
 
104
128
  const createMethodFetcher = <T extends Hono, M extends HttpMethod>(
105
129
  fetcher: (
@@ -158,6 +182,48 @@ const createMethodFetcher = <T extends Hono, M extends HttpMethod>(
158
182
  }) as TypedMethodFetcher<T, M>;
159
183
  };
160
184
 
185
+ const createWebSocketFetcher = <T extends Hono>(
186
+ fetcher: (
187
+ request: string,
188
+ init?: RequestInit,
189
+ ) => ReturnType<T["request"]> | Promise<ReturnType<T["request"]>>,
190
+ ): TypedWebSocketFetcher<T> => {
191
+ return (async (request) => {
192
+ let finalUrl: string = request.url;
193
+
194
+ const { init = {}, params, config } = request;
195
+ const autoAccept = config?.autoAccept ?? true; // Default to true
196
+
197
+ if (params && typeof params === "object") {
198
+ finalUrl = Object.entries(params).reduce((acc, [key, value]) => {
199
+ return acc.replace(`:${key}`, value as string);
200
+ }, finalUrl);
201
+ }
202
+
203
+ // biome-ignore lint/suspicious/noExplicitAny: Different runtimes have incompatible HeadersInit types
204
+ const newHeaders = new Headers(init.headers as any);
205
+ newHeaders.set("Upgrade", "websocket");
206
+
207
+ try {
208
+ const response = await fetcher(finalUrl, {
209
+ method: "GET",
210
+ headers: newHeaders,
211
+ ...init,
212
+ });
213
+
214
+ // Auto-accept the WebSocket if configured (default: true)
215
+ if (autoAccept && response.webSocket) {
216
+ response.webSocket.accept();
217
+ }
218
+
219
+ return response;
220
+ } catch (error) {
221
+ console.error("Error upgrading to WebSocket", error);
222
+ throw new Error(`Failed to upgrade WebSocket at ${finalUrl}: ${error}`);
223
+ }
224
+ }) as TypedWebSocketFetcher<T>;
225
+ };
226
+
161
227
  export type TypedHonoFetcher<T extends Hono> = BaseTypedHonoFetcher<T>;
162
228
 
163
229
  export const honoFetcher = <T extends Hono>(
@@ -183,5 +249,10 @@ export const honoFetcher = <T extends Hono>(
183
249
  {} as TypedHonoFetcher<T>,
184
250
  );
185
251
 
252
+ // Add websocket method
253
+ (
254
+ result as TypedHonoFetcher<T> & { websocket?: TypedWebSocketFetcher<T> }
255
+ ).websocket = createWebSocketFetcher(fetcher);
256
+
186
257
  return result;
187
258
  };
package/src/index.ts CHANGED
@@ -20,4 +20,6 @@ export {
20
20
  type JsonResponse,
21
21
  type ParsePathParams,
22
22
  type TypedHonoFetcher,
23
+ type TypedWebSocketFetcher,
24
+ type WebSocketConfig,
23
25
  } from "./honoFetcher";