@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 +139 -1
- package/package.json +1 -1
- package/src/honoFetcher.ts +72 -1
- package/src/index.ts +2 -0
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
package/src/honoFetcher.ts
CHANGED
|
@@ -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
|
};
|