@firtoz/hono-fetcher 1.1.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
 
@@ -29,12 +30,14 @@ This package requires the following peer dependencies:
29
30
  bun add hono
30
31
  ```
31
32
 
32
- For Durable Object support:
33
+ For Durable Object support, use `wrangler types` to generate accurate types:
33
34
 
34
35
  ```bash
35
- bun add @cloudflare/workers-types
36
+ wrangler types
36
37
  ```
37
38
 
39
+ This generates `worker-configuration.d.ts` with types for your specific environment bindings.
40
+
38
41
  ## Quick Start
39
42
 
40
43
  ### Basic Usage
@@ -129,11 +132,11 @@ export class ChatRoomDO extends DurableObject {
129
132
  // In your worker
130
133
  export default {
131
134
  async fetch(request: Request, env: Env): Promise<Response> {
132
- // Option 1: From a stub
133
- const stub = env.CHAT_ROOM.get(env.CHAT_ROOM.idFromName('room-1'));
135
+ // Option 1: From a stub (using new getByName API)
136
+ const stub = env.CHAT_ROOM.getByName('room-1');
134
137
  const api = honoDoFetcher(stub);
135
138
 
136
- // Option 2: Directly with name
139
+ // Option 2: Directly with name (recommended)
137
140
  const api2 = honoDoFetcherWithName(env.CHAT_ROOM, 'room-1');
138
141
 
139
142
  // Use it!
@@ -296,17 +299,132 @@ await api.get({
296
299
  });
297
300
  ```
298
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
+
299
413
  ## Durable Objects API
300
414
 
301
415
  ### `honoDoFetcher<T>(stub)`
302
416
 
303
- 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.
304
418
 
305
419
  ```typescript
306
- const stub = env.MY_DO.get(env.MY_DO.idFromName('example'));
420
+ const stub = env.MY_DO.getByName('example');
307
421
  const api = honoDoFetcher(stub);
308
422
 
423
+ // HTTP requests
309
424
  await api.get({ url: '/status' });
425
+
426
+ // WebSocket connections
427
+ const wsResp = await api.websocket({ url: '/ws' });
310
428
  ```
311
429
 
312
430
  ### `honoDoFetcherWithName<T>(namespace, name)`
@@ -315,7 +433,12 @@ Convenience method to create a fetcher from a namespace and name.
315
433
 
316
434
  ```typescript
317
435
  const api = honoDoFetcherWithName(env.MY_DO, 'example');
436
+
437
+ // HTTP
318
438
  await api.get({ url: '/status' });
439
+
440
+ // WebSocket
441
+ await api.websocket({ url: '/chat' });
319
442
  ```
320
443
 
321
444
  ### `honoDoFetcherWithId<T>(namespace, id)`
@@ -352,6 +475,23 @@ const response: JsonResponse<{ id: string }> = await api.get({ url: '/user' });
352
475
  const data = await response.json(); // Type: { id: string }
353
476
  ```
354
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
+
355
495
  ### `ParsePathParams<T>`
356
496
 
357
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": "1.1.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",
@@ -63,8 +63,8 @@
63
63
  },
64
64
  "devDependencies": {
65
65
  "@hono/node-server": "^1.19.5",
66
- "@hono/zod-validator": "^0.7.3",
67
- "bun-types": "^1.2.23",
66
+ "@hono/zod-validator": "^0.7.4",
67
+ "bun-types": "^1.3.0",
68
68
  "zod": "^4.1.12"
69
69
  }
70
70
  }
@@ -44,7 +44,7 @@ export const honoDoFetcherWithName = <
44
44
  namespace: DurableObjectNamespace<T>,
45
45
  name: string,
46
46
  ): TypedDoFetcher<DurableObjectStub<T>> => {
47
- return honoDoFetcher(namespace.get(namespace.idFromName(name)));
47
+ return honoDoFetcher(namespace.getByName(name));
48
48
  };
49
49
 
50
50
  export const honoDoFetcherWithId = <
@@ -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";