@firtoz/hono-fetcher 2.5.0 → 2.6.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,14 +131,13 @@ export class ChatRoomDO extends DurableObject {
131
131
  }
132
132
  }
133
133
 
134
- // In your worker use `using` so the RPC stub is disposed when the block exits
134
+ // `using api` disposes the stub. Returning the DO `Response` directly is a common pass-through pattern;
135
+ // if you consume the body in this worker instead, also dispose the RPC result (e.g. `using res`).
136
+ // See https://developers.cloudflare.com/workers/runtime-apis/rpc/lifecycle/
135
137
  export default {
136
138
  async fetch(request: Request, env: Env): Promise<Response> {
137
- // From namespace + name (recommended)
138
139
  using api = honoDoFetcherWithName(env.CHAT_ROOM, 'room-1');
139
- // Or from an existing stub: using api = honoDoFetcher(env.CHAT_ROOM.getByName('room-1'));
140
- const response = await api.get({ url: '/messages' });
141
- return response;
140
+ return api.get({ url: '/messages' });
142
141
  }
143
142
  };
144
143
  ```
@@ -409,22 +408,26 @@ See the [ZodWebSocketClient documentation](#) for more details on type-safe WebS
409
408
 
410
409
  ## Durable Objects API
411
410
 
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.
411
+ All of these return a fetcher that is also a **`Disposable`** for the **stub**: **`api[Symbol.dispose]()`** releases the Durable Object stub only.
412
+
413
+ **RPC `Response` typing:** **`honoDoFetcherWithName`** / **`honoDoFetcherWithId`** (and **`honoDoFetcher`** when you pass a **full `DurableObjectStub`**) use **`TypedDoFetcher`**: HTTP results are **`RpcDisposableJsonResponse`** and **`websocket`** is **`Response & Disposable`**, so **`using res = await …`** type-checks when Workers RPC attaches disposers. If you pass only **`Pick<DurableObjectStub, "fetch">`** (minimal mock), the return type is **`TypedHonoFetcher<Hono>`** with **plain `JsonResponse` / `Response`**—**not** typed as **`Disposable`**, so we do not pretend mocks have RPC disposers. See [Durable Object stubs and disposal](#durable-object-stubs-and-disposal) and the [RPC lifecycle](https://developers.cloudflare.com/workers/runtime-apis/rpc/lifecycle/) doc.
413
414
 
414
415
  ### `honoDoFetcher<T>(stub)`
415
416
 
416
417
  Creates a typed fetcher for a Durable Object stub with support for both HTTP and WebSocket connections.
417
418
 
418
- **Returns:** `TypedDoFetcher<T> & Disposable`
419
+ **Returns:** **`TypedDoFetcher<T> & Disposable`** when **`T`** is a **full `DurableObjectStub<DOWithHonoApp>`**; **`TypedHonoFetcher<Hono> & Disposable`** when **`T`** is only **`Pick<DurableObjectStub<DOWithHonoApp>, "fetch">`** (e.g. unit tests).
419
420
 
420
421
  ```typescript
421
422
  using api = honoDoFetcher(env.MY_DO.getByName('example'));
422
423
 
423
- // HTTP requests
424
- await api.get({ url: '/status' });
424
+ // HTTP — dispose each RPC Response (stub disposal alone is not enough)
425
+ using res = await api.get({ url: '/status' });
426
+ const data = await res.json();
425
427
 
426
- // WebSocket connections
427
- const wsResp = await api.websocket({ url: '/ws' });
428
+ // WebSocket
429
+ using wsRes = await api.websocket({ url: '/ws' });
430
+ wsRes.webSocket?.accept();
428
431
  ```
429
432
 
430
433
  ### `honoDoFetcherWithName<T>(namespace, name)`
@@ -436,11 +439,11 @@ Convenience method to create a fetcher from a namespace and name. Uses a single
436
439
  ```typescript
437
440
  using api = honoDoFetcherWithName(env.MY_DO, 'example');
438
441
 
439
- // HTTP
440
- await api.get({ url: '/status' });
442
+ using res = await api.get({ url: '/status' });
443
+ await res.json();
441
444
 
442
- // WebSocket
443
- await api.websocket({ url: '/chat' });
445
+ using wsRes = await api.websocket({ url: '/chat' });
446
+ wsRes.webSocket?.accept();
444
447
  ```
445
448
 
446
449
  ### `honoDoFetcherWithId<T>(namespace, id)`
@@ -451,15 +454,20 @@ Convenience method to create a fetcher from a namespace and hex ID string.
451
454
 
452
455
  ```typescript
453
456
  using api = honoDoFetcherWithId(env.MY_DO, 'abc123...');
