ts-procedures 5.15.0 → 5.16.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/agent_config/claude-code/skills/ts-procedures/SKILL.md +1 -1
- package/agent_config/claude-code/skills/ts-procedures/api-reference.md +57 -4
- package/agent_config/claude-code/skills/ts-procedures/patterns.md +102 -3
- package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/client.md +33 -5
- package/agent_config/copilot/copilot-instructions.md +55 -7
- package/agent_config/cursor/cursorrules +55 -7
- package/build/client/call.d.ts +18 -9
- package/build/client/call.js +25 -19
- package/build/client/call.js.map +1 -1
- package/build/client/call.test.js +167 -17
- package/build/client/call.test.js.map +1 -1
- package/build/client/index.d.ts +1 -1
- package/build/client/index.js +18 -3
- package/build/client/index.js.map +1 -1
- package/build/client/index.test.js +104 -0
- package/build/client/index.test.js.map +1 -1
- package/build/client/resolve-options.d.ts +45 -0
- package/build/client/resolve-options.js +82 -0
- package/build/client/resolve-options.js.map +1 -0
- package/build/client/resolve-options.test.d.ts +1 -0
- package/build/client/resolve-options.test.js +158 -0
- package/build/client/resolve-options.test.js.map +1 -0
- package/build/client/stream.d.ts +18 -9
- package/build/client/stream.js +24 -19
- package/build/client/stream.js.map +1 -1
- package/build/client/stream.test.js +102 -46
- package/build/client/stream.test.js.map +1 -1
- package/build/client/types.d.ts +68 -1
- package/build/client/types.js +1 -1
- package/build/codegen/e2e.test.js +141 -0
- package/build/codegen/e2e.test.js.map +1 -1
- package/build/codegen/emit-client-runtime.js +3 -0
- package/build/codegen/emit-client-runtime.js.map +1 -1
- package/docs/client-and-codegen.md +123 -2
- package/package.json +1 -1
- package/src/client/call.test.ts +202 -29
- package/src/client/call.ts +41 -28
- package/src/client/index.test.ts +117 -0
- package/src/client/index.ts +25 -8
- package/src/client/resolve-options.test.ts +205 -0
- package/src/client/resolve-options.ts +113 -0
- package/src/client/stream.test.ts +132 -107
- package/src/client/stream.ts +40 -25
- package/src/client/types.ts +74 -2
- package/src/codegen/e2e.test.ts +151 -0
- package/src/codegen/emit-client-runtime.ts +3 -0
- package/src/implementations/http/README.md +9 -1
|
@@ -151,7 +151,7 @@ The npm package ships user-facing documentation with narrative explanations and
|
|
|
151
151
|
| `docs/core.md` | Procedures factory, Create, CreateStream, schema.input, error handling |
|
|
152
152
|
| `docs/streaming.md` | Streaming procedures, AbortSignal, SSE patterns |
|
|
153
153
|
| `docs/http-integrations.md` | Express RPC, Hono RPC/Stream/API builders, DocRegistry |
|
|
154
|
-
| `docs/client-and-codegen.md` | Client code generation, createClient, CLI options |
|
|
154
|
+
| `docs/client-and-codegen.md` | Client code generation, createClient, per-call options (timeout/signal/headers/basePath/meta), client-level defaults, typed RequestMeta augmentation, CLI options |
|
|
155
155
|
|
|
156
156
|
## Workflow
|
|
157
157
|
|
|
@@ -732,6 +732,7 @@ function createClient<TScopes>(config: {
|
|
|
732
732
|
basePath: string
|
|
733
733
|
scopes: (client: ClientInstance) => TScopes
|
|
734
734
|
hooks?: ClientHooks
|
|
735
|
+
defaults?: ProcedureCallDefaults
|
|
735
736
|
}): TScopes
|
|
736
737
|
```
|
|
737
738
|
|
|
@@ -740,7 +741,8 @@ function createClient<TScopes>(config: {
|
|
|
740
741
|
- `config.adapter` — Transport adapter implementing `ClientAdapter`. Use `createFetchAdapter()` for fetch-based transport.
|
|
741
742
|
- `config.basePath` — Base URL prepended to all request paths (e.g., `'http://localhost:3000'`).
|
|
742
743
|
- `config.scopes` — Factory function that receives a raw `ClientInstance` and returns the typed scope object. The generated `create${ServiceName}Bindings` export (defaults to `createApiBindings`; pass `--service-name <Name>` to rename).
|
|
743
|
-
- `config.hooks` — Optional global hooks applied to every call.
|
|
744
|
+
- `config.hooks` — Optional global hooks applied to every call. Per-call hooks (passed via the options bag) run *after* global hooks — they stack, not replace.
|
|
745
|
+
- `config.defaults` — Optional default request options (timeout, signal, headers, basePath) applied to every call. Overridden by per-call options.
|
|
744
746
|
|
|
745
747
|
### Return Value
|
|
746
748
|
|
|
@@ -750,11 +752,51 @@ Returns the result of `config.scopes(clientInstance)` — a typed object where e
|
|
|
750
752
|
|
|
751
753
|
```typescript
|
|
752
754
|
interface ClientHooks {
|
|
753
|
-
onBeforeRequest?: (ctx:
|
|
754
|
-
onAfterResponse?: (ctx:
|
|
755
|
+
onBeforeRequest?: (ctx: BeforeRequestContext) => BeforeRequestContext | Promise<BeforeRequestContext>
|
|
756
|
+
onAfterResponse?: (ctx: AfterResponseContext) => void | Promise<void>
|
|
757
|
+
onError?: (ctx: ErrorContext) => void | Promise<void>
|
|
755
758
|
}
|
|
756
759
|
```
|
|
757
760
|
|
|
761
|
+
### ProcedureCallDefaults / ProcedureCallOptions
|
|
762
|
+
|
|
763
|
+
`ProcedureCallDefaults` is the shape used at `config.defaults` and is a strict subset of `ProcedureCallOptions` (per-call options). `ProcedureCallOptions` additionally includes the `ClientHooks` fields so a single options bag covers both request config and hooks.
|
|
764
|
+
|
|
765
|
+
```typescript
|
|
766
|
+
interface ProcedureCallDefaults {
|
|
767
|
+
signal?: AbortSignal // cancel signal (combined with per-call via AbortSignal.any)
|
|
768
|
+
timeout?: number // ms — per-call timeout:0 disables an inherited default
|
|
769
|
+
headers?: Record<string, string> // merged (per-call keys win)
|
|
770
|
+
basePath?: string // per-call > default > config.basePath
|
|
771
|
+
meta?: RequestMeta // typed per-request metadata (see RequestMeta below)
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
interface ProcedureCallOptions extends ProcedureCallDefaults, ClientHooks {}
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
### RequestMeta (typed per-request metadata)
|
|
778
|
+
|
|
779
|
+
`RequestMeta` is an empty interface designed for TypeScript declaration merging. Augment it in your project to type the `meta` field end-to-end (per-call options, hook contexts, and the adapter).
|
|
780
|
+
|
|
781
|
+
```typescript
|
|
782
|
+
// Self-contained (code-generated) client
|
|
783
|
+
declare module './generated/_types' {
|
|
784
|
+
interface RequestMeta {
|
|
785
|
+
traceId: string
|
|
786
|
+
priority?: 'high' | 'low'
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// Or when using ts-procedures/client directly
|
|
791
|
+
declare module 'ts-procedures/client' {
|
|
792
|
+
interface RequestMeta {
|
|
793
|
+
traceId: string
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
After augmentation, `request.meta` is typed in adapters, hooks, defaults, and per-call options. If `RequestMeta` declares required fields, supply them via `defaults.meta` or per-call `options.meta` — the merged shape must satisfy them at runtime.
|
|
799
|
+
|
|
758
800
|
### Example
|
|
759
801
|
|
|
760
802
|
```typescript
|
|
@@ -765,6 +807,7 @@ const client = createClient({
|
|
|
765
807
|
adapter: createFetchAdapter(),
|
|
766
808
|
basePath: 'http://localhost:3000',
|
|
767
809
|
scopes: createApiBindings,
|
|
810
|
+
defaults: { timeout: 30_000, headers: { 'X-Client-Version': '1.0.0' } },
|
|
768
811
|
hooks: {
|
|
769
812
|
onBeforeRequest(ctx) {
|
|
770
813
|
ctx.request.headers = { ...ctx.request.headers, Authorization: `Bearer ${getToken()}` }
|
|
@@ -773,7 +816,17 @@ const client = createClient({
|
|
|
773
816
|
},
|
|
774
817
|
})
|
|
775
818
|
|
|
776
|
-
|
|
819
|
+
// Per-call timeout + signal + hooks — all in one options bag
|
|
820
|
+
const controller = new AbortController()
|
|
821
|
+
const user = await client.users.GetUser(
|
|
822
|
+
{ pathParams: { id: '123' } },
|
|
823
|
+
{
|
|
824
|
+
timeout: 5000,
|
|
825
|
+
signal: controller.signal,
|
|
826
|
+
headers: { 'X-Request-Id': crypto.randomUUID() },
|
|
827
|
+
onAfterResponse(ctx) { log(ctx) },
|
|
828
|
+
},
|
|
829
|
+
)
|
|
777
830
|
|
|
778
831
|
// Reach types via the namespace: Api.Users.GetUser.Params, Api.Errors.ProcedureError
|
|
779
832
|
```
|
|
@@ -841,10 +841,11 @@ const result = await stream.result // Typed as WatchNotificationsReturn
|
|
|
841
841
|
|
|
842
842
|
---
|
|
843
843
|
|
|
844
|
-
## Per-Procedure
|
|
844
|
+
## Per-Procedure Hooks
|
|
845
|
+
|
|
846
|
+
Per-call hooks run *after* global hooks (they don't replace them). They live in the same options bag as `timeout`, `signal`, `headers`, etc.
|
|
845
847
|
|
|
846
848
|
```typescript
|
|
847
|
-
// Override hooks for a specific call
|
|
848
849
|
await client.users.GetUser({ pathParams: { id: '123' } }, {
|
|
849
850
|
onAfterResponse(ctx) {
|
|
850
851
|
const rateLimit = ctx.response.headers['x-rate-limit-remaining']
|
|
@@ -855,6 +856,102 @@ await client.users.GetUser({ pathParams: { id: '123' } }, {
|
|
|
855
856
|
|
|
856
857
|
---
|
|
857
858
|
|
|
859
|
+
## Per-Call Request Options (timeout, signal, headers, basePath)
|
|
860
|
+
|
|
861
|
+
Every generated callable takes an optional second argument for per-call config. The options bag covers both request-level config and hooks.
|
|
862
|
+
|
|
863
|
+
```typescript
|
|
864
|
+
// Timeout — aborts the request after 5 seconds
|
|
865
|
+
const user = await client.users.GetUser({ pathParams: { id: '123' } }, { timeout: 5000 })
|
|
866
|
+
|
|
867
|
+
// Cancellation — supply your own AbortSignal
|
|
868
|
+
const controller = new AbortController()
|
|
869
|
+
const user = await client.users.GetUser(
|
|
870
|
+
{ pathParams: { id: '123' } },
|
|
871
|
+
{ signal: controller.signal },
|
|
872
|
+
)
|
|
873
|
+
|
|
874
|
+
// Extra per-call headers
|
|
875
|
+
await client.users.GetUser(
|
|
876
|
+
{ pathParams: { id: '123' } },
|
|
877
|
+
{ headers: { 'X-Request-Id': crypto.randomUUID() } },
|
|
878
|
+
)
|
|
879
|
+
|
|
880
|
+
// Override base path for a single call (multi-region, dev/prod switching)
|
|
881
|
+
await client.users.GetUser(
|
|
882
|
+
{ pathParams: { id: '123' } },
|
|
883
|
+
{ basePath: 'https://api-eu.example.com' },
|
|
884
|
+
)
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
---
|
|
888
|
+
|
|
889
|
+
## Client-Level Defaults
|
|
890
|
+
|
|
891
|
+
Set defaults via `config.defaults` — applied to every call, overridden by per-call options. Headers merge (per-call keys win); `signal` combines via `AbortSignal.any` (whichever aborts first wins); per-call `timeout: 0` disables an inherited default timeout.
|
|
892
|
+
|
|
893
|
+
```typescript
|
|
894
|
+
const client = createClient({
|
|
895
|
+
adapter: createFetchAdapter(),
|
|
896
|
+
basePath: 'http://localhost:3000',
|
|
897
|
+
scopes: createApiBindings,
|
|
898
|
+
defaults: {
|
|
899
|
+
timeout: 30_000,
|
|
900
|
+
headers: { 'X-Client-Version': '1.0.0' },
|
|
901
|
+
},
|
|
902
|
+
})
|
|
903
|
+
```
|
|
904
|
+
|
|
905
|
+
---
|
|
906
|
+
|
|
907
|
+
## Typed Per-Request Meta (RequestMeta Augmentation)
|
|
908
|
+
|
|
909
|
+
`AdapterRequest.meta` is typed via the `RequestMeta` interface — declared empty so developers augment it via TypeScript declaration merging. Augmented fields are then typed end-to-end: per-call options, hook contexts, and adapter.
|
|
910
|
+
|
|
911
|
+
```typescript
|
|
912
|
+
// Self-contained (code-generated) client
|
|
913
|
+
declare module './generated/_types' {
|
|
914
|
+
interface RequestMeta {
|
|
915
|
+
traceId: string
|
|
916
|
+
priority?: 'high' | 'low'
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// Or, when using ts-procedures/client directly
|
|
921
|
+
declare module 'ts-procedures/client' {
|
|
922
|
+
interface RequestMeta {
|
|
923
|
+
traceId: string
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// After augmentation, meta is typed everywhere:
|
|
928
|
+
await client.users.GetUser(
|
|
929
|
+
{ pathParams: { id: '123' } },
|
|
930
|
+
{ meta: { traceId: 'req-abc', priority: 'high' } }, // typed
|
|
931
|
+
)
|
|
932
|
+
|
|
933
|
+
// Typed in hooks
|
|
934
|
+
hooks: {
|
|
935
|
+
onBeforeRequest(ctx) {
|
|
936
|
+
const trace = ctx.request.meta?.traceId // string | undefined
|
|
937
|
+
return ctx
|
|
938
|
+
},
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// Typed in adapters
|
|
942
|
+
const adapter: ClientAdapter = {
|
|
943
|
+
async request(req) {
|
|
944
|
+
const priority = req.meta?.priority // 'high' | 'low' | undefined
|
|
945
|
+
return { status: 200, headers: {}, body: {} }
|
|
946
|
+
},
|
|
947
|
+
async stream(req) { /* ... */ },
|
|
948
|
+
}
|
|
949
|
+
```
|
|
950
|
+
|
|
951
|
+
If `RequestMeta` declares required fields, supply them in `defaults.meta` or per-call `options.meta` — the merged shape must contain them at runtime.
|
|
952
|
+
|
|
953
|
+
---
|
|
954
|
+
|
|
858
955
|
## Custom Client Adapter (Axios)
|
|
859
956
|
|
|
860
957
|
```typescript
|
|
@@ -862,7 +959,9 @@ import type { ClientAdapter } from 'ts-procedures/client'
|
|
|
862
959
|
import axios from 'axios'
|
|
863
960
|
|
|
864
961
|
const axiosAdapter: ClientAdapter = {
|
|
865
|
-
async request({ url, method, headers, body, signal }) {
|
|
962
|
+
async request({ url, method, headers, body, signal, meta }) {
|
|
963
|
+
// meta is typed via RequestMeta augmentation — use it for tracing,
|
|
964
|
+
// priority routing, custom auth, etc.
|
|
866
965
|
const res = await axios({ url, method, headers, data: body, signal })
|
|
867
966
|
return {
|
|
868
967
|
status: res.status,
|
|
@@ -7,12 +7,28 @@ import { createClient, createFetchAdapter } from 'ts-procedures/client'
|
|
|
7
7
|
import { createApiBindings, Api } from './generated/api'
|
|
8
8
|
// With --service-name {{Name}}: import { create{{Name}}Bindings, {{Name}} } from './generated/api'
|
|
9
9
|
|
|
10
|
+
// --- Optional: type per-request meta end-to-end via declaration merging ---
|
|
11
|
+
// Self-contained clients augment './generated/_types' instead:
|
|
12
|
+
// declare module './generated/_types' { interface RequestMeta { traceId: string } }
|
|
13
|
+
declare module 'ts-procedures/client' {
|
|
14
|
+
interface RequestMeta {
|
|
15
|
+
// TODO: add your fields — typed in options.meta, ctx.request.meta, adapter req.meta
|
|
16
|
+
// traceId?: string
|
|
17
|
+
// priority?: 'high' | 'low'
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
10
21
|
// Create the typed client
|
|
11
22
|
export const {{name}}Client = createClient({
|
|
12
23
|
adapter: createFetchAdapter(),
|
|
13
24
|
basePath: 'http://localhost:3000', // TODO: configure base URL
|
|
14
25
|
scopes: createApiBindings,
|
|
15
26
|
// With --service-name {{Name}}: scopes: create{{Name}}Bindings,
|
|
27
|
+
defaults: {
|
|
28
|
+
// TODO: set client-wide defaults (overridden by per-call options)
|
|
29
|
+
// timeout: 30_000,
|
|
30
|
+
// headers: { 'X-Client-Version': '1.0.0' },
|
|
31
|
+
},
|
|
16
32
|
hooks: {
|
|
17
33
|
onBeforeRequest(ctx) {
|
|
18
34
|
// TODO: add auth headers, request IDs, etc.
|
|
@@ -42,12 +58,24 @@ export const {{name}}Client = createClient({
|
|
|
42
58
|
// }
|
|
43
59
|
// const result = await stream.result // Typed from server returnType schema
|
|
44
60
|
|
|
45
|
-
// --- Per-
|
|
46
|
-
// const user = await {{name}}Client.users.GetUser(
|
|
47
|
-
//
|
|
48
|
-
//
|
|
61
|
+
// --- Per-call options (timeout, signal, headers, basePath, meta, hooks) ---
|
|
62
|
+
// const user = await {{name}}Client.users.GetUser(
|
|
63
|
+
// { id: '123' },
|
|
64
|
+
// {
|
|
65
|
+
// timeout: 5000,
|
|
66
|
+
// headers: { 'X-Request-Id': crypto.randomUUID() },
|
|
67
|
+
// // meta is typed via RequestMeta augmentation above:
|
|
68
|
+
// // meta: { traceId: 'req-abc' },
|
|
69
|
+
// onAfterResponse(ctx) {
|
|
70
|
+
// console.log('Rate limit:', ctx.response.headers['x-rate-limit-remaining'])
|
|
71
|
+
// },
|
|
49
72
|
// },
|
|
50
|
-
//
|
|
73
|
+
// )
|
|
74
|
+
|
|
75
|
+
// --- Cancellation with AbortController ---
|
|
76
|
+
// const controller = new AbortController()
|
|
77
|
+
// const promise = {{name}}Client.users.GetUser({ id: '123' }, { signal: controller.signal })
|
|
78
|
+
// setTimeout(() => controller.abort(), 3000)
|
|
51
79
|
```
|
|
52
80
|
|
|
53
81
|
## Test — `{{Name}}.client.test.ts`
|
|
@@ -336,6 +336,7 @@ const client = createClient({
|
|
|
336
336
|
adapter: createFetchAdapter(),
|
|
337
337
|
basePath: 'http://localhost:3000',
|
|
338
338
|
scopes: createApiBindings,
|
|
339
|
+
defaults: { timeout: 30_000, headers: { 'X-Client-Version': '1.0.0' } },
|
|
339
340
|
hooks: {
|
|
340
341
|
onBeforeRequest(ctx) {
|
|
341
342
|
ctx.request.headers = { ...ctx.request.headers, Authorization: `Bearer ${getToken()}` }
|
|
@@ -350,20 +351,67 @@ const client = createClient({
|
|
|
350
351
|
// Fully typed — params and response inferred from server schemas; type aliases live under Api.<Scope>.<Route>.
|
|
351
352
|
const user = await client.users.GetUser({ pathParams: { id: '123' } })
|
|
352
353
|
|
|
353
|
-
// Per-call
|
|
354
|
-
await client.users.GetUser(
|
|
355
|
-
|
|
356
|
-
|
|
354
|
+
// Per-call options — timeout, signal, headers, basePath, and hooks all share one bag
|
|
355
|
+
await client.users.GetUser(
|
|
356
|
+
{ pathParams: { id: '123' } },
|
|
357
|
+
{
|
|
358
|
+
timeout: 5000,
|
|
359
|
+
headers: { 'X-Request-Id': crypto.randomUUID() },
|
|
360
|
+
onAfterResponse(ctx) {
|
|
361
|
+
console.log(ctx.response.headers['x-rate-limit-remaining'])
|
|
362
|
+
},
|
|
357
363
|
},
|
|
358
|
-
|
|
364
|
+
)
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### Per-Call Options & Defaults
|
|
368
|
+
|
|
369
|
+
```typescript
|
|
370
|
+
interface ProcedureCallDefaults {
|
|
371
|
+
signal?: AbortSignal // cancellation (combined with per-call via AbortSignal.any)
|
|
372
|
+
timeout?: number // ms — per-call timeout:0 disables inherited default
|
|
373
|
+
headers?: Record<string, string> // merged, per-call keys win
|
|
374
|
+
basePath?: string // per-call > default > config.basePath
|
|
375
|
+
meta?: RequestMeta // typed per-request metadata (declaration-mergeable)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
interface ProcedureCallOptions extends ProcedureCallDefaults, ClientHooks {}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
- Defaults are set via `config.defaults`; per-call options are passed as the 2nd argument to any generated callable.
|
|
382
|
+
- Header precedence (low → high): adapter config < `defaults.headers` < per-call `headers` < route-declared `schema.input.headers` < `onBeforeRequest` mutations.
|
|
383
|
+
|
|
384
|
+
### Typed RequestMeta (Declaration Merging)
|
|
385
|
+
|
|
386
|
+
Augment `RequestMeta` to type `meta` end-to-end (options, hooks, adapter):
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
// For code-generated self-contained clients
|
|
390
|
+
declare module './generated/_types' {
|
|
391
|
+
interface RequestMeta {
|
|
392
|
+
traceId: string
|
|
393
|
+
priority?: 'high' | 'low'
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Or for direct ts-procedures/client usage
|
|
398
|
+
declare module 'ts-procedures/client' {
|
|
399
|
+
interface RequestMeta { traceId: string }
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
await client.users.GetUser(
|
|
403
|
+
{ pathParams: { id: '1' } },
|
|
404
|
+
{ meta: { traceId: 'req-abc', priority: 'high' } }, // fully typed
|
|
405
|
+
)
|
|
359
406
|
```
|
|
360
407
|
|
|
361
408
|
### Hook Types
|
|
362
409
|
|
|
363
410
|
```typescript
|
|
364
411
|
interface ClientHooks {
|
|
365
|
-
onBeforeRequest?: (ctx:
|
|
366
|
-
onAfterResponse?: (ctx:
|
|
412
|
+
onBeforeRequest?: (ctx: BeforeRequestContext) => BeforeRequestContext | Promise<BeforeRequestContext>
|
|
413
|
+
onAfterResponse?: (ctx: AfterResponseContext) => void | Promise<void>
|
|
414
|
+
onError?: (ctx: ErrorContext) => void | Promise<void>
|
|
367
415
|
}
|
|
368
416
|
```
|
|
369
417
|
|
|
@@ -336,6 +336,7 @@ const client = createClient({
|
|
|
336
336
|
adapter: createFetchAdapter(),
|
|
337
337
|
basePath: 'http://localhost:3000',
|
|
338
338
|
scopes: createApiBindings,
|
|
339
|
+
defaults: { timeout: 30_000, headers: { 'X-Client-Version': '1.0.0' } },
|
|
339
340
|
hooks: {
|
|
340
341
|
onBeforeRequest(ctx) {
|
|
341
342
|
ctx.request.headers = { ...ctx.request.headers, Authorization: `Bearer ${getToken()}` }
|
|
@@ -350,20 +351,67 @@ const client = createClient({
|
|
|
350
351
|
// Fully typed — params and response inferred from server schemas; type aliases live under Api.<Scope>.<Route>.
|
|
351
352
|
const user = await client.users.GetUser({ pathParams: { id: '123' } })
|
|
352
353
|
|
|
353
|
-
// Per-call
|
|
354
|
-
await client.users.GetUser(
|
|
355
|
-
|
|
356
|
-
|
|
354
|
+
// Per-call options — timeout, signal, headers, basePath, and hooks all share one bag
|
|
355
|
+
await client.users.GetUser(
|
|
356
|
+
{ pathParams: { id: '123' } },
|
|
357
|
+
{
|
|
358
|
+
timeout: 5000,
|
|
359
|
+
headers: { 'X-Request-Id': crypto.randomUUID() },
|
|
360
|
+
onAfterResponse(ctx) {
|
|
361
|
+
console.log(ctx.response.headers['x-rate-limit-remaining'])
|
|
362
|
+
},
|
|
357
363
|
},
|
|
358
|
-
|
|
364
|
+
)
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### Per-Call Options & Defaults
|
|
368
|
+
|
|
369
|
+
```typescript
|
|
370
|
+
interface ProcedureCallDefaults {
|
|
371
|
+
signal?: AbortSignal // cancellation (combined with per-call via AbortSignal.any)
|
|
372
|
+
timeout?: number // ms — per-call timeout:0 disables inherited default
|
|
373
|
+
headers?: Record<string, string> // merged, per-call keys win
|
|
374
|
+
basePath?: string // per-call > default > config.basePath
|
|
375
|
+
meta?: RequestMeta // typed per-request metadata (declaration-mergeable)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
interface ProcedureCallOptions extends ProcedureCallDefaults, ClientHooks {}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
- Defaults are set via `config.defaults`; per-call options are passed as the 2nd argument to any generated callable.
|
|
382
|
+
- Header precedence (low → high): adapter config < `defaults.headers` < per-call `headers` < route-declared `schema.input.headers` < `onBeforeRequest` mutations.
|
|
383
|
+
|
|
384
|
+
### Typed RequestMeta (Declaration Merging)
|
|
385
|
+
|
|
386
|
+
Augment `RequestMeta` to type `meta` end-to-end (options, hooks, adapter):
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
// For code-generated self-contained clients
|
|
390
|
+
declare module './generated/_types' {
|
|
391
|
+
interface RequestMeta {
|
|
392
|
+
traceId: string
|
|
393
|
+
priority?: 'high' | 'low'
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Or for direct ts-procedures/client usage
|
|
398
|
+
declare module 'ts-procedures/client' {
|
|
399
|
+
interface RequestMeta { traceId: string }
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
await client.users.GetUser(
|
|
403
|
+
{ pathParams: { id: '1' } },
|
|
404
|
+
{ meta: { traceId: 'req-abc', priority: 'high' } }, // fully typed
|
|
405
|
+
)
|
|
359
406
|
```
|
|
360
407
|
|
|
361
408
|
### Hook Types
|
|
362
409
|
|
|
363
410
|
```typescript
|
|
364
411
|
interface ClientHooks {
|
|
365
|
-
onBeforeRequest?: (ctx:
|
|
366
|
-
onAfterResponse?: (ctx:
|
|
412
|
+
onBeforeRequest?: (ctx: BeforeRequestContext) => BeforeRequestContext | Promise<BeforeRequestContext>
|
|
413
|
+
onAfterResponse?: (ctx: AfterResponseContext) => void | Promise<void>
|
|
414
|
+
onError?: (ctx: ErrorContext) => void | Promise<void>
|
|
367
415
|
}
|
|
368
416
|
```
|
|
369
417
|
|
package/build/client/call.d.ts
CHANGED
|
@@ -1,14 +1,23 @@
|
|
|
1
|
-
import type { ClientAdapter, ClientHooks, CallDescriptor } from './types.js';
|
|
1
|
+
import type { ClientAdapter, ClientHooks, CallDescriptor, ProcedureCallDefaults, ProcedureCallOptions } from './types.js';
|
|
2
|
+
export interface ExecuteCallConfig {
|
|
3
|
+
descriptor: CallDescriptor;
|
|
4
|
+
basePath: string;
|
|
5
|
+
adapter: ClientAdapter;
|
|
6
|
+
hooks: ClientHooks;
|
|
7
|
+
defaults?: ProcedureCallDefaults;
|
|
8
|
+
options?: ProcedureCallOptions;
|
|
9
|
+
}
|
|
2
10
|
/**
|
|
3
11
|
* Executes a single procedure call through the adapter.
|
|
4
12
|
*
|
|
5
13
|
* Flow:
|
|
6
|
-
* 1.
|
|
7
|
-
* 2.
|
|
8
|
-
* 3.
|
|
9
|
-
* 4.
|
|
10
|
-
* 5.
|
|
11
|
-
* 6.
|
|
12
|
-
* 7.
|
|
14
|
+
* 1. Resolve base path (per-call > defaults > config) and build AdapterRequest
|
|
15
|
+
* 2. Apply request options (headers, signal, timeout, meta) from defaults + per-call
|
|
16
|
+
* 3. Run onBeforeRequest hooks (global then local) — may further mutate request
|
|
17
|
+
* 4. Call adapter.request()
|
|
18
|
+
* 5. On adapter error: run onError hooks, re-throw
|
|
19
|
+
* 6. Run onAfterResponse hooks (may mutate response.status to swallow errors)
|
|
20
|
+
* 7. If response status is non-2xx: throw ClientRequestError
|
|
21
|
+
* 8. Return response.body as TResponse
|
|
13
22
|
*/
|
|
14
|
-
export declare function executeCall<TResponse>(
|
|
23
|
+
export declare function executeCall<TResponse>(config: ExecuteCallConfig): Promise<TResponse>;
|
package/build/client/call.js
CHANGED
|
@@ -1,37 +1,43 @@
|
|
|
1
1
|
import { buildAdapterRequest } from './request-builder.js';
|
|
2
2
|
import { runBeforeRequest, runAfterResponse, runOnError } from './hooks.js';
|
|
3
|
+
import { applyRequestOptions, resolveBasePath } from './resolve-options.js';
|
|
3
4
|
import { ClientRequestError } from './errors.js';
|
|
4
5
|
/**
|
|
5
6
|
* Executes a single procedure call through the adapter.
|
|
6
7
|
*
|
|
7
8
|
* Flow:
|
|
8
|
-
* 1.
|
|
9
|
-
* 2.
|
|
10
|
-
* 3.
|
|
11
|
-
* 4.
|
|
12
|
-
* 5.
|
|
13
|
-
* 6.
|
|
14
|
-
* 7.
|
|
9
|
+
* 1. Resolve base path (per-call > defaults > config) and build AdapterRequest
|
|
10
|
+
* 2. Apply request options (headers, signal, timeout, meta) from defaults + per-call
|
|
11
|
+
* 3. Run onBeforeRequest hooks (global then local) — may further mutate request
|
|
12
|
+
* 4. Call adapter.request()
|
|
13
|
+
* 5. On adapter error: run onError hooks, re-throw
|
|
14
|
+
* 6. Run onAfterResponse hooks (may mutate response.status to swallow errors)
|
|
15
|
+
* 7. If response status is non-2xx: throw ClientRequestError
|
|
16
|
+
* 8. Return response.body as TResponse
|
|
15
17
|
*/
|
|
16
|
-
export async function executeCall(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
export async function executeCall(config) {
|
|
19
|
+
const { descriptor, basePath, adapter, hooks, defaults, options } = config;
|
|
20
|
+
// 1. Build the initial request (path/query/body from descriptor)
|
|
21
|
+
const resolvedBasePath = resolveBasePath(defaults, options, basePath);
|
|
22
|
+
let request = buildAdapterRequest(descriptor, resolvedBasePath);
|
|
23
|
+
// 2. Apply request-level options (headers, signal, timeout, meta)
|
|
24
|
+
request = applyRequestOptions(request, defaults, options);
|
|
25
|
+
// 3. Run before-request hooks — they may further mutate the request
|
|
26
|
+
const beforeCtx = await runBeforeRequest({ procedureName: descriptor.name, scope: descriptor.scope, request }, hooks, options);
|
|
21
27
|
request = beforeCtx.request;
|
|
22
|
-
//
|
|
28
|
+
// 4. Call the adapter
|
|
23
29
|
let response;
|
|
24
30
|
try {
|
|
25
31
|
response = await adapter.request(request);
|
|
26
32
|
}
|
|
27
33
|
catch (err) {
|
|
28
|
-
//
|
|
29
|
-
await runOnError({ procedureName: descriptor.name, scope: descriptor.scope, request, error: err },
|
|
34
|
+
// 5. On adapter error: run error hooks, re-throw
|
|
35
|
+
await runOnError({ procedureName: descriptor.name, scope: descriptor.scope, request, error: err }, hooks, options);
|
|
30
36
|
throw err;
|
|
31
37
|
}
|
|
32
|
-
//
|
|
33
|
-
await runAfterResponse({ procedureName: descriptor.name, scope: descriptor.scope, request, response },
|
|
34
|
-
//
|
|
38
|
+
// 6. Run after-response hooks — they may mutate response.status to swallow errors
|
|
39
|
+
await runAfterResponse({ procedureName: descriptor.name, scope: descriptor.scope, request, response }, hooks, options);
|
|
40
|
+
// 7. Check status AFTER hooks (hooks may have swallowed the error by mutating status)
|
|
35
41
|
if (response.status < 200 || response.status >= 300) {
|
|
36
42
|
throw new ClientRequestError({
|
|
37
43
|
status: response.status,
|
|
@@ -41,7 +47,7 @@ export async function executeCall(descriptor, basePath, adapter, globalHooks, lo
|
|
|
41
47
|
scope: descriptor.scope,
|
|
42
48
|
});
|
|
43
49
|
}
|
|
44
|
-
//
|
|
50
|
+
// 8. Return the body
|
|
45
51
|
return response.body;
|
|
46
52
|
}
|
|
47
53
|
//# sourceMappingURL=call.js.map
|
package/build/client/call.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"call.js","sourceRoot":"","sources":["../../src/client/call.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAA;AAC1D,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAC3E,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"call.js","sourceRoot":"","sources":["../../src/client/call.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAA;AAC1D,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAC3E,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAC3E,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAkBhD;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAY,MAAyB;IACpE,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAAA;IAE1E,iEAAiE;IACjE,MAAM,gBAAgB,GAAG,eAAe,CAAC,QAAQ,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAA;IACrE,IAAI,OAAO,GAAG,mBAAmB,CAAC,UAAU,EAAE,gBAAgB,CAAC,CAAA;IAE/D,kEAAkE;IAClE,OAAO,GAAG,mBAAmB,CAAC,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAA;IAEzD,oEAAoE;IACpE,MAAM,SAAS,GAAG,MAAM,gBAAgB,CACtC,EAAE,aAAa,EAAE,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,UAAU,CAAC,KAAK,EAAE,OAAO,EAAE,EACpE,KAAK,EACL,OAAO,CACR,CAAA;IACD,OAAO,GAAG,SAAS,CAAC,OAAO,CAAA;IAE3B,sBAAsB;IACtB,IAAI,QAAQ,CAAA;IACZ,IAAI,CAAC;QACH,QAAQ,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;IAC3C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,iDAAiD;QACjD,MAAM,UAAU,CACd,EAAE,aAAa,EAAE,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,UAAU,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,EAChF,KAAK,EACL,OAAO,CACR,CAAA;QACD,MAAM,GAAG,CAAA;IACX,CAAC;IAED,kFAAkF;IAClF,MAAM,gBAAgB,CACpB,EAAE,aAAa,EAAE,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,UAAU,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,EAC9E,KAAK,EACL,OAAO,CACR,CAAA;IAED,sFAAsF;IACtF,IAAI,QAAQ,CAAC,MAAM,GAAG,GAAG,IAAI,QAAQ,CAAC,MAAM,IAAI,GAAG,EAAE,CAAC;QACpD,MAAM,IAAI,kBAAkB,CAAC;YAC3B,MAAM,EAAE,QAAQ,CAAC,MAAM;YACvB,OAAO,EAAE,QAAQ,CAAC,OAAO;YACzB,IAAI,EAAE,QAAQ,CAAC,IAAI;YACnB,aAAa,EAAE,UAAU,CAAC,IAAI;YAC9B,KAAK,EAAE,UAAU,CAAC,KAAK;SACxB,CAAC,CAAA;IACJ,CAAC;IAED,qBAAqB;IACrB,OAAO,QAAQ,CAAC,IAAiB,CAAA;AACnC,CAAC"}
|