ts-procedures 5.15.0 → 6.0.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 +2 -0
- package/agent_config/claude-code/agents/ts-procedures-architect.md +13 -6
- package/agent_config/claude-code/skills/ts-procedures/SKILL.md +26 -4
- package/agent_config/claude-code/skills/ts-procedures/anti-patterns.md +85 -17
- package/agent_config/claude-code/skills/ts-procedures/api-reference.md +220 -9
- package/agent_config/claude-code/skills/ts-procedures/patterns.md +271 -16
- package/agent_config/claude-code/skills/ts-procedures-review/SKILL.md +1 -1
- package/agent_config/claude-code/skills/ts-procedures-review/checklist.md +20 -12
- package/agent_config/claude-code/skills/ts-procedures-scaffold/SKILL.md +2 -1
- package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/client.md +53 -18
- package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/express-rpc.md +20 -17
- package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-api.md +20 -16
- package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-rpc.md +20 -17
- package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-stream.md +16 -3
- package/agent_config/copilot/copilot-instructions.md +132 -19
- package/agent_config/cursor/cursorrules +132 -19
- package/build/client/call.d.ts +19 -9
- package/build/client/call.js +33 -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/error-dispatch.d.ts +13 -0
- package/build/client/error-dispatch.js +26 -0
- package/build/client/error-dispatch.js.map +1 -0
- package/build/client/error-dispatch.test.d.ts +1 -0
- package/build/client/error-dispatch.test.js +56 -0
- package/build/client/error-dispatch.test.js.map +1 -0
- package/build/client/fetch-adapter.js +10 -4
- package/build/client/fetch-adapter.js.map +1 -1
- package/build/client/index.d.ts +2 -1
- package/build/client/index.js +22 -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 +19 -9
- package/build/client/stream.js +36 -21
- 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/typed-error-dispatch.test.d.ts +1 -0
- package/build/client/typed-error-dispatch.test.js +168 -0
- package/build/client/typed-error-dispatch.test.js.map +1 -0
- package/build/client/types.d.ts +105 -1
- package/build/client/types.js +1 -1
- package/build/codegen/e2e.test.js +150 -4
- package/build/codegen/e2e.test.js.map +1 -1
- package/build/codegen/emit-client-runtime.js +7 -0
- package/build/codegen/emit-client-runtime.js.map +1 -1
- package/build/codegen/emit-errors.d.ts +17 -6
- package/build/codegen/emit-errors.integration.test.d.ts +1 -0
- package/build/codegen/emit-errors.integration.test.js +162 -0
- package/build/codegen/emit-errors.integration.test.js.map +1 -0
- package/build/codegen/emit-errors.js +50 -39
- package/build/codegen/emit-errors.js.map +1 -1
- package/build/codegen/emit-errors.test.js +75 -78
- package/build/codegen/emit-errors.test.js.map +1 -1
- package/build/codegen/emit-index.d.ts +7 -0
- package/build/codegen/emit-index.js +26 -4
- package/build/codegen/emit-index.js.map +1 -1
- package/build/codegen/emit-index.test.js +55 -23
- package/build/codegen/emit-index.test.js.map +1 -1
- package/build/codegen/emit-scope.d.ts +8 -0
- package/build/codegen/emit-scope.js +82 -7
- package/build/codegen/emit-scope.js.map +1 -1
- package/build/codegen/pipeline.js +22 -2
- package/build/codegen/pipeline.js.map +1 -1
- package/build/implementations/http/doc-registry.d.ts +21 -0
- package/build/implementations/http/doc-registry.js +51 -78
- package/build/implementations/http/doc-registry.js.map +1 -1
- package/build/implementations/http/doc-registry.test.js +8 -6
- package/build/implementations/http/doc-registry.test.js.map +1 -1
- package/build/implementations/http/error-taxonomy.d.ts +240 -0
- package/build/implementations/http/error-taxonomy.js +230 -0
- package/build/implementations/http/error-taxonomy.js.map +1 -0
- package/build/implementations/http/error-taxonomy.test.d.ts +1 -0
- package/build/implementations/http/error-taxonomy.test.js +399 -0
- package/build/implementations/http/error-taxonomy.test.js.map +1 -0
- package/build/implementations/http/express-rpc/error-taxonomy.test.d.ts +1 -0
- package/build/implementations/http/express-rpc/error-taxonomy.test.js +83 -0
- package/build/implementations/http/express-rpc/error-taxonomy.test.js.map +1 -0
- package/build/implementations/http/express-rpc/index.d.ts +39 -8
- package/build/implementations/http/express-rpc/index.js +39 -8
- package/build/implementations/http/express-rpc/index.js.map +1 -1
- package/build/implementations/http/hono-api/error-taxonomy.test.d.ts +1 -0
- package/build/implementations/http/hono-api/error-taxonomy.test.js +137 -0
- package/build/implementations/http/hono-api/error-taxonomy.test.js.map +1 -0
- package/build/implementations/http/hono-api/index.d.ts +38 -1
- package/build/implementations/http/hono-api/index.js +32 -0
- package/build/implementations/http/hono-api/index.js.map +1 -1
- package/build/implementations/http/hono-rpc/error-taxonomy.test.d.ts +1 -0
- package/build/implementations/http/hono-rpc/error-taxonomy.test.js +64 -0
- package/build/implementations/http/hono-rpc/error-taxonomy.test.js.map +1 -0
- package/build/implementations/http/hono-rpc/index.d.ts +34 -7
- package/build/implementations/http/hono-rpc/index.js +31 -4
- package/build/implementations/http/hono-rpc/index.js.map +1 -1
- package/build/implementations/http/hono-stream/error-taxonomy.test.d.ts +1 -0
- package/build/implementations/http/hono-stream/error-taxonomy.test.js +87 -0
- package/build/implementations/http/hono-stream/error-taxonomy.test.js.map +1 -0
- package/build/implementations/http/hono-stream/index.d.ts +40 -3
- package/build/implementations/http/hono-stream/index.js +37 -10
- package/build/implementations/http/hono-stream/index.js.map +1 -1
- package/build/implementations/http/hono-stream/index.test.js +45 -18
- package/build/implementations/http/hono-stream/index.test.js.map +1 -1
- package/build/implementations/http/on-request-error.test.d.ts +1 -0
- package/build/implementations/http/on-request-error.test.js +173 -0
- package/build/implementations/http/on-request-error.test.js.map +1 -0
- package/build/implementations/http/route-errors.test.d.ts +1 -0
- package/build/implementations/http/route-errors.test.js +140 -0
- package/build/implementations/http/route-errors.test.js.map +1 -0
- package/build/implementations/types.d.ts +30 -2
- package/docs/client-and-codegen.md +228 -14
- package/docs/core.md +14 -5
- package/docs/http-integrations.md +135 -4
- package/docs/streaming.md +3 -1
- package/package.json +7 -2
- package/src/client/call.test.ts +202 -29
- package/src/client/call.ts +50 -28
- package/src/client/error-dispatch.test.ts +72 -0
- package/src/client/error-dispatch.ts +27 -0
- package/src/client/fetch-adapter.ts +11 -5
- package/src/client/index.test.ts +117 -0
- package/src/client/index.ts +34 -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 +53 -27
- package/src/client/typed-error-dispatch.test.ts +211 -0
- package/src/client/types.ts +116 -2
- package/src/codegen/e2e.test.ts +160 -4
- package/src/codegen/emit-client-runtime.ts +7 -0
- package/src/codegen/emit-errors.integration.test.ts +183 -0
- package/src/codegen/emit-errors.test.ts +91 -87
- package/src/codegen/emit-errors.ts +123 -41
- package/src/codegen/emit-index.test.ts +68 -24
- package/src/codegen/emit-index.ts +66 -4
- package/src/codegen/emit-scope.ts +124 -7
- package/src/codegen/pipeline.ts +25 -2
- package/src/implementations/http/README.md +28 -5
- package/src/implementations/http/doc-registry.test.ts +10 -6
- package/src/implementations/http/doc-registry.ts +63 -80
- package/src/implementations/http/error-taxonomy.test.ts +438 -0
- package/src/implementations/http/error-taxonomy.ts +337 -0
- package/src/implementations/http/express-rpc/README.md +21 -22
- package/src/implementations/http/express-rpc/error-taxonomy.test.ts +103 -0
- package/src/implementations/http/express-rpc/index.ts +75 -14
- package/src/implementations/http/hono-api/README.md +284 -0
- package/src/implementations/http/hono-api/error-taxonomy.test.ts +179 -0
- package/src/implementations/http/hono-api/index.ts +76 -1
- package/src/implementations/http/hono-rpc/README.md +18 -19
- package/src/implementations/http/hono-rpc/error-taxonomy.test.ts +82 -0
- package/src/implementations/http/hono-rpc/index.ts +65 -9
- package/src/implementations/http/hono-stream/README.md +44 -25
- package/src/implementations/http/hono-stream/error-taxonomy.test.ts +98 -0
- package/src/implementations/http/hono-stream/index.test.ts +54 -18
- package/src/implementations/http/hono-stream/index.ts +83 -13
- package/src/implementations/http/on-request-error.test.ts +201 -0
- package/src/implementations/http/route-errors.test.ts +177 -0
- package/src/implementations/types.ts +30 -2
|
@@ -7,6 +7,8 @@ import type {
|
|
|
7
7
|
AdapterStreamResponse,
|
|
8
8
|
ClientHooks,
|
|
9
9
|
StreamDescriptor,
|
|
10
|
+
ProcedureCallDefaults,
|
|
11
|
+
ProcedureCallOptions,
|
|
10
12
|
} from './types.js'
|
|
11
13
|
|
|
12
14
|
// ── helpers ───────────────────────────────────────────────
|
|
@@ -47,6 +49,26 @@ function makeStreamAdapter(
|
|
|
47
49
|
}
|
|
48
50
|
}
|
|
49
51
|
|
|
52
|
+
interface RunConfig {
|
|
53
|
+
adapter: ClientAdapter
|
|
54
|
+
hooks?: ClientHooks
|
|
55
|
+
defaults?: ProcedureCallDefaults
|
|
56
|
+
options?: ProcedureCallOptions
|
|
57
|
+
descriptor?: StreamDescriptor
|
|
58
|
+
basePath?: string
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function run<TYield, TReturn = void>({
|
|
62
|
+
adapter,
|
|
63
|
+
hooks = {},
|
|
64
|
+
defaults,
|
|
65
|
+
options,
|
|
66
|
+
descriptor = makeDescriptor(),
|
|
67
|
+
basePath = 'https://api.example.com',
|
|
68
|
+
}: RunConfig) {
|
|
69
|
+
return executeStream<TYield, TReturn>({ descriptor, basePath, adapter, hooks, defaults, options })
|
|
70
|
+
}
|
|
71
|
+
|
|
50
72
|
// ── createTypedStream — SSE mode ──────────────────────────
|
|
51
73
|
|
|
52
74
|
describe('createTypedStream — SSE mode', () => {
|
|
@@ -55,15 +77,10 @@ describe('createTypedStream — SSE mode', () => {
|
|
|
55
77
|
{ data: { count: 1 }, event: 'update' },
|
|
56
78
|
{ data: { count: 2 }, event: 'update' },
|
|
57
79
|
]
|
|
58
|
-
const stream = createTypedStream<{ count: number }, void>(
|
|
59
|
-
makeAsyncIterable(sseItems),
|
|
60
|
-
'sse'
|
|
61
|
-
)
|
|
80
|
+
const stream = createTypedStream<{ count: number }, void>(makeAsyncIterable(sseItems), 'sse')
|
|
62
81
|
|
|
63
82
|
const received: { count: number }[] = []
|
|
64
|
-
for await (const item of stream)
|
|
65
|
-
received.push(item)
|
|
66
|
-
}
|
|
83
|
+
for await (const item of stream) received.push(item)
|
|
67
84
|
expect(received).toEqual([{ count: 1 }, { count: 2 }])
|
|
68
85
|
})
|
|
69
86
|
|
|
@@ -74,34 +91,23 @@ describe('createTypedStream — SSE mode', () => {
|
|
|
74
91
|
]
|
|
75
92
|
const stream = createTypedStream<{ count: number }, { total: number }>(
|
|
76
93
|
makeAsyncIterable(sseItems),
|
|
77
|
-
'sse'
|
|
94
|
+
'sse',
|
|
78
95
|
)
|
|
79
96
|
|
|
80
97
|
const yielded: { count: number }[] = []
|
|
81
|
-
for await (const item of stream)
|
|
82
|
-
yielded.push(item)
|
|
83
|
-
}
|
|
98
|
+
for await (const item of stream) yielded.push(item)
|
|
84
99
|
|
|
85
|
-
// Only the 'update' event is yielded
|
|
86
100
|
expect(yielded).toEqual([{ count: 1 }])
|
|
87
|
-
|
|
88
|
-
// The 'return' event resolves .result
|
|
89
|
-
const result = await stream.result
|
|
90
|
-
expect(result).toEqual({ total: 99 })
|
|
101
|
+
await expect(stream.result).resolves.toEqual({ total: 99 })
|
|
91
102
|
})
|
|
92
103
|
|
|
93
104
|
it('resolves .result with undefined when no return event', async () => {
|
|
94
|
-
const sseItems = [
|
|
95
|
-
{ data: 'hello', event: 'message' },
|
|
96
|
-
]
|
|
97
105
|
const stream = createTypedStream<string, undefined>(
|
|
98
|
-
makeAsyncIterable(
|
|
99
|
-
'sse'
|
|
106
|
+
makeAsyncIterable([{ data: 'hello', event: 'message' }]),
|
|
107
|
+
'sse',
|
|
100
108
|
)
|
|
101
|
-
|
|
102
109
|
for await (const _ of stream) { /* drain */ }
|
|
103
|
-
|
|
104
|
-
expect(result).toBeUndefined()
|
|
110
|
+
await expect(stream.result).resolves.toBeUndefined()
|
|
105
111
|
})
|
|
106
112
|
|
|
107
113
|
it('rejects .result and re-throws on error', async () => {
|
|
@@ -111,30 +117,19 @@ describe('createTypedStream — SSE mode', () => {
|
|
|
111
117
|
}
|
|
112
118
|
|
|
113
119
|
const stream = createTypedStream<string, void>(errorIterable(), 'sse')
|
|
114
|
-
|
|
115
|
-
// Consuming the stream should throw
|
|
116
120
|
await expect(async () => {
|
|
117
121
|
for await (const _ of stream) { /* drain */ }
|
|
118
122
|
}).rejects.toThrow('stream broke')
|
|
119
|
-
|
|
120
|
-
// .result should also reject
|
|
121
123
|
await expect(stream.result).rejects.toThrow('stream broke')
|
|
122
124
|
})
|
|
123
125
|
|
|
124
126
|
it('handles SSE items without event field (defaults to yielding data)', async () => {
|
|
125
|
-
const sseItems = [
|
|
126
|
-
{ data: 'a' },
|
|
127
|
-
{ data: 'b' },
|
|
128
|
-
]
|
|
129
127
|
const stream = createTypedStream<string, void>(
|
|
130
|
-
makeAsyncIterable(
|
|
131
|
-
'sse'
|
|
128
|
+
makeAsyncIterable([{ data: 'a' }, { data: 'b' }]),
|
|
129
|
+
'sse',
|
|
132
130
|
)
|
|
133
|
-
|
|
134
131
|
const yielded: string[] = []
|
|
135
|
-
for await (const item of stream)
|
|
136
|
-
yielded.push(item)
|
|
137
|
-
}
|
|
132
|
+
for await (const item of stream) yielded.push(item)
|
|
138
133
|
expect(yielded).toEqual(['a', 'b'])
|
|
139
134
|
})
|
|
140
135
|
})
|
|
@@ -144,27 +139,16 @@ describe('createTypedStream — SSE mode', () => {
|
|
|
144
139
|
describe('createTypedStream — text mode', () => {
|
|
145
140
|
it('yields each chunk as-is', async () => {
|
|
146
141
|
const chunks = ['chunk1', 'chunk2', 'chunk3']
|
|
147
|
-
const stream = createTypedStream<string, void>(
|
|
148
|
-
makeAsyncIterable(chunks),
|
|
149
|
-
'text'
|
|
150
|
-
)
|
|
151
|
-
|
|
142
|
+
const stream = createTypedStream<string, void>(makeAsyncIterable(chunks), 'text')
|
|
152
143
|
const received: string[] = []
|
|
153
|
-
for await (const chunk of stream)
|
|
154
|
-
received.push(chunk)
|
|
155
|
-
}
|
|
144
|
+
for await (const chunk of stream) received.push(chunk)
|
|
156
145
|
expect(received).toEqual(chunks)
|
|
157
146
|
})
|
|
158
147
|
|
|
159
148
|
it('.result resolves to void on normal completion', async () => {
|
|
160
|
-
const stream = createTypedStream<string, void>(
|
|
161
|
-
makeAsyncIterable(['a', 'b']),
|
|
162
|
-
'text'
|
|
163
|
-
)
|
|
164
|
-
|
|
149
|
+
const stream = createTypedStream<string, void>(makeAsyncIterable(['a', 'b']), 'text')
|
|
165
150
|
for await (const _ of stream) { /* drain */ }
|
|
166
|
-
|
|
167
|
-
expect(result).toBeUndefined()
|
|
151
|
+
await expect(stream.result).resolves.toBeUndefined()
|
|
168
152
|
})
|
|
169
153
|
|
|
170
154
|
it('rejects .result and re-throws on error', async () => {
|
|
@@ -172,13 +156,10 @@ describe('createTypedStream — text mode', () => {
|
|
|
172
156
|
yield 'before error'
|
|
173
157
|
throw new Error('text stream error')
|
|
174
158
|
}
|
|
175
|
-
|
|
176
159
|
const stream = createTypedStream<string, void>(errorIterable(), 'text')
|
|
177
|
-
|
|
178
160
|
await expect(async () => {
|
|
179
161
|
for await (const _ of stream) { /* drain */ }
|
|
180
162
|
}).rejects.toThrow('text stream error')
|
|
181
|
-
|
|
182
163
|
await expect(stream.result).rejects.toThrow('text stream error')
|
|
183
164
|
})
|
|
184
165
|
})
|
|
@@ -189,20 +170,11 @@ describe('executeStream', () => {
|
|
|
189
170
|
it('calls adapter.stream and returns a TypedStream', async () => {
|
|
190
171
|
const items = [{ data: 'hello', event: 'msg' }]
|
|
191
172
|
const adapter = makeStreamAdapter({}, items)
|
|
192
|
-
|
|
193
|
-
const stream = await executeStream<string, void>(
|
|
194
|
-
makeDescriptor(),
|
|
195
|
-
'https://api.example.com',
|
|
196
|
-
adapter,
|
|
197
|
-
{},
|
|
198
|
-
undefined
|
|
199
|
-
)
|
|
173
|
+
const stream = await run<string, void>({ adapter })
|
|
200
174
|
|
|
201
175
|
expect(adapter.stream).toHaveBeenCalledOnce()
|
|
202
176
|
const received: string[] = []
|
|
203
|
-
for await (const item of stream)
|
|
204
|
-
received.push(item)
|
|
205
|
-
}
|
|
177
|
+
for await (const item of stream) received.push(item)
|
|
206
178
|
expect(received).toEqual(['hello'])
|
|
207
179
|
})
|
|
208
180
|
|
|
@@ -216,23 +188,15 @@ describe('executeStream', () => {
|
|
|
216
188
|
}),
|
|
217
189
|
}
|
|
218
190
|
|
|
219
|
-
const
|
|
191
|
+
const hooks: ClientHooks = {
|
|
220
192
|
onBeforeRequest: (ctx) => ({
|
|
221
193
|
...ctx,
|
|
222
194
|
request: { ...ctx.request, headers: { 'x-stream-auth': 'stream-token' } },
|
|
223
195
|
}),
|
|
224
196
|
}
|
|
225
197
|
|
|
226
|
-
const stream = await
|
|
227
|
-
|
|
228
|
-
'https://api.example.com',
|
|
229
|
-
adapter,
|
|
230
|
-
globalHooks,
|
|
231
|
-
undefined
|
|
232
|
-
)
|
|
233
|
-
// Drain
|
|
234
|
-
for await (const _ of stream) { /* noop */ }
|
|
235
|
-
|
|
198
|
+
const stream = await run({ adapter, hooks })
|
|
199
|
+
for await (const _ of stream) { /* drain */ }
|
|
236
200
|
expect(capturedHeaders[0]?.['x-stream-auth']).toBe('stream-token')
|
|
237
201
|
})
|
|
238
202
|
|
|
@@ -245,12 +209,11 @@ describe('executeStream', () => {
|
|
|
245
209
|
return { status: 200, headers: {}, body: makeAsyncIterable([]) }
|
|
246
210
|
}),
|
|
247
211
|
}
|
|
248
|
-
const
|
|
212
|
+
const hooks: ClientHooks = {
|
|
249
213
|
onAfterResponse: () => { order.push('afterResponse') },
|
|
250
214
|
}
|
|
251
215
|
|
|
252
|
-
await
|
|
253
|
-
// After executeStream returns (before iteration), afterResponse should have fired
|
|
216
|
+
await run({ adapter, hooks })
|
|
254
217
|
expect(order).toEqual(['adapter', 'afterResponse'])
|
|
255
218
|
})
|
|
256
219
|
|
|
@@ -263,10 +226,7 @@ describe('executeStream', () => {
|
|
|
263
226
|
body: makeAsyncIterable([]),
|
|
264
227
|
})),
|
|
265
228
|
}
|
|
266
|
-
|
|
267
|
-
await expect(
|
|
268
|
-
executeStream(makeDescriptor(), 'https://api.example.com', adapter, {}, undefined)
|
|
269
|
-
).rejects.toThrow(ClientRequestError)
|
|
229
|
+
await expect(run({ adapter })).rejects.toThrow(ClientRequestError)
|
|
270
230
|
})
|
|
271
231
|
|
|
272
232
|
it('runs onError on adapter failure and re-throws', async () => {
|
|
@@ -276,13 +236,11 @@ describe('executeStream', () => {
|
|
|
276
236
|
stream: vi.fn(async () => { throw adapterError }),
|
|
277
237
|
}
|
|
278
238
|
const receivedErrors: unknown[] = []
|
|
279
|
-
const
|
|
239
|
+
const hooks: ClientHooks = {
|
|
280
240
|
onError: (ctx) => { receivedErrors.push(ctx.error) },
|
|
281
241
|
}
|
|
282
242
|
|
|
283
|
-
await expect(
|
|
284
|
-
executeStream(makeDescriptor(), 'https://api.example.com', adapter, globalHooks, undefined)
|
|
285
|
-
).rejects.toThrow('stream connection failed')
|
|
243
|
+
await expect(run({ adapter, hooks })).rejects.toThrow('stream connection failed')
|
|
286
244
|
expect(receivedErrors[0]).toBe(adapterError)
|
|
287
245
|
})
|
|
288
246
|
|
|
@@ -293,18 +251,13 @@ describe('executeStream', () => {
|
|
|
293
251
|
]
|
|
294
252
|
const adapter = makeStreamAdapter({}, sseItems)
|
|
295
253
|
|
|
296
|
-
const stream = await
|
|
297
|
-
makeDescriptor({ streamMode: 'sse' }),
|
|
298
|
-
'https://api.example.com',
|
|
254
|
+
const stream = await run<{ n: number }, { final: boolean }>({
|
|
299
255
|
adapter,
|
|
300
|
-
{},
|
|
301
|
-
|
|
302
|
-
)
|
|
256
|
+
descriptor: makeDescriptor({ streamMode: 'sse' }),
|
|
257
|
+
})
|
|
303
258
|
|
|
304
259
|
const yielded: { n: number }[] = []
|
|
305
|
-
for await (const item of stream)
|
|
306
|
-
yielded.push(item)
|
|
307
|
-
}
|
|
260
|
+
for await (const item of stream) yielded.push(item)
|
|
308
261
|
expect(yielded).toEqual([{ n: 1 }])
|
|
309
262
|
await expect(stream.result).resolves.toEqual({ final: true })
|
|
310
263
|
})
|
|
@@ -313,19 +266,91 @@ describe('executeStream', () => {
|
|
|
313
266
|
const chunks = ['line1', 'line2']
|
|
314
267
|
const adapter = makeStreamAdapter({}, chunks)
|
|
315
268
|
|
|
316
|
-
const stream = await
|
|
317
|
-
makeDescriptor({ streamMode: 'text' }),
|
|
318
|
-
'https://api.example.com',
|
|
269
|
+
const stream = await run<string, void>({
|
|
319
270
|
adapter,
|
|
320
|
-
{},
|
|
321
|
-
|
|
322
|
-
)
|
|
271
|
+
descriptor: makeDescriptor({ streamMode: 'text' }),
|
|
272
|
+
})
|
|
323
273
|
|
|
324
274
|
const received: string[] = []
|
|
325
|
-
for await (const chunk of stream)
|
|
326
|
-
received.push(chunk)
|
|
327
|
-
}
|
|
275
|
+
for await (const chunk of stream) received.push(chunk)
|
|
328
276
|
expect(received).toEqual(chunks)
|
|
329
277
|
await expect(stream.result).resolves.toBeUndefined()
|
|
330
278
|
})
|
|
279
|
+
|
|
280
|
+
// ── Per-call options ──
|
|
281
|
+
|
|
282
|
+
it('per-call timeout attaches a signal via AbortSignal.timeout', async () => {
|
|
283
|
+
const spy = vi.spyOn(AbortSignal, 'timeout')
|
|
284
|
+
try {
|
|
285
|
+
let observedSignal: AbortSignal | undefined
|
|
286
|
+
const adapter: ClientAdapter = {
|
|
287
|
+
request: vi.fn(async (): Promise<never> => { throw new Error('not expected') }),
|
|
288
|
+
stream: vi.fn(async (req: AdapterRequest): Promise<AdapterStreamResponse> => {
|
|
289
|
+
observedSignal = req.signal
|
|
290
|
+
return { status: 200, headers: {}, body: makeAsyncIterable([]) }
|
|
291
|
+
}),
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
await run({ adapter, options: { timeout: 5000 } })
|
|
295
|
+
expect(spy).toHaveBeenCalledWith(5000)
|
|
296
|
+
expect(observedSignal).toBeDefined()
|
|
297
|
+
} finally {
|
|
298
|
+
spy.mockRestore()
|
|
299
|
+
}
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
it('adapter receives a signal that reflects abort when the caller cancels', async () => {
|
|
303
|
+
const controller = new AbortController()
|
|
304
|
+
const adapter: ClientAdapter = {
|
|
305
|
+
request: vi.fn(async (): Promise<never> => { throw new Error('not expected') }),
|
|
306
|
+
stream: vi.fn(async (req: AdapterRequest): Promise<AdapterStreamResponse> => {
|
|
307
|
+
return new Promise((_resolve, reject) => {
|
|
308
|
+
const abort = () => reject(new Error('aborted'))
|
|
309
|
+
if (req.signal?.aborted) abort()
|
|
310
|
+
else req.signal?.addEventListener('abort', abort, { once: true })
|
|
311
|
+
})
|
|
312
|
+
}),
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const promise = run({ adapter, options: { signal: controller.signal } })
|
|
316
|
+
// Let executeStream reach the adapter before aborting
|
|
317
|
+
await Promise.resolve()
|
|
318
|
+
controller.abort()
|
|
319
|
+
await expect(promise).rejects.toThrow('aborted')
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
it('per-call basePath overrides base path for streams', async () => {
|
|
323
|
+
const capturedUrls: string[] = []
|
|
324
|
+
const adapter: ClientAdapter = {
|
|
325
|
+
request: vi.fn(async (): Promise<never> => { throw new Error('not expected') }),
|
|
326
|
+
stream: vi.fn(async (req: AdapterRequest): Promise<AdapterStreamResponse> => {
|
|
327
|
+
capturedUrls.push(req.url)
|
|
328
|
+
return { status: 200, headers: {}, body: makeAsyncIterable([]) }
|
|
329
|
+
}),
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const stream = await run({
|
|
333
|
+
adapter,
|
|
334
|
+
descriptor: makeDescriptor({ path: '/tail' }),
|
|
335
|
+
basePath: 'https://default.example.com',
|
|
336
|
+
options: { basePath: 'https://override.example.com' },
|
|
337
|
+
})
|
|
338
|
+
for await (const _ of stream) { /* drain */ }
|
|
339
|
+
|
|
340
|
+
expect(capturedUrls[0]).toBe('https://override.example.com/tail')
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
it('per-call meta is forwarded to the adapter', async () => {
|
|
344
|
+
let observedMeta: unknown
|
|
345
|
+
const adapter: ClientAdapter = {
|
|
346
|
+
request: vi.fn(async (): Promise<never> => { throw new Error('not expected') }),
|
|
347
|
+
stream: vi.fn(async (req: AdapterRequest): Promise<AdapterStreamResponse> => {
|
|
348
|
+
observedMeta = req.meta
|
|
349
|
+
return { status: 200, headers: {}, body: makeAsyncIterable([]) }
|
|
350
|
+
}),
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
await run({ adapter, options: { meta: { traceId: 'stream-trace' } as never } })
|
|
354
|
+
expect(observedMeta).toEqual({ traceId: 'stream-trace' })
|
|
355
|
+
})
|
|
331
356
|
})
|
package/src/client/stream.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
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'
|
|
5
|
+
import { dispatchTypedError } from './error-dispatch.js'
|
|
4
6
|
import type {
|
|
5
7
|
ClientAdapter,
|
|
6
8
|
ClientHooks,
|
|
9
|
+
ErrorRegistry,
|
|
7
10
|
StreamDescriptor,
|
|
8
11
|
TypedStream,
|
|
9
12
|
AdapterResponse,
|
|
13
|
+
ProcedureCallDefaults,
|
|
14
|
+
ProcedureCallOptions,
|
|
10
15
|
} from './types.js'
|
|
11
16
|
|
|
12
17
|
// ── SSE item shape ────────────────────────────────────────
|
|
@@ -90,66 +95,87 @@ export function createTypedStream<TYield, TReturn = void>(
|
|
|
90
95
|
|
|
91
96
|
// ── executeStream ─────────────────────────────────────────
|
|
92
97
|
|
|
98
|
+
export interface ExecuteStreamConfig {
|
|
99
|
+
descriptor: StreamDescriptor
|
|
100
|
+
basePath: string
|
|
101
|
+
adapter: ClientAdapter
|
|
102
|
+
hooks: ClientHooks
|
|
103
|
+
defaults?: ProcedureCallDefaults
|
|
104
|
+
options?: ProcedureCallOptions
|
|
105
|
+
errorRegistry?: ErrorRegistry
|
|
106
|
+
}
|
|
107
|
+
|
|
93
108
|
/**
|
|
94
109
|
* Executes a streaming procedure call through the adapter.
|
|
95
110
|
*
|
|
96
111
|
* Flow:
|
|
97
|
-
* 1.
|
|
98
|
-
* 2.
|
|
99
|
-
* 3.
|
|
100
|
-
* 4.
|
|
101
|
-
* 5.
|
|
102
|
-
* 6.
|
|
103
|
-
* 7.
|
|
112
|
+
* 1. Resolve base path and build AdapterRequest
|
|
113
|
+
* 2. Apply request options (headers, signal, timeout, meta) from defaults + per-call
|
|
114
|
+
* 3. Run onBeforeRequest hooks
|
|
115
|
+
* 4. Call adapter.stream()
|
|
116
|
+
* 5. On adapter error: run onError hooks, re-throw
|
|
117
|
+
* 6. Run onAfterResponse immediately (before iteration), body is null
|
|
118
|
+
* 7. If non-2xx: throw ClientRequestError
|
|
119
|
+
* 8. Return createTypedStream(streamResponse.body, descriptor.streamMode)
|
|
104
120
|
*/
|
|
105
121
|
export async function executeStream<TYield, TReturn = void>(
|
|
106
|
-
|
|
107
|
-
basePath: string,
|
|
108
|
-
adapter: ClientAdapter,
|
|
109
|
-
globalHooks: ClientHooks,
|
|
110
|
-
localHooks: ClientHooks | undefined
|
|
122
|
+
config: ExecuteStreamConfig,
|
|
111
123
|
): Promise<TypedStream<TYield, TReturn>> {
|
|
124
|
+
const { descriptor, basePath, adapter, hooks, defaults, options, errorRegistry } = config
|
|
125
|
+
|
|
112
126
|
// 1. Build the initial request
|
|
113
|
-
|
|
127
|
+
const resolvedBasePath = resolveBasePath(defaults, options, basePath)
|
|
128
|
+
let request = buildAdapterRequest(descriptor, resolvedBasePath)
|
|
129
|
+
|
|
130
|
+
// 2. Apply request-level options (headers, signal, timeout, meta)
|
|
131
|
+
request = applyRequestOptions(request, defaults, options)
|
|
114
132
|
|
|
115
|
-
//
|
|
133
|
+
// 3. Run before-request hooks
|
|
116
134
|
const beforeCtx = await runBeforeRequest(
|
|
117
135
|
{ procedureName: descriptor.name, scope: descriptor.scope, request },
|
|
118
|
-
|
|
119
|
-
|
|
136
|
+
hooks,
|
|
137
|
+
options,
|
|
120
138
|
)
|
|
121
139
|
request = beforeCtx.request
|
|
122
140
|
|
|
123
|
-
//
|
|
141
|
+
// 4. Call the adapter
|
|
124
142
|
let streamResponse
|
|
125
143
|
try {
|
|
126
144
|
streamResponse = await adapter.stream(request)
|
|
127
145
|
} catch (err) {
|
|
128
|
-
//
|
|
146
|
+
// 5. On adapter error: run error hooks, re-throw
|
|
129
147
|
await runOnError(
|
|
130
148
|
{ procedureName: descriptor.name, scope: descriptor.scope, request, error: err },
|
|
131
|
-
|
|
132
|
-
|
|
149
|
+
hooks,
|
|
150
|
+
options,
|
|
133
151
|
)
|
|
134
152
|
throw err
|
|
135
153
|
}
|
|
136
154
|
|
|
137
|
-
// Build an AdapterResponse shape for the hooks
|
|
155
|
+
// Build an AdapterResponse shape for the hooks. For success the body is null
|
|
156
|
+
// (the actual data flows through the async iterable); for non-2xx the adapter
|
|
157
|
+
// eagerly parses the JSON response body and surfaces it via `errorBody`.
|
|
138
158
|
const responseForHooks: AdapterResponse = {
|
|
139
159
|
status: streamResponse.status,
|
|
140
160
|
headers: streamResponse.headers,
|
|
141
|
-
body: null,
|
|
161
|
+
body: streamResponse.errorBody ?? null,
|
|
142
162
|
}
|
|
143
163
|
|
|
144
|
-
//
|
|
164
|
+
// 6. Run after-response hooks immediately (before iteration)
|
|
145
165
|
await runAfterResponse(
|
|
146
166
|
{ procedureName: descriptor.name, scope: descriptor.scope, request, response: responseForHooks },
|
|
147
|
-
|
|
148
|
-
|
|
167
|
+
hooks,
|
|
168
|
+
options,
|
|
149
169
|
)
|
|
150
170
|
|
|
151
|
-
//
|
|
171
|
+
// 7. Check status after hooks (hooks may mutate responseForHooks.status)
|
|
152
172
|
if (responseForHooks.status < 200 || responseForHooks.status >= 300) {
|
|
173
|
+
const typed = dispatchTypedError(errorRegistry, responseForHooks.body, {
|
|
174
|
+
status: responseForHooks.status,
|
|
175
|
+
procedureName: descriptor.name,
|
|
176
|
+
scope: descriptor.scope,
|
|
177
|
+
})
|
|
178
|
+
if (typed) throw typed
|
|
153
179
|
throw new ClientRequestError({
|
|
154
180
|
status: responseForHooks.status,
|
|
155
181
|
headers: responseForHooks.headers,
|
|
@@ -159,6 +185,6 @@ export async function executeStream<TYield, TReturn = void>(
|
|
|
159
185
|
})
|
|
160
186
|
}
|
|
161
187
|
|
|
162
|
-
//
|
|
188
|
+
// 8. Return the typed stream
|
|
163
189
|
return createTypedStream<TYield, TReturn>(streamResponse.body, descriptor.streamMode)
|
|
164
190
|
}
|