454
- await api.get({ url: '/status' });
457
+ using res = await api.get({ url: '/status' });
458
+ await res.json();
455
459
  ```
456
460
 
457
461
  ### Durable Object stubs and disposal
458
462
 
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`).
463
+ Cloudflare Workers RPC gives **separate disposers** for the **Durable Object stub** and for **non-primitive RPC results** such as the `Response` from `stub.fetch()`. Disposing only the stub can still produce *“An RPC stub was not disposed properly”* if the `Response` was not released. See the official **[Workers RPC lifecycle](https://developers.cloudflare.com/workers/runtime-apis/rpc/lifecycle/)** documentation.
464
+
465
+ - **Stub (`using api = honoDoFetcherWithName(...)`):** Releases the **stub** when the block ends. This does **not** release RPC **`Response`** objects from individual requests.
466
+ - **Response (HTTP / WebSocket upgrade):** On a **real stub** (`TypedDoFetcher`), TypeScript types those results as **`Disposable`** so **`using res`** / **`using wsRes`** is valid. At runtime, **`[Symbol.dispose]`** is present when Workers RPC attaches it (often in production); **minimal `fetch`-only mocks** are typed as **non-**`Disposable` **`Response`**s because disposers are usually absent. Prefer **`using res`** when types allow it; otherwise call **`res[Symbol.dispose]()`** or use a **`DisposableStack`** as described in Cloudflare’s **[Workers RPC lifecycle](https://developers.cloudflare.com/workers/runtime-apis/rpc/lifecycle/)** documentation. Reading the body does **not** implicitly dispose the RPC result in this library.
467
+ - **Returning `Response` to the client:** **Do not** use **`using res`** on a response you **`return`** from your Worker—disposal can run on scope exit before the runtime finishes serving it. Return the value directly; see **[Workers RPC lifecycle](https://developers.cloudflare.com/workers/runtime-apis/rpc/lifecycle/)** for pass-through patterns.
468
+ - **Vite SSR, some Miniflare setups, or test mocks** may expose stubs or **`Response`** objects without **`Symbol.dispose`**; there is nothing to call in that case.
461
469
  - **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`.
470
+ - **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`. That applies to **`TypedDoFetcher`** (full **`DurableObjectStub`** path): **`RpcDisposableJsonResponse`** / **`Response & Disposable`** on **`websocket`**. **`Pick<stub, "fetch">`** clients get ordinary **`TypedHonoFetcher`** return types without **`Disposable`** on responses. Cloudflare’s runtime rules for RPC disposal are documented under **[Workers RPC lifecycle](https://developers.cloudflare.com/workers/runtime-apis/rpc/lifecycle/)**.
463
471
 
464
472
  ## Type Exports
465
473
 
@@ -486,6 +494,10 @@ const response: JsonResponse<{ id: string }> = await api.get({ url: '/user' });
486
494
  const data = await response.json(); // Type: { id: string }
487
495
  ```
488
496
 
497
+ ### `RpcDisposableJsonResponse<T>` / `BaseDisposableTypedHonoFetcher<T>` / `HonoDoFetcherStubInput`
498
+
499
+ **`RpcDisposableJsonResponse<T>`** is **`JsonResponse<T> & Disposable`**. **`BaseDisposableTypedHonoFetcher<T>`** mirrors **`TypedHonoFetcher<T>`** but uses that for HTTP methods and **`Response & Disposable`** for **`websocket`**. **`TypedDoFetcher`** is **`BaseDisposableTypedHonoFetcher<Hono<…, DO schema>>`** — used only when **`honoDoFetcher`** is given a **full `DurableObjectStub`**, or when using **`honoDoFetcherWithName` / `honoDoFetcherWithId`**. **`HonoDoFetcherStubInput`** documents the **`DurableObjectStub | Pick<stub, "fetch">`** union for **`honoDoFetcher`**. Background: **[Workers RPC lifecycle](https://developers.cloudflare.com/workers/runtime-apis/rpc/lifecycle/)** (official disposal / `using` / `DisposableStack` guidance).
500
+
489
501
  ### `WebSocketConfig`
490
502
 
491
503
  Configuration options for WebSocket connections.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@firtoz/hono-fetcher",
3
- "version": "2.5.0",
3
+ "version": "2.6.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",
@@ -1,6 +1,10 @@
1
1
  import type { Hono, Schema } from "hono";
2
2
  import type { ExtractSchema } from "hono/types";
3
- import { honoFetcher, type TypedHonoFetcher } from "./honoFetcher";
3
+ import {
4
+ honoFetcher,
5
+ type BaseDisposableTypedHonoFetcher,
6
+ type TypedHonoFetcher,
7
+ } from "./honoFetcher";
4
8
 
5
9
  const DUMMY_URL = "http://dummy-url";
6
10
 
@@ -24,12 +28,29 @@ export type DOStubSchema<T extends DurableObjectStub> =
24
28
  : never
25
29
  : never;
26
30
 
27
- export type TypedDoFetcher<T extends DurableObjectStub> = TypedHonoFetcher<
28
- // biome-ignore lint/suspicious/noExplicitAny: Generic parameter needs flexibility
29
- Hono<any, DOStubSchema<T>>
30
- >;
31
+ /**
32
+ * Fetcher for a **real** `DurableObjectStub`: HTTP results are {@link RpcDisposableJsonResponse}
33
+ * and `websocket` returns `Response & Disposable`, matching Workers RPC when the runtime attaches
34
+ * disposers. Use with {@link honoDoFetcher} / {@link honoDoFetcherWithName} / {@link honoDoFetcherWithId}
35
+ * when `T` is a full stub—not with a minimal `Pick<stub, "fetch">` mock (that path uses plain
36
+ * {@link TypedHonoFetcher} responses instead).
37
+ *
38
+ * @see https://developers.cloudflare.com/workers/runtime-apis/rpc/lifecycle/
39
+ */
40
+ export type TypedDoFetcher<T extends DurableObjectStub> =
41
+ BaseDisposableTypedHonoFetcher<
42
+ // biome-ignore lint/suspicious/noExplicitAny: Generic parameter needs flexibility
43
+ Hono<any, DOStubSchema<T>>
44
+ >;
31
45
 
32
- /** Shape honoDoFetcher uses at runtime; full stubs or minimal mocks (e.g. tests) with only `fetch`. */
46
+ /**
47
+ * Argument to {@link honoDoFetcher}: a **full** {@link DurableObjectStub} (production) or a minimal
48
+ * **`{ fetch }`** mock. Only the full stub is typed as {@link TypedDoFetcher} with disposable RPC
49
+ * responses; **`Pick<stub, "fetch">`** is typed as {@link TypedHonoFetcher} for `Hono` with ordinary
50
+ * `JsonResponse` / `Response` (no `Disposable` on results—matches mocks without `Symbol.dispose`).
51
+ *
52
+ * @see https://developers.cloudflare.com/workers/runtime-apis/rpc/lifecycle/
53
+ */
33
54
  export type HonoDoFetcherStubInput =
34
55
  | DurableObjectStub<DOWithHonoApp>
35
56
  | Pick<DurableObjectStub<DOWithHonoApp>, "fetch">;
@@ -62,6 +83,22 @@ function withStubDispose<
62
83
  });
63
84
  }
64
85
 
86
+ /**
87
+ * Typed fetcher for a Durable Object stub.
88
+ *
89
+ * - **Full `DurableObjectStub`:** return type is {@link TypedDoFetcher} **`& Disposable`** — each
90
+ * HTTP/WebSocket result is typed as disposable (`RpcDisposableJsonResponse` / `Response & Disposable`)
91
+ * so **`using res = await …`** type-checks when `"ESNext.Disposable"` is in `lib`, matching Workers RPC
92
+ * when the runtime attaches `[Symbol.dispose]` (see `@see` below).
93
+ * - **`Pick<stub, "fetch">` only (e.g. tests):** return type is **`TypedHonoFetcher<Hono> & Disposable`**
94
+ * — same **`JsonResponse` / `Response`** shapes as {@link honoFetcher}; results are **not** typed as
95
+ * `Disposable` so typings are not faked for mocks that lack RPC disposers.
96
+ *
97
+ * Disposing only the fetcher (`using api = …`) releases the **stub**; RPC **`Response`** disposal
98
+ * (when applicable) is separate — prefer **`using res`**, **`res[Symbol.dispose]()`**, or **`DisposableStack`**.
99
+ *
100
+ * @see https://developers.cloudflare.com/workers/runtime-apis/rpc/lifecycle/
101
+ */
65
102
  export const honoDoFetcher = <const T extends HonoDoFetcherStubInput>(
66
103
  durableObject: T,
67
104
  ): T extends DurableObjectStub<DOWithHonoApp>
@@ -29,6 +29,17 @@ export type JsonResponse<T> = Omit<Response, "json"> & {
29
29
  json: () => Promise<T>;
30
30
  };
31
31
 
32
+ /**
33
+ * {@link JsonResponse} intersected with `Disposable` for Workers RPC: `Response`
34
+ * values from `DurableObjectStub#fetch()` may implement `[Symbol.dispose]` even
35
+ * though `Fetcher.fetch` is still typed as `Promise<Response>`. Use with
36
+ * {@link BaseDisposableTypedHonoFetcher} (and `TypedDoFetcher` from `./honoDoFetcher`) so
37
+ * `using resp = await api.get(...)` type-checks when `"ESNext.Disposable"` is in `lib`.
38
+ *
39
+ * @see https://developers.cloudflare.com/workers/runtime-apis/rpc/lifecycle/
40
+ */
41
+ export type RpcDisposableJsonResponse<T> = JsonResponse<T> & Disposable;
42
+
32
43
  type HasPathParams<T extends string> = T extends `${string}:${string}`
33
44
  ? true
34
45
  : false;
@@ -111,6 +122,16 @@ type SchemaOutput<
111
122
  ? JsonResponse<HonoSchema<T>[M][SchemaPath][DollarM]["output"]>
112
123
  : never;
113
124
 
125
+ type DoSchemaOutput<
126
+ T extends Hono,
127
+ M extends HttpMethod,
128
+ SchemaPath extends string & keyof HonoSchema<T>[M],
129
+ DollarM extends `$${M}` & keyof HonoSchema<T>[M][SchemaPath] = `$${M}` &
130
+ keyof HonoSchema<T>[M][SchemaPath],
131
+ > = "output" extends keyof HonoSchema<T>[M][SchemaPath][DollarM]
132
+ ? RpcDisposableJsonResponse<HonoSchema<T>[M][SchemaPath][DollarM]["output"]>
133
+ : never;
134
+
114
135
  type BodyParams<
115
136
  TApp extends Hono,
116
137
  TMethod extends HttpMethod,
@@ -171,6 +192,38 @@ export type BaseTypedHonoFetcher<T extends Hono> = {
171
192
  {}
172
193
  : { websocket: TypedWebSocketFetcher<T> });
173
194
 
195
+ type TypedDisposableMethodFetcher<T extends Hono, M extends HttpMethod> = <
196
+ SchemaPath extends string & keyof HonoSchema<T>[M],
197
+ >(
198
+ request: {
199
+ url: SchemaPath;
200
+ } & FetcherParams<SchemaPath> &
201
+ (M extends "get" | "delete" ? EmptyObject : BodyParams<T, M, SchemaPath>),
202
+ ) => Promise<DoSchemaOutput<T, M, SchemaPath>>;
203
+
204
+ export type TypedDisposableWebSocketFetcher<T extends Hono> = <
205
+ SchemaPath extends string & keyof HonoSchema<T>["get"],
206
+ >(
207
+ request: {
208
+ url: SchemaPath;
209
+ config?: WebSocketConfig;
210
+ } & FetcherParams<SchemaPath>,
211
+ ) => Promise<Response & Disposable>;
212
+
213
+ /**
214
+ * Same shape as {@link BaseTypedHonoFetcher} but HTTP methods return
215
+ * {@link RpcDisposableJsonResponse} and `websocket` returns `Response & Disposable`
216
+ * so `using` on RPC results type-checks for Durable Object clients.
217
+ *
218
+ * @see https://developers.cloudflare.com/workers/runtime-apis/rpc/lifecycle/
219
+ */
220
+ export type BaseDisposableTypedHonoFetcher<T extends Hono> = {
221
+ [M in AvailableMethods<T>]: TypedDisposableMethodFetcher<T, M>;
222
+ } & (keyof HonoSchema<T>["get"] extends never
223
+ ? // biome-ignore lint/complexity/noBannedTypes: We really do want an empty object if the get method is not available
224
+ {}
225
+ : { websocket: TypedDisposableWebSocketFetcher<T> });
226
+
174
227
  const createMethodFetcher = <T extends Hono, M extends HttpMethod>(
175
228
  fetcher: (
176
229
  request: string,
package/src/index.ts CHANGED
@@ -6,6 +6,7 @@ export {
6
6
  type DOSchemaMap,
7
7
  type DOStubSchema,
8
8
  type DOWithHonoApp,
9
+ type HonoDoFetcherStubInput,
9
10
  honoDoFetcher,
10
11
  honoDoFetcherWithId,
11
12
  honoDoFetcherWithName,
@@ -13,6 +14,7 @@ export {
13
14
  } from "./honoDoFetcher";
14
15
  // Core fetcher functionality
15
16
  export {
17
+ type BaseDisposableTypedHonoFetcher,
16
18
  type BaseTypedHonoFetcher,
17
19
  type HonoFetcherQueryParamValue,
18
20
  type HonoFetcherQueryParams,
@@ -21,6 +23,8 @@ export {
21
23
  honoFetcher,
22
24
  type JsonResponse,
23
25
  type ParsePathParams,
26
+ type RpcDisposableJsonResponse,
27
+ type TypedDisposableWebSocketFetcher,
24
28
  type TypedHonoFetcher,
25
29
  type TypedWebSocketFetcher,
26
30
  type WebSocketConfig,