@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 +31 -19
- package/package.json +1 -1
- package/src/honoDoFetcher.ts +43 -6
- package/src/honoFetcher.ts +53 -0
- package/src/index.ts +4 -0
package/README.md
CHANGED
|
@@ -131,14 +131,13 @@ export class ChatRoomDO extends DurableObject {
|
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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:**
|
|
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
|
|
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
|
|
427
|
-
|
|
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
|
-
|
|
440
|
-
await
|
|
442
|
+
using res = await api.get({ url: '/status' });
|
|
443
|
+
await res.json();
|
|
441
444
|
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
460
|
-
|
|
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
package/src/honoDoFetcher.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { Hono, Schema } from "hono";
|
|
2
2
|
import type { ExtractSchema } from "hono/types";
|
|
3
|
-
import {
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
/**
|
|
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>
|
package/src/honoFetcher.ts
CHANGED
|
@@ -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,
|