@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 +147 -7
- package/package.json +3 -3
- package/src/honoDoFetcher.ts +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
|
|
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
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": "
|
|
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.
|
|
67
|
-
"bun-types": "^1.
|
|
66
|
+
"@hono/zod-validator": "^0.7.4",
|
|
67
|
+
"bun-types": "^1.3.0",
|
|
68
68
|
"zod": "^4.1.12"
|
|
69
69
|
}
|
|
70
70
|
}
|
package/src/honoDoFetcher.ts
CHANGED
|
@@ -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.
|
|
47
|
+
return honoDoFetcher(namespace.getByName(name));
|
|
48
48
|
};
|
|
49
49
|
|
|
50
50
|
export const honoDoFetcherWithId = <
|
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
|
};
|