@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 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
- // Option 1: From a stub (using new getByName API)
138
- const stub = env.CHAT_ROOM.getByName('room-1');
139
- const api = honoDoFetcher(stub);
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
- const stub = env.MY_DO.getByName('example');
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
- const api = honoDoFetcherWithName(env.MY_DO, 'example');
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
- const api = honoDoFetcherWithId(env.MY_DO, 'abc123...');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@firtoz/hono-fetcher",
3
- "version": "2.4.0",
3
+ "version": "2.5.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",
@@ -29,13 +29,56 @@ export type TypedDoFetcher<T extends DurableObjectStub> = TypedHonoFetcher<
29
29
  Hono<any, DOStubSchema<T>>
30
30
  >;
31
31
 
32
- export const honoDoFetcher = <const T extends DurableObjectStub<DOWithHonoApp>>(
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
- ): TypedDoFetcher<T> => {
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
- return honoFetcher<Hono<any, DOStubSchema<T>>>((url, init) => {
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(namespace.get(namespace.idFromString(id)));
101
+ ): TypedDoFetcher<DurableObjectStub<T>> & Disposable => {
102
+ return honoDoFetcher(
103
+ namespace.get(namespace.idFromString(id)),
104
+ ) as TypedDoFetcher<DurableObjectStub<T>> & Disposable;
57
105
  };
@@ -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)