@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 +44 -28
- package/dist/chunk-EXAM5VFZ.js +43 -0
- package/dist/chunk-EXAM5VFZ.js.map +1 -0
- package/dist/chunk-ULIZJ5OD.js +112 -0
- package/dist/chunk-ULIZJ5OD.js.map +1 -0
- package/dist/chunk-YCTPXFUH.js +12 -0
- package/dist/chunk-YCTPXFUH.js.map +1 -0
- package/dist/honoDirectFetcher.d.ts +7 -0
- package/dist/honoDirectFetcher.js +4 -0
- package/dist/honoDirectFetcher.js.map +1 -0
- package/dist/honoDoFetcher.d.ts +50 -0
- package/dist/honoDoFetcher.js +4 -0
- package/dist/honoDoFetcher.js.map +1 -0
- package/dist/honoFetcher.d.ts +113 -0
- package/dist/honoFetcher.js +3 -0
- package/dist/honoFetcher.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/package.json +14 -10
- package/src/honoDoFetcher.ts +43 -6
- package/src/honoFetcher.ts +53 -0
- package/src/index.ts +4 -0
package/README.md
CHANGED
|
@@ -4,7 +4,11 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/@firtoz/hono-fetcher)
|
|
5
5
|
[](https://github.com/firtoz/fullstack-toolkit/blob/main/LICENSE)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
[](https://www.typescriptlang.org/)
|
|
8
|
+
[](https://hono.dev)
|
|
9
|
+
[](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
|
-
//
|
|
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
|
-
|
|
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
|
|
378
|
+
### Integration with StandardSchemaWebSocketClient
|
|
376
379
|
|
|
377
|
-
For even better type safety, combine with `@firtoz/websocket-do`'s `
|
|
380
|
+
For even better type safety, combine with `@firtoz/websocket-do`'s `StandardSchemaWebSocketClient`:
|
|
378
381
|
|
|
379
382
|
```typescript
|
|
380
|
-
import {
|
|
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
|
|
390
|
+
config: { autoAccept: false }, // Let StandardSchemaWebSocketClient handle acceptance
|
|
388
391
|
});
|
|
389
392
|
|
|
390
393
|
// 2. Wrap with type-safe client
|
|
391
|
-
const client = new
|
|
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
|
|
408
|
+
await client.send({ type: 'chat', text: 'Hello!' }); // Validated with Standard Schema
|
|
406
409
|
```
|
|
407
410
|
|
|
408
|
-
See the
|
|
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
|
-
|
|
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:**
|
|
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
|
|
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
|
|
427
|
-
|
|
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
|
-
|
|
440
|
-
await
|
|
446
|
+
using res = await api.get({ url: '/status' });
|
|
447
|
+
await res.json();
|
|
441
448
|
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
460
|
-
|
|
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 `
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"honoFetcher.js"}
|
package/dist/index.d.ts
ADDED
|
@@ -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.
|
|
3
|
+
"version": "2.7.0",
|
|
4
4
|
"description": "Type-safe Hono API client with full TypeScript inference for routes, params, and payloads",
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"
|
|
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": "./
|
|
11
|
-
"import": "./
|
|
12
|
-
"require": "./src/index.ts"
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
13
|
},
|
|
14
14
|
"./honoDoFetcher": {
|
|
15
|
-
"types": "./
|
|
16
|
-
"import": "./
|
|
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",
|
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,
|