@firtoz/hono-fetcher 2.5.0 → 2.7.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
@@ -4,7 +4,11 @@
4
4
  [![npm downloads](https://img.shields.io/npm/dm/%40firtoz%2Fhono-fetcher.svg)](https://www.npmjs.com/package/@firtoz/hono-fetcher)
5
5
  [![license](https://img.shields.io/npm/l/%40firtoz%2Fhono-fetcher.svg)](https://github.com/firtoz/fullstack-toolkit/blob/main/LICENSE)
6
6
 
7
- Type-safe Hono API client with full TypeScript inference for routes, params, and payloads.
7
+ [![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
8
+ [![Hono](https://img.shields.io/badge/Hono-E36002?logo=hono&logoColor=white)](https://hono.dev)
9
+ [![Cloudflare Workers](https://img.shields.io/badge/Cloudflare-Workers-F38020?logo=cloudflare&logoColor=white)](https://developers.cloudflare.com/workers/)
10
+
11
+ **Typed Hono fetcher for Workers and Durable Objects** — infer `AppType` routes, params, and JSON bodies end to end.
8
12
 
9
13
  > **⚠️ Early WIP Notice:** This package is in very early development and is **not production-ready**. It is TypeScript-only and may have breaking changes. While I (the maintainer) have limited time, I'm open to PRs for features, bug fixes, or additional support (like JS builds). Please feel free to try it out and contribute! See [CONTRIBUTING.md](../../CONTRIBUTING.md) for details.
10
14
 
@@ -131,14 +135,13 @@ export class ChatRoomDO extends DurableObject {
131
135
  }
132
136
  }
133
137
 
134
- // In your worker use `using` so the RPC stub is disposed when the block exits
138
+ // `using api` disposes the stub. Returning the DO `Response` directly is a common pass-through pattern;
139
+ // if you consume the body in this worker instead, also dispose the RPC result (e.g. `using res`).
140
+ // See https://developers.cloudflare.com/workers/runtime-apis/rpc/lifecycle/
135
141
  export default {
136
142
  async fetch(request: Request, env: Env): Promise<Response> {
137
- // From namespace + name (recommended)
138
143
  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;
144
+ return api.get({ url: '/messages' });
142
145
  }
143
146
  };
144
147
  ```
@@ -372,23 +375,23 @@ const wsResp = await api.websocket({
372
375
  });
373
376
  ```
374
377
 
375
- ### Integration with ZodWebSocketClient
378
+ ### Integration with StandardSchemaWebSocketClient
376
379
 
377
- For even better type safety, combine with `@firtoz/websocket-do`'s `ZodWebSocketClient`:
380
+ For even better type safety, combine with `@firtoz/websocket-do`'s `StandardSchemaWebSocketClient`:
378
381
 
379
382
  ```typescript
380
- import { ZodWebSocketClient } from '@firtoz/websocket-do';
383
+ import { StandardSchemaWebSocketClient } from '@firtoz/websocket-do';
381
384
  import { honoDoFetcherWithName } from '@firtoz/hono-fetcher';
382
385
 
383
386
  // 1. Connect to DO WebSocket
384
387
  const api = honoDoFetcherWithName(env.CHAT_ROOM, 'room-1');
385
388
  const wsResp = await api.websocket({
386
389
  url: '/websocket',
387
- config: { autoAccept: false }, // Let ZodWebSocketClient handle acceptance
390
+ config: { autoAccept: false }, // Let StandardSchemaWebSocketClient handle acceptance
388
391
  });
389
392
 
390
393
  // 2. Wrap with type-safe client
391
- const client = new ZodWebSocketClient({
394
+ const client = new StandardSchemaWebSocketClient({
392
395
  webSocket: wsResp.webSocket,
393
396
  clientSchema: ClientMessageSchema,
394
397
  serverSchema: ServerMessageSchema,
@@ -402,29 +405,33 @@ const client = new ZodWebSocketClient({
402
405
  wsResp.webSocket?.accept();
403
406
 
404
407
  // 4. Send type-safe messages
405
- client.send({ type: 'chat', text: 'Hello!' }); // Validated with Zod!
408
+ await client.send({ type: 'chat', text: 'Hello!' }); // Validated with Standard Schema
406
409
  ```
407
410
 
408
- See the [ZodWebSocketClient documentation](#) for more details on type-safe WebSocket communication.
411
+ See the `@firtoz/websocket-do` README for more details on type-safe WebSocket communication.
409
412
 
410
413
  ## Durable Objects API
411
414
 
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.
415
+ All of these return a fetcher that is also a **`Disposable`** for the **stub**: **`api[Symbol.dispose]()`** releases the Durable Object stub only.
416
+
417
+ **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
418
 
414
419
  ### `honoDoFetcher<T>(stub)`
415
420
 
416
421
  Creates a typed fetcher for a Durable Object stub with support for both HTTP and WebSocket connections.
417
422
 
418
- **Returns:** `TypedDoFetcher<T> & Disposable`
423
+ **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
424
 
420
425
  ```typescript
421
426
  using api = honoDoFetcher(env.MY_DO.getByName('example'));
422
427
 
423
- // HTTP requests
424
- await api.get({ url: '/status' });
428
+ // HTTP — dispose each RPC Response (stub disposal alone is not enough)
429
+ using res = await api.get({ url: '/status' });
430
+ const data = await res.json();
425
431
 
426
- // WebSocket connections
427
- const wsResp = await api.websocket({ url: '/ws' });
432
+ // WebSocket
433
+ using wsRes = await api.websocket({ url: '/ws' });
434
+ wsRes.webSocket?.accept();
428
435
  ```
429
436
 
430
437
  ### `honoDoFetcherWithName<T>(namespace, name)`
@@ -436,11 +443,11 @@ Convenience method to create a fetcher from a namespace and name. Uses a single
436
443
  ```typescript
437
444
  using api = honoDoFetcherWithName(env.MY_DO, 'example');
438
445
 
439
- // HTTP
440
- await api.get({ url: '/status' });
446
+ using res = await api.get({ url: '/status' });
447
+ await res.json();
441
448
 
442
- // WebSocket
443
- await api.websocket({ url: '/chat' });
449
+ using wsRes = await api.websocket({ url: '/chat' });
450
+ wsRes.webSocket?.accept();
444
451
  ```
445
452
 
446
453
  ### `honoDoFetcherWithId<T>(namespace, id)`
@@ -451,15 +458,20 @@ Convenience method to create a fetcher from a namespace and hex ID string.
451
458
 
452
459
  ```typescript
453
460
  using api = honoDoFetcherWithId(env.MY_DO, 'abc123...');
454
- await api.get({ url: '/status' });
461
+ using res = await api.get({ url: '/status' });
462
+ await res.json();
455
463
  ```
456
464
 
457
465
  ### Durable Object stubs and disposal
458
466
 
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`).
467
+ 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.
468
+
469
+ - **Stub (`using api = honoDoFetcherWithName(...)`):** Releases the **stub** when the block ends. This does **not** release RPC **`Response`** objects from individual requests.
470
+ - **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.
471
+ - **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.
472
+ - **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
473
  - **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`.
474
+ - **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
475
 
464
476
  ## Type Exports
465
477
 
@@ -486,6 +498,10 @@ const response: JsonResponse<{ id: string }> = await api.get({ url: '/user' });
486
498
  const data = await response.json(); // Type: { id: string }
487
499
  ```
488
500
 
501
+ ### `RpcDisposableJsonResponse<T>` / `BaseDisposableTypedHonoFetcher<T>` / `HonoDoFetcherStubInput`
502
+
503
+ **`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).
504
+
489
505
  ### `WebSocketConfig`
490
506
 
491
507
  Configuration options for WebSocket connections.
@@ -501,7 +517,7 @@ await api.websocket({ url: '/ws', config });
501
517
  ```
502
518
 
503
519
  **Options:**
504
- - `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`).
520
+ - `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 `StandardSchemaWebSocketClient`).
505
521
 
506
522
  ### `ParsePathParams<T>`
507
523
 
@@ -0,0 +1,43 @@
1
+ import { honoFetcher } from './chunk-ULIZJ5OD.js';
2
+
3
+ // src/honoDoFetcher.ts
4
+ var DUMMY_URL = "http://dummy-url";
5
+ function withStubDispose(stub, api) {
6
+ return Object.assign(api, {
7
+ [Symbol.dispose]() {
8
+ const disposeFn = Reflect.get(stub, Symbol.dispose);
9
+ if (typeof disposeFn !== "function") {
10
+ return;
11
+ }
12
+ try {
13
+ disposeFn.call(stub);
14
+ } catch (e) {
15
+ console.error(
16
+ "[@firtoz/hono-fetcher] Durable Object stub dispose failed",
17
+ e
18
+ );
19
+ }
20
+ }
21
+ });
22
+ }
23
+ var honoDoFetcher = (durableObject) => {
24
+ const api = honoFetcher((url, init) => {
25
+ return durableObject.fetch(`${DUMMY_URL}${url}`, init);
26
+ });
27
+ return withStubDispose(
28
+ durableObject,
29
+ api
30
+ );
31
+ };
32
+ var honoDoFetcherWithName = (namespace, name) => {
33
+ return honoDoFetcher(namespace.getByName(name));
34
+ };
35
+ var honoDoFetcherWithId = (namespace, id) => {
36
+ return honoDoFetcher(
37
+ namespace.get(namespace.idFromString(id))
38
+ );
39
+ };
40
+
41
+ export { honoDoFetcher, honoDoFetcherWithId, honoDoFetcherWithName };
42
+ //# sourceMappingURL=chunk-EXAM5VFZ.js.map
43
+ //# sourceMappingURL=chunk-EXAM5VFZ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/honoDoFetcher.ts"],"names":[],"mappings":";;;AAQA,IAAM,SAAA,GAAY,kBAAA;AAiDlB,SAAS,eAAA,CAIR,MAEA,GAAA,EAE+C;AAC/C,EAAA,OAAO,MAAA,CAAO,OAAO,GAAA,EAAK;AAAA,IACzB,CAAC,MAAA,CAAO,OAAO,CAAA,GAAI;AAElB,MAAA,MAAM,SAAA,GAAY,OAAA,CAAQ,GAAA,CAAI,IAAA,EAAM,OAAO,OAAO,CAAA;AAClD,MAAA,IAAI,OAAO,cAAc,UAAA,EAAY;AACpC,QAAA;AAAA,MACD;AACA,MAAA,IAAI;AACH,QAAA,SAAA,CAAU,KAAK,IAAI,CAAA;AAAA,MACpB,SAAS,CAAA,EAAG;AACX,QAAA,OAAA,CAAQ,KAAA;AAAA,UACP,2DAAA;AAAA,UACA;AAAA,SACD;AAAA,MACD;AAAA,IACD;AAAA,GACA,CAAA;AACF;AAkBO,IAAM,aAAA,GAAgB,CAC5B,aAAA,KAGyC;AAIzC,EAAA,MAAM,GAAA,GAAM,WAAA,CAAkC,CAAC,GAAA,EAAK,IAAA,KAAS;AAC5D,IAAA,OAAO,cAAc,KAAA,CAAM,CAAA,EAAG,SAAS,CAAA,EAAG,GAAG,IAAI,IAAI,CAAA;AAAA,EACtD,CAAC,CAAA;AACD,EAAA,OAAO,eAAA;AAAA,IACN,aAAA;AAAA,IACA;AAAA,GACD;AAGD;AAEO,IAAM,qBAAA,GAAwB,CAGpC,SAAA,EACA,IAAA,KACuD;AACvD,EAAA,OAAO,aAAA,CAAc,SAAA,CAAU,SAAA,CAAU,IAAI,CAAC,CAAA;AAI/C;AAEO,IAAM,mBAAA,GAAsB,CAGlC,SAAA,EACA,EAAA,KACuD;AACvD,EAAA,OAAO,aAAA;AAAA,IACN,SAAA,CAAU,GAAA,CAAI,SAAA,CAAU,YAAA,CAAa,EAAE,CAAC;AAAA,GACzC;AACD","file":"chunk-EXAM5VFZ.js","sourcesContent":["import type { Hono, Schema } from \"hono\";\nimport type { ExtractSchema } from \"hono/types\";\nimport {\n\thonoFetcher,\n\ttype BaseDisposableTypedHonoFetcher,\n\ttype TypedHonoFetcher,\n} from \"./honoFetcher\";\n\nconst DUMMY_URL = \"http://dummy-url\";\n\nexport type DOWithHonoApp<S extends Schema = Schema> =\n\tRpc.DurableObjectBranded & {\n\t\t// biome-ignore lint/suspicious/noExplicitAny: We need to be able to pass in any schema\n\t\tapp: Hono<any, S>;\n\t};\n\nexport type DOSchemaMap<T extends DOWithHonoApp> = T extends DOWithHonoApp\n\t? ExtractSchema<T[\"app\"]>\n\t: never;\n\nexport type DOSchemaKeys<T extends DOWithHonoApp> = string &\n\tkeyof DOSchemaMap<T>;\n\nexport type DOStubSchema<T extends DurableObjectStub> =\n\tT extends DurableObjectStub<infer S>\n\t\t? S extends DOWithHonoApp\n\t\t\t? ExtractSchema<S[\"app\"]>\n\t\t\t: never\n\t\t: never;\n\n/**\n * Fetcher for a **real** `DurableObjectStub`: HTTP results are {@link RpcDisposableJsonResponse}\n * and `websocket` returns `Response & Disposable`, matching Workers RPC when the runtime attaches\n * disposers. Use with {@link honoDoFetcher} / {@link honoDoFetcherWithName} / {@link honoDoFetcherWithId}\n * when `T` is a full stub—not with a minimal `Pick<stub, \"fetch\">` mock (that path uses plain\n * {@link TypedHonoFetcher} responses instead).\n *\n * @see https://developers.cloudflare.com/workers/runtime-apis/rpc/lifecycle/\n */\nexport type TypedDoFetcher<T extends DurableObjectStub> =\n\tBaseDisposableTypedHonoFetcher<\n\t\t// biome-ignore lint/suspicious/noExplicitAny: Generic parameter needs flexibility\n\t\tHono<any, DOStubSchema<T>>\n\t>;\n\n/**\n * Argument to {@link honoDoFetcher}: a **full** {@link DurableObjectStub} (production) or a minimal\n * **`{ fetch }`** mock. Only the full stub is typed as {@link TypedDoFetcher} with disposable RPC\n * responses; **`Pick<stub, \"fetch\">`** is typed as {@link TypedHonoFetcher} for `Hono` with ordinary\n * `JsonResponse` / `Response` (no `Disposable` on results—matches mocks without `Symbol.dispose`).\n *\n * @see https://developers.cloudflare.com/workers/runtime-apis/rpc/lifecycle/\n */\nexport type HonoDoFetcherStubInput =\n\t| DurableObjectStub<DOWithHonoApp>\n\t| Pick<DurableObjectStub<DOWithHonoApp>, \"fetch\">;\n\nfunction withStubDispose<\n\tTStub extends Pick<DurableObjectStub<DOWithHonoApp>, \"fetch\">,\n\tTS extends Schema,\n>(\n\tstub: TStub,\n\t// biome-ignore lint/suspicious/noExplicitAny: Matches honoFetcher generic pattern for schema-driven apps\n\tapi: TypedHonoFetcher<Hono<any, TS>>,\n\t// biome-ignore lint/suspicious/noExplicitAny: Matches honoFetcher generic pattern for schema-driven apps\n): TypedHonoFetcher<Hono<any, TS>> & Disposable {\n\treturn Object.assign(api, {\n\t\t[Symbol.dispose]() {\n\t\t\t// Stubs may omit Symbol.dispose (e.g. Vite mocks); DurableObjectStub types may not list it.\n\t\t\tconst disposeFn = Reflect.get(stub, Symbol.dispose);\n\t\t\tif (typeof disposeFn !== \"function\") {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tdisposeFn.call(stub);\n\t\t\t} catch (e) {\n\t\t\t\tconsole.error(\n\t\t\t\t\t\"[@firtoz/hono-fetcher] Durable Object stub dispose failed\",\n\t\t\t\t\te,\n\t\t\t\t);\n\t\t\t}\n\t\t},\n\t});\n}\n\n/**\n * Typed fetcher for a Durable Object stub.\n *\n * - **Full `DurableObjectStub`:** return type is {@link TypedDoFetcher} **`& Disposable`** — each\n * HTTP/WebSocket result is typed as disposable (`RpcDisposableJsonResponse` / `Response & Disposable`)\n * so **`using res = await …`** type-checks when `\"ESNext.Disposable\"` is in `lib`, matching Workers RPC\n * when the runtime attaches `[Symbol.dispose]` (see `@see` below).\n * - **`Pick<stub, \"fetch\">` only (e.g. tests):** return type is **`TypedHonoFetcher<Hono> & Disposable`**\n * — same **`JsonResponse` / `Response`** shapes as {@link honoFetcher}; results are **not** typed as\n * `Disposable` so typings are not faked for mocks that lack RPC disposers.\n *\n * Disposing only the fetcher (`using api = …`) releases the **stub**; RPC **`Response`** disposal\n * (when applicable) is separate — prefer **`using res`**, **`res[Symbol.dispose]()`**, or **`DisposableStack`**.\n *\n * @see https://developers.cloudflare.com/workers/runtime-apis/rpc/lifecycle/\n */\nexport const honoDoFetcher = <const T extends HonoDoFetcherStubInput>(\n\tdurableObject: T,\n): T extends DurableObjectStub<DOWithHonoApp>\n\t? TypedDoFetcher<T> & Disposable\n\t: TypedHonoFetcher<Hono> & Disposable => {\n\ttype OutSchema =\n\t\tT extends DurableObjectStub<DOWithHonoApp> ? DOStubSchema<T> : Schema;\n\t// biome-ignore lint/suspicious/noExplicitAny: Generic parameter needs flexibility\n\tconst api = honoFetcher<Hono<any, OutSchema>>((url, init) => {\n\t\treturn durableObject.fetch(`${DUMMY_URL}${url}`, init);\n\t});\n\treturn withStubDispose(\n\t\tdurableObject,\n\t\tapi,\n\t) as T extends DurableObjectStub<DOWithHonoApp>\n\t\t? TypedDoFetcher<T> & Disposable\n\t\t: TypedHonoFetcher<Hono> & Disposable;\n};\n\nexport const honoDoFetcherWithName = <\n\tconst T extends Rpc.DurableObjectBranded & DOWithHonoApp,\n>(\n\tnamespace: DurableObjectNamespace<T>,\n\tname: string,\n): TypedDoFetcher<DurableObjectStub<T>> & Disposable => {\n\treturn honoDoFetcher(namespace.getByName(name)) as TypedDoFetcher<\n\t\tDurableObjectStub<T>\n\t> &\n\t\tDisposable;\n};\n\nexport const honoDoFetcherWithId = <\n\tconst T extends Rpc.DurableObjectBranded & DOWithHonoApp,\n>(\n\tnamespace: DurableObjectNamespace<T>,\n\tid: string,\n): TypedDoFetcher<DurableObjectStub<T>> & Disposable => {\n\treturn honoDoFetcher(\n\t\tnamespace.get(namespace.idFromString(id)),\n\t) as TypedDoFetcher<DurableObjectStub<T>> & Disposable;\n};\n"]}
@@ -0,0 +1,112 @@
1
+ // src/honoFetcher.ts
2
+ function appendQueryString(url, query) {
3
+ if (!query) {
4
+ return url;
5
+ }
6
+ const searchParams = new URLSearchParams();
7
+ for (const [key, value] of Object.entries(query)) {
8
+ if (value === void 0 || value === null) {
9
+ continue;
10
+ }
11
+ searchParams.append(key, String(value));
12
+ }
13
+ const serialized = searchParams.toString();
14
+ if (!serialized) {
15
+ return url;
16
+ }
17
+ const separator = url.includes("?") ? "&" : "?";
18
+ return `${url}${separator}${serialized}`;
19
+ }
20
+ function restOfRequestInit(init) {
21
+ const { headers: _h, body: _b, method: _m, ...rest } = init;
22
+ return rest;
23
+ }
24
+ var createMethodFetcher = (fetcher, method) => {
25
+ return (async (request) => {
26
+ let finalUrl = request.url;
27
+ const { init = {}, params, query } = request;
28
+ if (params && typeof params === "object") {
29
+ finalUrl = Object.entries(params).reduce((acc, [key, value]) => {
30
+ return acc.replace(`:${key}`, value);
31
+ }, finalUrl);
32
+ }
33
+ finalUrl = appendQueryString(finalUrl, query);
34
+ const requestAsOptionalFormBody = request;
35
+ let body;
36
+ if (requestAsOptionalFormBody.form) {
37
+ const formData = new FormData();
38
+ for (const [key, value] of Object.entries(
39
+ requestAsOptionalFormBody.form
40
+ )) {
41
+ formData.append(key, value);
42
+ }
43
+ body = formData;
44
+ } else if (requestAsOptionalFormBody.body) {
45
+ body = JSON.stringify(requestAsOptionalFormBody.body);
46
+ }
47
+ const newHeaders = new Headers(
48
+ init.headers
49
+ );
50
+ if (body && !requestAsOptionalFormBody.form) {
51
+ newHeaders.set("Content-Type", "application/json");
52
+ }
53
+ try {
54
+ return await fetcher(finalUrl, {
55
+ ...restOfRequestInit(init),
56
+ method: method.toUpperCase(),
57
+ headers: newHeaders,
58
+ ...body ? { body } : {}
59
+ });
60
+ } catch (error) {
61
+ console.error(`Error ${method}ing`, error);
62
+ throw new Error(`Failed to ${method} ${finalUrl}: ${error}`);
63
+ }
64
+ });
65
+ };
66
+ var createWebSocketFetcher = (fetcher) => {
67
+ return (async (request) => {
68
+ let finalUrl = request.url;
69
+ const { init = {}, params, query, config } = request;
70
+ const autoAccept = config?.autoAccept ?? true;
71
+ if (params && typeof params === "object") {
72
+ finalUrl = Object.entries(params).reduce((acc, [key, value]) => {
73
+ return acc.replace(`:${key}`, value);
74
+ }, finalUrl);
75
+ }
76
+ finalUrl = appendQueryString(finalUrl, query);
77
+ const newHeaders = new Headers(
78
+ init.headers
79
+ );
80
+ newHeaders.set("Upgrade", "websocket");
81
+ try {
82
+ const response = await fetcher(finalUrl, {
83
+ ...restOfRequestInit(init),
84
+ method: "GET",
85
+ headers: newHeaders
86
+ });
87
+ if (autoAccept && response.webSocket) {
88
+ response.webSocket.accept();
89
+ }
90
+ return response;
91
+ } catch (error) {
92
+ console.error("Error upgrading to WebSocket", error);
93
+ throw new Error(`Failed to upgrade WebSocket at ${finalUrl}: ${error}`);
94
+ }
95
+ });
96
+ };
97
+ var honoFetcher = (fetcher) => {
98
+ const methods = ["get", "post", "put", "delete", "patch"];
99
+ const result = methods.reduce(
100
+ (acc, method) => {
101
+ acc[method] = createMethodFetcher(fetcher, method);
102
+ return acc;
103
+ },
104
+ {}
105
+ );
106
+ result.websocket = createWebSocketFetcher(fetcher);
107
+ return result;
108
+ };
109
+
110
+ export { honoFetcher };
111
+ //# sourceMappingURL=chunk-ULIZJ5OD.js.map
112
+ //# sourceMappingURL=chunk-ULIZJ5OD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/honoFetcher.ts"],"names":[],"mappings":"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","file":"chunk-ULIZJ5OD.js","sourcesContent":["import type { Hono } from \"hono\";\nimport type { ExtractSchema } from \"hono/types\";\n\nexport type ParsePathParams<T extends string> =\n\tT extends `${infer _Start}/:${infer Param}/${infer Rest}`\n\t\t? { [K in Param | keyof ParsePathParams<`/${Rest}`>]: string }\n\t\t: T extends `${infer _Start}/:${infer Param}`\n\t\t\t? { [K in Param]: string }\n\t\t\t: never;\n\nexport type HttpMethod = \"get\" | \"post\" | \"put\" | \"delete\" | \"patch\";\n\nexport type HonoSchemaKeys<T extends Hono> = string & keyof ExtractSchema<T>;\n\ntype FilterKeysByMethod<\n\tTApp extends ExtractSchema<unknown>,\n\tTMethod extends HttpMethod,\n> = {\n\t[K in keyof TApp as TApp[K] extends { [key in `$${TMethod}`]: unknown }\n\t\t? K\n\t\t: never]: TApp[K];\n};\n\ntype HonoSchema<TApp extends Hono> = {\n\t[M in HttpMethod]: FilterKeysByMethod<ExtractSchema<TApp>, M>;\n};\n\nexport type JsonResponse<T> = Omit<Response, \"json\"> & {\n\tjson: () => Promise<T>;\n};\n\n/**\n * {@link JsonResponse} intersected with `Disposable` for Workers RPC: `Response`\n * values from `DurableObjectStub#fetch()` may implement `[Symbol.dispose]` even\n * though `Fetcher.fetch` is still typed as `Promise<Response>`. Use with\n * {@link BaseDisposableTypedHonoFetcher} (and `TypedDoFetcher` from `./honoDoFetcher`) so\n * `using resp = await api.get(...)` type-checks when `\"ESNext.Disposable\"` is in `lib`.\n *\n * @see https://developers.cloudflare.com/workers/runtime-apis/rpc/lifecycle/\n */\nexport type RpcDisposableJsonResponse<T> = JsonResponse<T> & Disposable;\n\ntype HasPathParams<T extends string> = T extends `${string}:${string}`\n\t? true\n\t: false;\n\n/**\n * Values allowed in the optional `query` object on fetcher requests.\n * `null` and `undefined` entries are omitted from the serialized query string.\n */\nexport type HonoFetcherQueryParamValue = string | number | boolean;\n\nexport type HonoFetcherQueryParams = Record<\n\tstring,\n\tHonoFetcherQueryParamValue | null | undefined\n>;\n\nfunction appendQueryString(\n\turl: string,\n\tquery?: HonoFetcherQueryParams,\n): string {\n\tif (!query) {\n\t\treturn url;\n\t}\n\tconst searchParams = new URLSearchParams();\n\tfor (const [key, value] of Object.entries(query)) {\n\t\tif (value === undefined || value === null) {\n\t\t\tcontinue;\n\t\t}\n\t\tsearchParams.append(key, String(value));\n\t}\n\tconst serialized = searchParams.toString();\n\tif (!serialized) {\n\t\treturn url;\n\t}\n\tconst separator = url.includes(\"?\") ? \"&\" : \"?\";\n\treturn `${url}${separator}${serialized}`;\n}\n\n/**\n * `RequestInit` fields that honoFetcher sets must not be overwritten by spreading `...init` last.\n */\nfunction restOfRequestInit(\n\tinit: RequestInit,\n): Omit<RequestInit, \"headers\" | \"body\" | \"method\"> {\n\tconst { headers: _h, body: _b, method: _m, ...rest } = init;\n\treturn rest;\n}\n\ntype FetcherParams<SchemaPath extends string> =\n\tHasPathParams<SchemaPath> extends true\n\t\t? {\n\t\t\t\tparams: ParsePathParams<SchemaPath>;\n\t\t\t\tquery?: HonoFetcherQueryParams;\n\t\t\t\tinit?: RequestInit;\n\t\t\t}\n\t\t: {\n\t\t\t\tparams?: never;\n\t\t\t\tquery?: HonoFetcherQueryParams;\n\t\t\t\tinit?: RequestInit;\n\t\t\t};\n\n// biome-ignore lint/complexity/noBannedTypes: We need an empty object to remove the body and form keys from the request object\ntype EmptyObject = {};\n\ntype TypedMethodFetcher<T extends Hono, M extends HttpMethod> = <\n\tSchemaPath extends string & keyof HonoSchema<T>[M],\n>(\n\trequest: {\n\t\turl: SchemaPath;\n\t} & FetcherParams<SchemaPath> &\n\t\t(M extends \"get\" | \"delete\" ? EmptyObject : BodyParams<T, M, SchemaPath>),\n) => Promise<SchemaOutput<T, M, SchemaPath>>;\n\ntype SchemaOutput<\n\tT extends Hono,\n\tM extends HttpMethod,\n\tSchemaPath extends string & keyof HonoSchema<T>[M],\n\tDollarM extends `$${M}` & keyof HonoSchema<T>[M][SchemaPath] = `$${M}` &\n\t\tkeyof HonoSchema<T>[M][SchemaPath],\n> = \"output\" extends keyof HonoSchema<T>[M][SchemaPath][DollarM]\n\t? JsonResponse<HonoSchema<T>[M][SchemaPath][DollarM][\"output\"]>\n\t: never;\n\ntype DoSchemaOutput<\n\tT extends Hono,\n\tM extends HttpMethod,\n\tSchemaPath extends string & keyof HonoSchema<T>[M],\n\tDollarM extends `$${M}` & keyof HonoSchema<T>[M][SchemaPath] = `$${M}` &\n\t\tkeyof HonoSchema<T>[M][SchemaPath],\n> = \"output\" extends keyof HonoSchema<T>[M][SchemaPath][DollarM]\n\t? RpcDisposableJsonResponse<HonoSchema<T>[M][SchemaPath][DollarM][\"output\"]>\n\t: never;\n\ntype BodyParams<\n\tTApp extends Hono,\n\tTMethod extends HttpMethod,\n\tSchemaPath extends string & keyof HonoSchema<TApp>[TMethod],\n\tDollarMethod extends `$${TMethod}` &\n\t\tkeyof HonoSchema<TApp>[TMethod][SchemaPath] = `$${TMethod}` &\n\t\tkeyof HonoSchema<TApp>[TMethod][SchemaPath],\n> = \"input\" extends keyof HonoSchema<TApp>[TMethod][SchemaPath][DollarMethod]\n\t? \"json\" extends keyof HonoSchema<TApp>[TMethod][SchemaPath][DollarMethod][\"input\"]\n\t\t? \"form\" extends keyof HonoSchema<TApp>[TMethod][SchemaPath][DollarMethod][\"input\"]\n\t\t\t?\n\t\t\t\t\t| {\n\t\t\t\t\t\t\tbody: HonoSchema<TApp>[TMethod][SchemaPath][DollarMethod][\"input\"][\"json\"];\n\t\t\t\t\t }\n\t\t\t\t\t| {\n\t\t\t\t\t\t\tform: HonoSchema<TApp>[TMethod][SchemaPath][DollarMethod][\"input\"][\"form\"];\n\t\t\t\t\t }\n\t\t\t: {\n\t\t\t\t\tbody: HonoSchema<TApp>[TMethod][SchemaPath][DollarMethod][\"input\"][\"json\"];\n\t\t\t\t}\n\t\t: \"form\" extends keyof HonoSchema<TApp>[TMethod][SchemaPath][DollarMethod][\"input\"]\n\t\t\t? {\n\t\t\t\t\tform: HonoSchema<TApp>[TMethod][SchemaPath][DollarMethod][\"input\"][\"form\"];\n\t\t\t\t}\n\t\t\t: { body?: unknown } | { form?: unknown }\n\t: EmptyObject;\n\ntype AvailableMethods<T extends Hono> = {\n\t[M in HttpMethod]: keyof HonoSchema<T>[M] extends never ? never : M;\n}[HttpMethod];\n\nexport interface WebSocketConfig {\n\t/**\n\t * Whether to automatically call accept() on the WebSocket before returning.\n\t * Defaults to true for convenience.\n\t *\n\t * In Cloudflare Workers, you must call accept() before using a WebSocket.\n\t * Setting this to false allows you to call accept() manually if needed.\n\t *\n\t * @default true\n\t */\n\tautoAccept?: boolean;\n}\n\nexport type TypedWebSocketFetcher<T extends Hono> = <\n\tSchemaPath extends string & keyof HonoSchema<T>[\"get\"],\n>(\n\trequest: {\n\t\turl: SchemaPath;\n\t\tconfig?: WebSocketConfig;\n\t} & FetcherParams<SchemaPath>,\n) => Promise<Response>;\n\nexport type BaseTypedHonoFetcher<T extends Hono> = {\n\t[M in AvailableMethods<T>]: TypedMethodFetcher<T, M>;\n} & (keyof HonoSchema<T>[\"get\"] extends never\n\t? // biome-ignore lint/complexity/noBannedTypes: We really do want an empty object if the get method is not available\n\t\t{}\n\t: { websocket: TypedWebSocketFetcher<T> });\n\ntype TypedDisposableMethodFetcher<T extends Hono, M extends HttpMethod> = <\n\tSchemaPath extends string & keyof HonoSchema<T>[M],\n>(\n\trequest: {\n\t\turl: SchemaPath;\n\t} & FetcherParams<SchemaPath> &\n\t\t(M extends \"get\" | \"delete\" ? EmptyObject : BodyParams<T, M, SchemaPath>),\n) => Promise<DoSchemaOutput<T, M, SchemaPath>>;\n\nexport type TypedDisposableWebSocketFetcher<T extends Hono> = <\n\tSchemaPath extends string & keyof HonoSchema<T>[\"get\"],\n>(\n\trequest: {\n\t\turl: SchemaPath;\n\t\tconfig?: WebSocketConfig;\n\t} & FetcherParams<SchemaPath>,\n) => Promise<Response & Disposable>;\n\n/**\n * Same shape as {@link BaseTypedHonoFetcher} but HTTP methods return\n * {@link RpcDisposableJsonResponse} and `websocket` returns `Response & Disposable`\n * so `using` on RPC results type-checks for Durable Object clients.\n *\n * @see https://developers.cloudflare.com/workers/runtime-apis/rpc/lifecycle/\n */\nexport type BaseDisposableTypedHonoFetcher<T extends Hono> = {\n\t[M in AvailableMethods<T>]: TypedDisposableMethodFetcher<T, M>;\n} & (keyof HonoSchema<T>[\"get\"] extends never\n\t? // biome-ignore lint/complexity/noBannedTypes: We really do want an empty object if the get method is not available\n\t\t{}\n\t: { websocket: TypedDisposableWebSocketFetcher<T> });\n\nconst createMethodFetcher = <T extends Hono, M extends HttpMethod>(\n\tfetcher: (\n\t\trequest: string,\n\t\tinit?: RequestInit,\n\t) => ReturnType<T[\"request\"]> | Promise<ReturnType<T[\"request\"]>>,\n\tmethod: M,\n): TypedMethodFetcher<T, M> => {\n\treturn (async (request) => {\n\t\tlet finalUrl: string = request.url;\n\n\t\tconst { init = {}, params, query } = request;\n\n\t\tif (params && typeof params === \"object\") {\n\t\t\tfinalUrl = Object.entries(params).reduce((acc, [key, value]) => {\n\t\t\t\treturn acc.replace(`:${key}`, value as string);\n\t\t\t}, finalUrl);\n\t\t}\n\n\t\tfinalUrl = appendQueryString(finalUrl, query);\n\n\t\tconst requestAsOptionalFormBody = request as {\n\t\t\tform?: unknown;\n\t\t\tbody?: unknown;\n\t\t};\n\n\t\tlet body: BodyInit | undefined;\n\t\tif (requestAsOptionalFormBody.form) {\n\t\t\tconst formData = new FormData();\n\t\t\tfor (const [key, value] of Object.entries(\n\t\t\t\trequestAsOptionalFormBody.form,\n\t\t\t)) {\n\t\t\t\tformData.append(key, value as string);\n\t\t\t}\n\t\t\tbody = formData;\n\t\t} else if (requestAsOptionalFormBody.body) {\n\t\t\tbody = JSON.stringify(requestAsOptionalFormBody.body) as BodyInit;\n\t\t}\n\n\t\tconst newHeaders = new Headers(\n\t\t\tinit.headers as unknown as ConstructorParameters<typeof Headers>[0],\n\t\t);\n\n\t\tif (body && !requestAsOptionalFormBody.form) {\n\t\t\tnewHeaders.set(\"Content-Type\", \"application/json\");\n\t\t}\n\n\t\ttry {\n\t\t\treturn await fetcher(finalUrl, {\n\t\t\t\t...restOfRequestInit(init),\n\t\t\t\tmethod: method.toUpperCase(),\n\t\t\t\theaders: newHeaders,\n\t\t\t\t...(body ? { body } : {}),\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tconsole.error(`Error ${method}ing`, error);\n\t\t\tthrow new Error(`Failed to ${method} ${finalUrl}: ${error}`);\n\t\t}\n\t}) as TypedMethodFetcher<T, M>;\n};\n\nconst createWebSocketFetcher = <T extends Hono>(\n\tfetcher: (\n\t\trequest: string,\n\t\tinit?: RequestInit,\n\t) => ReturnType<T[\"request\"]> | Promise<ReturnType<T[\"request\"]>>,\n): TypedWebSocketFetcher<T> => {\n\treturn (async (request) => {\n\t\tlet finalUrl: string = request.url;\n\n\t\tconst { init = {}, params, query, config } = request;\n\t\tconst autoAccept = config?.autoAccept ?? true; // Default to true\n\n\t\tif (params && typeof params === \"object\") {\n\t\t\tfinalUrl = Object.entries(params).reduce((acc, [key, value]) => {\n\t\t\t\treturn acc.replace(`:${key}`, value as string);\n\t\t\t}, finalUrl);\n\t\t}\n\n\t\tfinalUrl = appendQueryString(finalUrl, query);\n\n\t\tconst newHeaders = new Headers(\n\t\t\tinit.headers as unknown as ConstructorParameters<typeof Headers>[0],\n\t\t);\n\t\tnewHeaders.set(\"Upgrade\", \"websocket\");\n\n\t\ttry {\n\t\t\tconst response = await fetcher(finalUrl, {\n\t\t\t\t...restOfRequestInit(init),\n\t\t\t\tmethod: \"GET\",\n\t\t\t\theaders: newHeaders,\n\t\t\t});\n\n\t\t\t// Auto-accept the WebSocket if configured (default: true)\n\t\t\tif (autoAccept && response.webSocket) {\n\t\t\t\tresponse.webSocket.accept();\n\t\t\t}\n\n\t\t\treturn response;\n\t\t} catch (error) {\n\t\t\tconsole.error(\"Error upgrading to WebSocket\", error);\n\t\t\tthrow new Error(`Failed to upgrade WebSocket at ${finalUrl}: ${error}`);\n\t\t}\n\t}) as TypedWebSocketFetcher<T>;\n};\n\nexport type TypedHonoFetcher<T extends Hono> = BaseTypedHonoFetcher<T>;\n\nexport const honoFetcher = <T extends Hono>(\n\tfetcher: (\n\t\trequest: string,\n\t\tinit?: RequestInit,\n\t) => ReturnType<T[\"request\"]> | Promise<ReturnType<T[\"request\"]>>,\n): TypedHonoFetcher<T> => {\n\tconst methods = [\"get\", \"post\", \"put\", \"delete\", \"patch\"] as const;\n\n\tconst result = methods.reduce(\n\t\t(acc, method) => {\n\t\t\t(\n\t\t\t\tacc as TypedHonoFetcher<T> & {\n\t\t\t\t\t[M in typeof method]: TypedMethodFetcher<T, M>;\n\t\t\t\t}\n\t\t\t)[method] = createMethodFetcher(fetcher, method) as TypedMethodFetcher<\n\t\t\t\tT,\n\t\t\t\ttypeof method\n\t\t\t>;\n\t\t\treturn acc;\n\t\t},\n\t\t{} as TypedHonoFetcher<T>,\n\t);\n\n\t// Add websocket method\n\t(\n\t\tresult as TypedHonoFetcher<T> & { websocket?: TypedWebSocketFetcher<T> }\n\t).websocket = createWebSocketFetcher(fetcher);\n\n\treturn result;\n};\n"]}
@@ -0,0 +1,12 @@
1
+ import { honoFetcher } from './chunk-ULIZJ5OD.js';
2
+
3
+ // src/honoDirectFetcher.ts
4
+ var honoDirectFetcher = (baseUrl) => {
5
+ return honoFetcher((request, init) => {
6
+ return fetch(`${baseUrl}${request}`, init);
7
+ });
8
+ };
9
+
10
+ export { honoDirectFetcher };
11
+ //# sourceMappingURL=chunk-YCTPXFUH.js.map
12
+ //# sourceMappingURL=chunk-YCTPXFUH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/honoDirectFetcher.ts"],"names":[],"mappings":";;;AAGO,IAAM,iBAAA,GAAoB,CAChC,OAAA,KACyB;AACzB,EAAA,OAAO,WAAA,CAAe,CAAC,OAAA,EAAS,IAAA,KAAS;AACxC,IAAA,OAAO,MAAM,CAAA,EAAG,OAAO,CAAA,EAAG,OAAO,IAAI,IAAI,CAAA;AAAA,EAC1C,CAAC,CAAA;AACF","file":"chunk-YCTPXFUH.js","sourcesContent":["import type { Hono } from \"hono\";\nimport { honoFetcher, type TypedHonoFetcher } from \"./honoFetcher\";\n\nexport const honoDirectFetcher = <T extends Hono>(\n\tbaseUrl: string,\n): TypedHonoFetcher<T> => {\n\treturn honoFetcher<T>((request, init) => {\n\t\treturn fetch(`${baseUrl}${request}`, init) as ReturnType<T[\"request\"]>;\n\t});\n};\n"]}
@@ -0,0 +1,7 @@
1
+ import { Hono } from 'hono';
2
+ import { TypedHonoFetcher } from './honoFetcher.js';
3
+ import 'hono/types';
4
+
5
+ declare const honoDirectFetcher: <T extends Hono>(baseUrl: string) => TypedHonoFetcher<T>;
6
+
7
+ export { honoDirectFetcher };
@@ -0,0 +1,4 @@
1
+ export { honoDirectFetcher } from './chunk-YCTPXFUH.js';
2
+ import './chunk-ULIZJ5OD.js';
3
+ //# sourceMappingURL=honoDirectFetcher.js.map
4
+ //# sourceMappingURL=honoDirectFetcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"honoDirectFetcher.js"}
@@ -0,0 +1,50 @@
1
+ import { Schema, Hono } from 'hono';
2
+ import { ExtractSchema } from 'hono/types';
3
+ import { BaseDisposableTypedHonoFetcher, TypedHonoFetcher } from './honoFetcher.js';
4
+
5
+ type DOWithHonoApp<S extends Schema = Schema> = Rpc.DurableObjectBranded & {
6
+ app: Hono<any, S>;
7
+ };
8
+ type DOSchemaMap<T extends DOWithHonoApp> = T extends DOWithHonoApp ? ExtractSchema<T["app"]> : never;
9
+ type DOSchemaKeys<T extends DOWithHonoApp> = string & keyof DOSchemaMap<T>;
10
+ type DOStubSchema<T extends DurableObjectStub> = T extends DurableObjectStub<infer S> ? S extends DOWithHonoApp ? ExtractSchema<S["app"]> : never : never;
11
+ /**
12
+ * Fetcher for a **real** `DurableObjectStub`: HTTP results are {@link RpcDisposableJsonResponse}
13
+ * and `websocket` returns `Response & Disposable`, matching Workers RPC when the runtime attaches
14
+ * disposers. Use with {@link honoDoFetcher} / {@link honoDoFetcherWithName} / {@link honoDoFetcherWithId}
15
+ * when `T` is a full stub—not with a minimal `Pick<stub, "fetch">` mock (that path uses plain
16
+ * {@link TypedHonoFetcher} responses instead).
17
+ *
18
+ * @see https://developers.cloudflare.com/workers/runtime-apis/rpc/lifecycle/
19
+ */
20
+ type TypedDoFetcher<T extends DurableObjectStub> = BaseDisposableTypedHonoFetcher<Hono<any, DOStubSchema<T>>>;
21
+ /**
22
+ * Argument to {@link honoDoFetcher}: a **full** {@link DurableObjectStub} (production) or a minimal
23
+ * **`{ fetch }`** mock. Only the full stub is typed as {@link TypedDoFetcher} with disposable RPC
24
+ * responses; **`Pick<stub, "fetch">`** is typed as {@link TypedHonoFetcher} for `Hono` with ordinary
25
+ * `JsonResponse` / `Response` (no `Disposable` on results—matches mocks without `Symbol.dispose`).
26
+ *
27
+ * @see https://developers.cloudflare.com/workers/runtime-apis/rpc/lifecycle/
28
+ */
29
+ type HonoDoFetcherStubInput = DurableObjectStub<DOWithHonoApp> | Pick<DurableObjectStub<DOWithHonoApp>, "fetch">;
30
+ /**
31
+ * Typed fetcher for a Durable Object stub.
32
+ *
33
+ * - **Full `DurableObjectStub`:** return type is {@link TypedDoFetcher} **`& Disposable`** — each
34
+ * HTTP/WebSocket result is typed as disposable (`RpcDisposableJsonResponse` / `Response & Disposable`)
35
+ * so **`using res = await …`** type-checks when `"ESNext.Disposable"` is in `lib`, matching Workers RPC
36
+ * when the runtime attaches `[Symbol.dispose]` (see `@see` below).
37
+ * - **`Pick<stub, "fetch">` only (e.g. tests):** return type is **`TypedHonoFetcher<Hono> & Disposable`**
38
+ * — same **`JsonResponse` / `Response`** shapes as {@link honoFetcher}; results are **not** typed as
39
+ * `Disposable` so typings are not faked for mocks that lack RPC disposers.
40
+ *
41
+ * Disposing only the fetcher (`using api = …`) releases the **stub**; RPC **`Response`** disposal
42
+ * (when applicable) is separate — prefer **`using res`**, **`res[Symbol.dispose]()`**, or **`DisposableStack`**.
43
+ *
44
+ * @see https://developers.cloudflare.com/workers/runtime-apis/rpc/lifecycle/
45
+ */
46
+ declare const honoDoFetcher: <const T extends HonoDoFetcherStubInput>(durableObject: T) => T extends DurableObjectStub<DOWithHonoApp> ? TypedDoFetcher<T> & Disposable : TypedHonoFetcher<Hono> & Disposable;
47
+ declare const honoDoFetcherWithName: <const T extends Rpc.DurableObjectBranded & DOWithHonoApp>(namespace: DurableObjectNamespace<T>, name: string) => TypedDoFetcher<DurableObjectStub<T>> & Disposable;
48
+ declare const honoDoFetcherWithId: <const T extends Rpc.DurableObjectBranded & DOWithHonoApp>(namespace: DurableObjectNamespace<T>, id: string) => TypedDoFetcher<DurableObjectStub<T>> & Disposable;
49
+
50
+ export { type DOSchemaKeys, type DOSchemaMap, type DOStubSchema, type DOWithHonoApp, type HonoDoFetcherStubInput, type TypedDoFetcher, honoDoFetcher, honoDoFetcherWithId, honoDoFetcherWithName };
@@ -0,0 +1,4 @@
1
+ export { honoDoFetcher, honoDoFetcherWithId, honoDoFetcherWithName } from './chunk-EXAM5VFZ.js';
2
+ import './chunk-ULIZJ5OD.js';
3
+ //# sourceMappingURL=honoDoFetcher.js.map
4
+ //# sourceMappingURL=honoDoFetcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"honoDoFetcher.js"}
@@ -0,0 +1,113 @@
1
+ import { Hono } from 'hono';
2
+ import { ExtractSchema } from 'hono/types';
3
+
4
+ type ParsePathParams<T extends string> = T extends `${infer _Start}/:${infer Param}/${infer Rest}` ? {
5
+ [K in Param | keyof ParsePathParams<`/${Rest}`>]: string;
6
+ } : T extends `${infer _Start}/:${infer Param}` ? {
7
+ [K in Param]: string;
8
+ } : never;
9
+ type HttpMethod = "get" | "post" | "put" | "delete" | "patch";
10
+ type HonoSchemaKeys<T extends Hono> = string & keyof ExtractSchema<T>;
11
+ type FilterKeysByMethod<TApp extends ExtractSchema<unknown>, TMethod extends HttpMethod> = {
12
+ [K in keyof TApp as TApp[K] extends {
13
+ [key in `$${TMethod}`]: unknown;
14
+ } ? K : never]: TApp[K];
15
+ };
16
+ type HonoSchema<TApp extends Hono> = {
17
+ [M in HttpMethod]: FilterKeysByMethod<ExtractSchema<TApp>, M>;
18
+ };
19
+ type JsonResponse<T> = Omit<Response, "json"> & {
20
+ json: () => Promise<T>;
21
+ };
22
+ /**
23
+ * {@link JsonResponse} intersected with `Disposable` for Workers RPC: `Response`
24
+ * values from `DurableObjectStub#fetch()` may implement `[Symbol.dispose]` even
25
+ * though `Fetcher.fetch` is still typed as `Promise<Response>`. Use with
26
+ * {@link BaseDisposableTypedHonoFetcher} (and `TypedDoFetcher` from `./honoDoFetcher`) so
27
+ * `using resp = await api.get(...)` type-checks when `"ESNext.Disposable"` is in `lib`.
28
+ *
29
+ * @see https://developers.cloudflare.com/workers/runtime-apis/rpc/lifecycle/
30
+ */
31
+ type RpcDisposableJsonResponse<T> = JsonResponse<T> & Disposable;
32
+ type HasPathParams<T extends string> = T extends `${string}:${string}` ? true : false;
33
+ /**
34
+ * Values allowed in the optional `query` object on fetcher requests.
35
+ * `null` and `undefined` entries are omitted from the serialized query string.
36
+ */
37
+ type HonoFetcherQueryParamValue = string | number | boolean;
38
+ type HonoFetcherQueryParams = Record<string, HonoFetcherQueryParamValue | null | undefined>;
39
+ type FetcherParams<SchemaPath extends string> = HasPathParams<SchemaPath> extends true ? {
40
+ params: ParsePathParams<SchemaPath>;
41
+ query?: HonoFetcherQueryParams;
42
+ init?: RequestInit;
43
+ } : {
44
+ params?: never;
45
+ query?: HonoFetcherQueryParams;
46
+ init?: RequestInit;
47
+ };
48
+ type EmptyObject = {};
49
+ type TypedMethodFetcher<T extends Hono, M extends HttpMethod> = <SchemaPath extends string & keyof HonoSchema<T>[M]>(request: {
50
+ url: SchemaPath;
51
+ } & FetcherParams<SchemaPath> & (M extends "get" | "delete" ? EmptyObject : BodyParams<T, M, SchemaPath>)) => Promise<SchemaOutput<T, M, SchemaPath>>;
52
+ type SchemaOutput<T extends Hono, M extends HttpMethod, SchemaPath extends string & keyof HonoSchema<T>[M], DollarM extends `$${M}` & keyof HonoSchema<T>[M][SchemaPath] = `$${M}` & keyof HonoSchema<T>[M][SchemaPath]> = "output" extends keyof HonoSchema<T>[M][SchemaPath][DollarM] ? JsonResponse<HonoSchema<T>[M][SchemaPath][DollarM]["output"]> : never;
53
+ type DoSchemaOutput<T extends Hono, M extends HttpMethod, SchemaPath extends string & keyof HonoSchema<T>[M], DollarM extends `$${M}` & keyof HonoSchema<T>[M][SchemaPath] = `$${M}` & keyof HonoSchema<T>[M][SchemaPath]> = "output" extends keyof HonoSchema<T>[M][SchemaPath][DollarM] ? RpcDisposableJsonResponse<HonoSchema<T>[M][SchemaPath][DollarM]["output"]> : never;
54
+ type BodyParams<TApp extends Hono, TMethod extends HttpMethod, SchemaPath extends string & keyof HonoSchema<TApp>[TMethod], DollarMethod extends `$${TMethod}` & keyof HonoSchema<TApp>[TMethod][SchemaPath] = `$${TMethod}` & keyof HonoSchema<TApp>[TMethod][SchemaPath]> = "input" extends keyof HonoSchema<TApp>[TMethod][SchemaPath][DollarMethod] ? "json" extends keyof HonoSchema<TApp>[TMethod][SchemaPath][DollarMethod]["input"] ? "form" extends keyof HonoSchema<TApp>[TMethod][SchemaPath][DollarMethod]["input"] ? {
55
+ body: HonoSchema<TApp>[TMethod][SchemaPath][DollarMethod]["input"]["json"];
56
+ } | {
57
+ form: HonoSchema<TApp>[TMethod][SchemaPath][DollarMethod]["input"]["form"];
58
+ } : {
59
+ body: HonoSchema<TApp>[TMethod][SchemaPath][DollarMethod]["input"]["json"];
60
+ } : "form" extends keyof HonoSchema<TApp>[TMethod][SchemaPath][DollarMethod]["input"] ? {
61
+ form: HonoSchema<TApp>[TMethod][SchemaPath][DollarMethod]["input"]["form"];
62
+ } : {
63
+ body?: unknown;
64
+ } | {
65
+ form?: unknown;
66
+ } : EmptyObject;
67
+ type AvailableMethods<T extends Hono> = {
68
+ [M in HttpMethod]: keyof HonoSchema<T>[M] extends never ? never : M;
69
+ }[HttpMethod];
70
+ interface WebSocketConfig {
71
+ /**
72
+ * Whether to automatically call accept() on the WebSocket before returning.
73
+ * Defaults to true for convenience.
74
+ *
75
+ * In Cloudflare Workers, you must call accept() before using a WebSocket.
76
+ * Setting this to false allows you to call accept() manually if needed.
77
+ *
78
+ * @default true
79
+ */
80
+ autoAccept?: boolean;
81
+ }
82
+ type TypedWebSocketFetcher<T extends Hono> = <SchemaPath extends string & keyof HonoSchema<T>["get"]>(request: {
83
+ url: SchemaPath;
84
+ config?: WebSocketConfig;
85
+ } & FetcherParams<SchemaPath>) => Promise<Response>;
86
+ type BaseTypedHonoFetcher<T extends Hono> = {
87
+ [M in AvailableMethods<T>]: TypedMethodFetcher<T, M>;
88
+ } & (keyof HonoSchema<T>["get"] extends never ? {} : {
89
+ websocket: TypedWebSocketFetcher<T>;
90
+ });
91
+ type TypedDisposableMethodFetcher<T extends Hono, M extends HttpMethod> = <SchemaPath extends string & keyof HonoSchema<T>[M]>(request: {
92
+ url: SchemaPath;
93
+ } & FetcherParams<SchemaPath> & (M extends "get" | "delete" ? EmptyObject : BodyParams<T, M, SchemaPath>)) => Promise<DoSchemaOutput<T, M, SchemaPath>>;
94
+ type TypedDisposableWebSocketFetcher<T extends Hono> = <SchemaPath extends string & keyof HonoSchema<T>["get"]>(request: {
95
+ url: SchemaPath;
96
+ config?: WebSocketConfig;
97
+ } & FetcherParams<SchemaPath>) => Promise<Response & Disposable>;
98
+ /**
99
+ * Same shape as {@link BaseTypedHonoFetcher} but HTTP methods return
100
+ * {@link RpcDisposableJsonResponse} and `websocket` returns `Response & Disposable`
101
+ * so `using` on RPC results type-checks for Durable Object clients.
102
+ *
103
+ * @see https://developers.cloudflare.com/workers/runtime-apis/rpc/lifecycle/
104
+ */
105
+ type BaseDisposableTypedHonoFetcher<T extends Hono> = {
106
+ [M in AvailableMethods<T>]: TypedDisposableMethodFetcher<T, M>;
107
+ } & (keyof HonoSchema<T>["get"] extends never ? {} : {
108
+ websocket: TypedDisposableWebSocketFetcher<T>;
109
+ });
110
+ type TypedHonoFetcher<T extends Hono> = BaseTypedHonoFetcher<T>;
111
+ declare const honoFetcher: <T extends Hono>(fetcher: (request: string, init?: RequestInit) => ReturnType<T["request"]> | Promise<ReturnType<T["request"]>>) => TypedHonoFetcher<T>;
112
+
113
+ export { type BaseDisposableTypedHonoFetcher, type BaseTypedHonoFetcher, type HonoFetcherQueryParamValue, type HonoFetcherQueryParams, type HonoSchemaKeys, type HttpMethod, type JsonResponse, type ParsePathParams, type RpcDisposableJsonResponse, type TypedDisposableWebSocketFetcher, type TypedHonoFetcher, type TypedWebSocketFetcher, type WebSocketConfig, honoFetcher };
@@ -0,0 +1,3 @@
1
+ export { honoFetcher } from './chunk-ULIZJ5OD.js';
2
+ //# sourceMappingURL=honoFetcher.js.map
3
+ //# sourceMappingURL=honoFetcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"honoFetcher.js"}
@@ -0,0 +1,5 @@
1
+ export { honoDirectFetcher } from './honoDirectFetcher.js';
2
+ export { DOSchemaKeys, DOSchemaMap, DOStubSchema, DOWithHonoApp, HonoDoFetcherStubInput, TypedDoFetcher, honoDoFetcher, honoDoFetcherWithId, honoDoFetcherWithName } from './honoDoFetcher.js';
3
+ export { BaseDisposableTypedHonoFetcher, BaseTypedHonoFetcher, HonoFetcherQueryParamValue, HonoFetcherQueryParams, HonoSchemaKeys, HttpMethod, JsonResponse, ParsePathParams, RpcDisposableJsonResponse, TypedDisposableWebSocketFetcher, TypedHonoFetcher, TypedWebSocketFetcher, WebSocketConfig, honoFetcher } from './honoFetcher.js';
4
+ import 'hono';
5
+ import 'hono/types';
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { honoDirectFetcher } from './chunk-YCTPXFUH.js';
2
+ export { honoDoFetcher, honoDoFetcherWithId, honoDoFetcherWithName } from './chunk-EXAM5VFZ.js';
3
+ export { honoFetcher } from './chunk-ULIZJ5OD.js';
4
+ //# sourceMappingURL=index.js.map
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
package/package.json CHANGED
@@ -1,28 +1,32 @@
1
1
  {
2
2
  "name": "@firtoz/hono-fetcher",
3
- "version": "2.5.0",
3
+ "version": "2.7.0",
4
4
  "description": "Type-safe Hono API client with full TypeScript inference for routes, params, and payloads",
5
- "main": "./src/index.ts",
6
- "module": "./src/index.ts",
7
- "types": "./src/index.ts",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
8
9
  "exports": {
9
10
  ".": {
10
- "types": "./src/index.ts",
11
- "import": "./src/index.ts",
12
- "require": "./src/index.ts"
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
13
  },
14
14
  "./honoDoFetcher": {
15
- "types": "./src/honoDoFetcher.ts",
16
- "import": "./src/honoDoFetcher.ts",
17
- "require": "./src/honoDoFetcher.ts"
15
+ "types": "./dist/honoDoFetcher.d.ts",
16
+ "import": "./dist/honoDoFetcher.js"
18
17
  }
19
18
  },
20
19
  "files": [
20
+ "dist/**/*.js",
21
+ "dist/**/*.js.map",
22
+ "dist/**/*.d.ts",
21
23
  "src/**/*.ts",
22
24
  "!src/**/*.test.ts",
23
25
  "README.md"
24
26
  ],
25
27
  "scripts": {
28
+ "build": "tsup",
29
+ "prepack": "bun run build",
26
30
  "typecheck": "tsgo --noEmit -p ./tsconfig.json",
27
31
  "lint": "biome check --write src",
28
32
  "lint:ci": "biome ci src",
@@ -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,