@firtoz/hono-fetcher 2.4.0 → 2.5.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 +23 -14
- package/package.json +1 -1
- package/src/honoDoFetcher.ts +55 -7
- package/src/honoFetcher.ts +12 -2
package/README.md
CHANGED
|
@@ -131,17 +131,12 @@ export class ChatRoomDO extends DurableObject {
|
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
// In your worker
|
|
134
|
+
// In your worker — use `using` so the RPC stub is disposed when the block exits
|
|
135
135
|
export default {
|
|
136
136
|
async fetch(request: Request, env: Env): Promise<Response> {
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
// Option 2: Directly with name (recommended)
|
|
142
|
-
const api2 = honoDoFetcherWithName(env.CHAT_ROOM, 'room-1');
|
|
143
|
-
|
|
144
|
-
// Use it!
|
|
137
|
+
// From namespace + name (recommended)
|
|
138
|
+
using api = honoDoFetcherWithName(env.CHAT_ROOM, 'room-1');
|
|
139
|
+
// Or from an existing stub: using api = honoDoFetcher(env.CHAT_ROOM.getByName('room-1'));
|
|
145
140
|
const response = await api.get({ url: '/messages' });
|
|
146
141
|
return response;
|
|
147
142
|
}
|
|
@@ -414,13 +409,16 @@ See the [ZodWebSocketClient documentation](#) for more details on type-safe WebS
|
|
|
414
409
|
|
|
415
410
|
## Durable Objects API
|
|
416
411
|
|
|
412
|
+
Each of `honoDoFetcher`, `honoDoFetcherWithName`, and `honoDoFetcherWithId` returns a fetcher that is also a **`Disposable`**: calling `api[Symbol.dispose]()` releases the underlying Durable Object stub. In production Workers, prefer the **`using`** declaration so disposal runs when the block exits (success or throw). See [Durable Object stubs and disposal](#durable-object-stubs-and-disposal) below.
|
|
413
|
+
|
|
417
414
|
### `honoDoFetcher<T>(stub)`
|
|
418
415
|
|
|
419
416
|
Creates a typed fetcher for a Durable Object stub with support for both HTTP and WebSocket connections.
|
|
420
417
|
|
|
418
|
+
**Returns:** `TypedDoFetcher<T> & Disposable`
|
|
419
|
+
|
|
421
420
|
```typescript
|
|
422
|
-
|
|
423
|
-
const api = honoDoFetcher(stub);
|
|
421
|
+
using api = honoDoFetcher(env.MY_DO.getByName('example'));
|
|
424
422
|
|
|
425
423
|
// HTTP requests
|
|
426
424
|
await api.get({ url: '/status' });
|
|
@@ -431,10 +429,12 @@ const wsResp = await api.websocket({ url: '/ws' });
|
|
|
431
429
|
|
|
432
430
|
### `honoDoFetcherWithName<T>(namespace, name)`
|
|
433
431
|
|
|
434
|
-
Convenience method to create a fetcher from a namespace and name.
|
|
432
|
+
Convenience method to create a fetcher from a namespace and name. Uses a single stub internally and wires disposal to that stub.
|
|
433
|
+
|
|
434
|
+
**Returns:** `TypedDoFetcher<DurableObjectStub<T>> & Disposable`
|
|
435
435
|
|
|
436
436
|
```typescript
|
|
437
|
-
|
|
437
|
+
using api = honoDoFetcherWithName(env.MY_DO, 'example');
|
|
438
438
|
|
|
439
439
|
// HTTP
|
|
440
440
|
await api.get({ url: '/status' });
|
|
@@ -447,11 +447,20 @@ await api.websocket({ url: '/chat' });
|
|
|
447
447
|
|
|
448
448
|
Convenience method to create a fetcher from a namespace and hex ID string.
|
|
449
449
|
|
|
450
|
+
**Returns:** `TypedDoFetcher<DurableObjectStub<T>> & Disposable`
|
|
451
|
+
|
|
450
452
|
```typescript
|
|
451
|
-
|
|
453
|
+
using api = honoDoFetcherWithId(env.MY_DO, 'abc123...');
|
|
452
454
|
await api.get({ url: '/status' });
|
|
453
455
|
```
|
|
454
456
|
|
|
457
|
+
### Durable Object stubs and disposal
|
|
458
|
+
|
|
459
|
+
- **Production Workers:** Durable Object stubs participate in RPC and should be released when you are done. The easiest pattern is **`using api = honoDoFetcherWithName(...)`** (or `honoDoFetcher` / `honoDoFetcherWithId`). You can also call **`api[Symbol.dispose]()`** manually (for example in a `finally` block) if you cannot use `using`.
|
|
460
|
+
- **Vite SSR, some Miniflare setups, or test mocks** may expose stubs that only implement **`fetch`** and not **`Symbol.dispose`**. The library checks for a callable `Symbol.dispose` before invoking it; if it is missing, disposal is a no-op (no `TypeError`).
|
|
461
|
+
- **Errors from `Symbol.dispose`:** If the runtime’s dispose implementation throws (for example during unwind after your code threw), the library catches the error and logs it with **`console.error`**. It does **not** rethrow, so your original error is not masked by a `SuppressedError`.
|
|
462
|
+
- **TypeScript `using`:** Add **`"ESNext.Disposable"`** to the `compilerOptions.lib` array in your **`tsconfig.json`** (alongside your existing libs) so `using` and `Disposable` type-check. TypeScript 5.2+ is required for `using`.
|
|
463
|
+
|
|
455
464
|
## Type Exports
|
|
456
465
|
|
|
457
466
|
### `TypedHonoFetcher<T>`
|
package/package.json
CHANGED
package/src/honoDoFetcher.ts
CHANGED
|
@@ -29,13 +29,56 @@ export type TypedDoFetcher<T extends DurableObjectStub> = TypedHonoFetcher<
|
|
|
29
29
|
Hono<any, DOStubSchema<T>>
|
|
30
30
|
>;
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
/** Shape honoDoFetcher uses at runtime; full stubs or minimal mocks (e.g. tests) with only `fetch`. */
|
|
33
|
+
export type HonoDoFetcherStubInput =
|
|
34
|
+
| DurableObjectStub<DOWithHonoApp>
|
|
35
|
+
| Pick<DurableObjectStub<DOWithHonoApp>, "fetch">;
|
|
36
|
+
|
|
37
|
+
function withStubDispose<
|
|
38
|
+
TStub extends Pick<DurableObjectStub<DOWithHonoApp>, "fetch">,
|
|
39
|
+
TS extends Schema,
|
|
40
|
+
>(
|
|
41
|
+
stub: TStub,
|
|
42
|
+
// biome-ignore lint/suspicious/noExplicitAny: Matches honoFetcher generic pattern for schema-driven apps
|
|
43
|
+
api: TypedHonoFetcher<Hono<any, TS>>,
|
|
44
|
+
// biome-ignore lint/suspicious/noExplicitAny: Matches honoFetcher generic pattern for schema-driven apps
|
|
45
|
+
): TypedHonoFetcher<Hono<any, TS>> & Disposable {
|
|
46
|
+
return Object.assign(api, {
|
|
47
|
+
[Symbol.dispose]() {
|
|
48
|
+
// Stubs may omit Symbol.dispose (e.g. Vite mocks); DurableObjectStub types may not list it.
|
|
49
|
+
const disposeFn = Reflect.get(stub, Symbol.dispose);
|
|
50
|
+
if (typeof disposeFn !== "function") {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
disposeFn.call(stub);
|
|
55
|
+
} catch (e) {
|
|
56
|
+
console.error(
|
|
57
|
+
"[@firtoz/hono-fetcher] Durable Object stub dispose failed",
|
|
58
|
+
e,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const honoDoFetcher = <const T extends HonoDoFetcherStubInput>(
|
|
33
66
|
durableObject: T,
|
|
34
|
-
):
|
|
67
|
+
): T extends DurableObjectStub<DOWithHonoApp>
|
|
68
|
+
? TypedDoFetcher<T> & Disposable
|
|
69
|
+
: TypedHonoFetcher<Hono> & Disposable => {
|
|
70
|
+
type OutSchema =
|
|
71
|
+
T extends DurableObjectStub<DOWithHonoApp> ? DOStubSchema<T> : Schema;
|
|
35
72
|
// biome-ignore lint/suspicious/noExplicitAny: Generic parameter needs flexibility
|
|
36
|
-
|
|
73
|
+
const api = honoFetcher<Hono<any, OutSchema>>((url, init) => {
|
|
37
74
|
return durableObject.fetch(`${DUMMY_URL}${url}`, init);
|
|
38
75
|
});
|
|
76
|
+
return withStubDispose(
|
|
77
|
+
durableObject,
|
|
78
|
+
api,
|
|
79
|
+
) as T extends DurableObjectStub<DOWithHonoApp>
|
|
80
|
+
? TypedDoFetcher<T> & Disposable
|
|
81
|
+
: TypedHonoFetcher<Hono> & Disposable;
|
|
39
82
|
};
|
|
40
83
|
|
|
41
84
|
export const honoDoFetcherWithName = <
|
|
@@ -43,8 +86,11 @@ export const honoDoFetcherWithName = <
|
|
|
43
86
|
>(
|
|
44
87
|
namespace: DurableObjectNamespace<T>,
|
|
45
88
|
name: string,
|
|
46
|
-
): TypedDoFetcher<DurableObjectStub<T>> => {
|
|
47
|
-
return honoDoFetcher(namespace.getByName(name))
|
|
89
|
+
): TypedDoFetcher<DurableObjectStub<T>> & Disposable => {
|
|
90
|
+
return honoDoFetcher(namespace.getByName(name)) as TypedDoFetcher<
|
|
91
|
+
DurableObjectStub<T>
|
|
92
|
+
> &
|
|
93
|
+
Disposable;
|
|
48
94
|
};
|
|
49
95
|
|
|
50
96
|
export const honoDoFetcherWithId = <
|
|
@@ -52,6 +98,8 @@ export const honoDoFetcherWithId = <
|
|
|
52
98
|
>(
|
|
53
99
|
namespace: DurableObjectNamespace<T>,
|
|
54
100
|
id: string,
|
|
55
|
-
): TypedDoFetcher<DurableObjectStub<T>> => {
|
|
56
|
-
return honoDoFetcher(
|
|
101
|
+
): TypedDoFetcher<DurableObjectStub<T>> & Disposable => {
|
|
102
|
+
return honoDoFetcher(
|
|
103
|
+
namespace.get(namespace.idFromString(id)),
|
|
104
|
+
) as TypedDoFetcher<DurableObjectStub<T>> & Disposable;
|
|
57
105
|
};
|
package/src/honoFetcher.ts
CHANGED
|
@@ -66,6 +66,16 @@ function appendQueryString(
|
|
|
66
66
|
return `${url}${separator}${serialized}`;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* `RequestInit` fields that honoFetcher sets must not be overwritten by spreading `...init` last.
|
|
71
|
+
*/
|
|
72
|
+
function restOfRequestInit(
|
|
73
|
+
init: RequestInit,
|
|
74
|
+
): Omit<RequestInit, "headers" | "body" | "method"> {
|
|
75
|
+
const { headers: _h, body: _b, method: _m, ...rest } = init;
|
|
76
|
+
return rest;
|
|
77
|
+
}
|
|
78
|
+
|
|
69
79
|
type FetcherParams<SchemaPath extends string> =
|
|
70
80
|
HasPathParams<SchemaPath> extends true
|
|
71
81
|
? {
|
|
@@ -209,10 +219,10 @@ const createMethodFetcher = <T extends Hono, M extends HttpMethod>(
|
|
|
209
219
|
|
|
210
220
|
try {
|
|
211
221
|
return await fetcher(finalUrl, {
|
|
222
|
+
...restOfRequestInit(init),
|
|
212
223
|
method: method.toUpperCase(),
|
|
213
224
|
headers: newHeaders,
|
|
214
225
|
...(body ? { body } : {}),
|
|
215
|
-
...init,
|
|
216
226
|
});
|
|
217
227
|
} catch (error) {
|
|
218
228
|
console.error(`Error ${method}ing`, error);
|
|
@@ -248,9 +258,9 @@ const createWebSocketFetcher = <T extends Hono>(
|
|
|
248
258
|
|
|
249
259
|
try {
|
|
250
260
|
const response = await fetcher(finalUrl, {
|
|
261
|
+
...restOfRequestInit(init),
|
|
251
262
|
method: "GET",
|
|
252
263
|
headers: newHeaders,
|
|
253
|
-
...init,
|
|
254
264
|
});
|
|
255
265
|
|
|
256
266
|
// Auto-accept the WebSocket if configured (default: true)
|