@firtoz/hono-fetcher 2.0.0 → 2.2.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 +6 -6
- package/src/honoFetcher.ts +77 -3
- 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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@firtoz/hono-fetcher",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.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",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"README.md"
|
|
24
24
|
],
|
|
25
25
|
"scripts": {
|
|
26
|
-
"typecheck": "tsc --noEmit",
|
|
26
|
+
"typecheck": "tsc --noEmit -p ./tsconfig.json",
|
|
27
27
|
"lint": "biome check --write src",
|
|
28
28
|
"lint:ci": "biome ci src",
|
|
29
29
|
"format": "biome format src --write",
|
|
@@ -52,8 +52,8 @@
|
|
|
52
52
|
"url": "https://github.com/firtoz/fullstack-toolkit/issues"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
|
-
"@cloudflare/workers-types": "
|
|
56
|
-
"hono": "
|
|
55
|
+
"@cloudflare/workers-types": "catalog:",
|
|
56
|
+
"hono": "catalog:"
|
|
57
57
|
},
|
|
58
58
|
"engines": {
|
|
59
59
|
"node": ">=18.0.0"
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"devDependencies": {
|
|
65
65
|
"@hono/node-server": "^1.19.5",
|
|
66
66
|
"@hono/zod-validator": "^0.7.4",
|
|
67
|
-
"bun-types": "
|
|
68
|
-
"zod": "
|
|
67
|
+
"bun-types": "catalog:",
|
|
68
|
+
"zod": "catalog:"
|
|
69
69
|
}
|
|
70
70
|
}
|
package/src/honoFetcher.ts
CHANGED
|
@@ -97,9 +97,34 @@ 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
|
+
? // biome-ignore lint/complexity/noBannedTypes: We really do want an empty object if the get method is not available
|
|
126
|
+
{}
|
|
127
|
+
: { websocket: TypedWebSocketFetcher<T> });
|
|
103
128
|
|
|
104
129
|
const createMethodFetcher = <T extends Hono, M extends HttpMethod>(
|
|
105
130
|
fetcher: (
|
|
@@ -137,8 +162,9 @@ const createMethodFetcher = <T extends Hono, M extends HttpMethod>(
|
|
|
137
162
|
body = JSON.stringify(requestAsOptionalFormBody.body) as BodyInit;
|
|
138
163
|
}
|
|
139
164
|
|
|
140
|
-
|
|
141
|
-
|
|
165
|
+
const newHeaders = new Headers(
|
|
166
|
+
init.headers as unknown as ConstructorParameters<typeof Headers>[0],
|
|
167
|
+
);
|
|
142
168
|
|
|
143
169
|
if (body && !requestAsOptionalFormBody.form) {
|
|
144
170
|
newHeaders.set("Content-Type", "application/json");
|
|
@@ -158,6 +184,49 @@ const createMethodFetcher = <T extends Hono, M extends HttpMethod>(
|
|
|
158
184
|
}) as TypedMethodFetcher<T, M>;
|
|
159
185
|
};
|
|
160
186
|
|
|
187
|
+
const createWebSocketFetcher = <T extends Hono>(
|
|
188
|
+
fetcher: (
|
|
189
|
+
request: string,
|
|
190
|
+
init?: RequestInit,
|
|
191
|
+
) => ReturnType<T["request"]> | Promise<ReturnType<T["request"]>>,
|
|
192
|
+
): TypedWebSocketFetcher<T> => {
|
|
193
|
+
return (async (request) => {
|
|
194
|
+
let finalUrl: string = request.url;
|
|
195
|
+
|
|
196
|
+
const { init = {}, params, config } = request;
|
|
197
|
+
const autoAccept = config?.autoAccept ?? true; // Default to true
|
|
198
|
+
|
|
199
|
+
if (params && typeof params === "object") {
|
|
200
|
+
finalUrl = Object.entries(params).reduce((acc, [key, value]) => {
|
|
201
|
+
return acc.replace(`:${key}`, value as string);
|
|
202
|
+
}, finalUrl);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const newHeaders = new Headers(
|
|
206
|
+
init.headers as unknown as ConstructorParameters<typeof Headers>[0],
|
|
207
|
+
);
|
|
208
|
+
newHeaders.set("Upgrade", "websocket");
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
const response = await fetcher(finalUrl, {
|
|
212
|
+
method: "GET",
|
|
213
|
+
headers: newHeaders,
|
|
214
|
+
...init,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Auto-accept the WebSocket if configured (default: true)
|
|
218
|
+
if (autoAccept && response.webSocket) {
|
|
219
|
+
response.webSocket.accept();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return response;
|
|
223
|
+
} catch (error) {
|
|
224
|
+
console.error("Error upgrading to WebSocket", error);
|
|
225
|
+
throw new Error(`Failed to upgrade WebSocket at ${finalUrl}: ${error}`);
|
|
226
|
+
}
|
|
227
|
+
}) as TypedWebSocketFetcher<T>;
|
|
228
|
+
};
|
|
229
|
+
|
|
161
230
|
export type TypedHonoFetcher<T extends Hono> = BaseTypedHonoFetcher<T>;
|
|
162
231
|
|
|
163
232
|
export const honoFetcher = <T extends Hono>(
|
|
@@ -183,5 +252,10 @@ export const honoFetcher = <T extends Hono>(
|
|
|
183
252
|
{} as TypedHonoFetcher<T>,
|
|
184
253
|
);
|
|
185
254
|
|
|
255
|
+
// Add websocket method
|
|
256
|
+
(
|
|
257
|
+
result as TypedHonoFetcher<T> & { websocket?: TypedWebSocketFetcher<T> }
|
|
258
|
+
).websocket = createWebSocketFetcher(fetcher);
|
|
259
|
+
|
|
186
260
|
return result;
|
|
187
261
|
};
|