ts-procedures 5.9.1 → 5.10.2
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 +1 -1
- package/agent_config/bin/postinstall.mjs +3 -3
- package/agent_config/bin/setup.mjs +22 -11
- package/agent_config/claude-code/agents/ts-procedures-architect.md +46 -101
- package/agent_config/claude-code/skills/{guide → ts-procedures}/SKILL.md +50 -35
- package/agent_config/claude-code/skills/{guide → ts-procedures}/anti-patterns.md +6 -5
- package/agent_config/claude-code/skills/{guide → ts-procedures}/api-reference.md +60 -49
- package/agent_config/claude-code/skills/ts-procedures-review/SKILL.md +48 -0
- package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/SKILL.md +19 -24
- package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/client.md +115 -0
- package/agent_config/lib/install-claude.mjs +35 -87
- package/build/src/client/call.d.ts +14 -0
- package/build/src/client/call.js +47 -0
- package/build/src/client/call.js.map +1 -0
- package/build/src/client/call.test.d.ts +1 -0
- package/build/src/client/call.test.js +124 -0
- package/build/src/client/call.test.js.map +1 -0
- package/build/src/client/errors.d.ts +25 -0
- package/build/src/client/errors.js +33 -0
- package/build/src/client/errors.js.map +1 -0
- package/build/src/client/errors.test.d.ts +1 -0
- package/build/src/client/errors.test.js +41 -0
- package/build/src/client/errors.test.js.map +1 -0
- package/build/src/client/fetch-adapter.d.ts +12 -0
- package/build/src/client/fetch-adapter.js +156 -0
- package/build/src/client/fetch-adapter.js.map +1 -0
- package/build/src/client/fetch-adapter.test.d.ts +1 -0
- package/build/src/client/fetch-adapter.test.js +271 -0
- package/build/src/client/fetch-adapter.test.js.map +1 -0
- package/build/src/client/hooks.d.ts +17 -0
- package/build/src/client/hooks.js +40 -0
- package/build/src/client/hooks.js.map +1 -0
- package/build/src/client/hooks.test.d.ts +1 -0
- package/build/src/client/hooks.test.js +163 -0
- package/build/src/client/hooks.test.js.map +1 -0
- package/build/src/client/index.d.ts +22 -0
- package/build/src/client/index.js +67 -0
- package/build/src/client/index.js.map +1 -0
- package/build/src/client/index.test.d.ts +1 -0
- package/build/src/client/index.test.js +231 -0
- package/build/src/client/index.test.js.map +1 -0
- package/build/src/client/request-builder.d.ts +13 -0
- package/build/src/client/request-builder.js +53 -0
- package/build/src/client/request-builder.js.map +1 -0
- package/build/src/client/request-builder.test.d.ts +1 -0
- package/build/src/client/request-builder.test.js +160 -0
- package/build/src/client/request-builder.test.js.map +1 -0
- package/build/src/client/stream.d.ts +27 -0
- package/build/src/client/stream.js +118 -0
- package/build/src/client/stream.js.map +1 -0
- package/build/src/client/stream.test.d.ts +1 -0
- package/build/src/client/stream.test.js +228 -0
- package/build/src/client/stream.test.js.map +1 -0
- package/build/src/client/types.d.ts +78 -0
- package/build/src/client/types.js +3 -0
- package/build/src/client/types.js.map +1 -0
- package/build/src/codegen/bin/cli.d.ts +45 -0
- package/build/src/codegen/bin/cli.js +246 -0
- package/build/src/codegen/bin/cli.js.map +1 -0
- package/build/src/codegen/bin/cli.test.d.ts +1 -0
- package/build/src/codegen/bin/cli.test.js +220 -0
- package/build/src/codegen/bin/cli.test.js.map +1 -0
- package/build/src/codegen/constants.d.ts +1 -0
- package/build/src/codegen/constants.js +2 -0
- package/build/src/codegen/constants.js.map +1 -0
- package/build/src/codegen/e2e.test.d.ts +1 -0
- package/build/src/codegen/e2e.test.js +464 -0
- package/build/src/codegen/e2e.test.js.map +1 -0
- package/build/src/codegen/emit-client-runtime.d.ts +9 -0
- package/build/src/codegen/emit-client-runtime.js +99 -0
- package/build/src/codegen/emit-client-runtime.js.map +1 -0
- package/build/src/codegen/emit-client-runtime.test.d.ts +1 -0
- package/build/src/codegen/emit-client-runtime.test.js +78 -0
- package/build/src/codegen/emit-client-runtime.test.js.map +1 -0
- package/build/src/codegen/emit-client-types.d.ts +8 -0
- package/build/src/codegen/emit-client-types.js +25 -0
- package/build/src/codegen/emit-client-types.js.map +1 -0
- package/build/src/codegen/emit-client-types.test.d.ts +1 -0
- package/build/src/codegen/emit-client-types.test.js +33 -0
- package/build/src/codegen/emit-client-types.test.js.map +1 -0
- package/build/src/codegen/emit-errors.d.ts +19 -0
- package/build/src/codegen/emit-errors.js +59 -0
- package/build/src/codegen/emit-errors.js.map +1 -0
- package/build/src/codegen/emit-errors.test.d.ts +1 -0
- package/build/src/codegen/emit-errors.test.js +175 -0
- package/build/src/codegen/emit-errors.test.js.map +1 -0
- package/build/src/codegen/emit-index.d.ts +12 -0
- package/build/src/codegen/emit-index.js +41 -0
- package/build/src/codegen/emit-index.js.map +1 -0
- package/build/src/codegen/emit-index.test.d.ts +1 -0
- package/build/src/codegen/emit-index.test.js +106 -0
- package/build/src/codegen/emit-index.test.js.map +1 -0
- package/build/src/codegen/emit-scope.d.ts +15 -0
- package/build/src/codegen/emit-scope.js +299 -0
- package/build/src/codegen/emit-scope.js.map +1 -0
- package/build/src/codegen/emit-scope.test.d.ts +1 -0
- package/build/src/codegen/emit-scope.test.js +559 -0
- package/build/src/codegen/emit-scope.test.js.map +1 -0
- package/build/src/codegen/emit-types.d.ts +43 -0
- package/build/src/codegen/emit-types.js +111 -0
- package/build/src/codegen/emit-types.js.map +1 -0
- package/build/src/codegen/emit-types.test.d.ts +1 -0
- package/build/src/codegen/emit-types.test.js +184 -0
- package/build/src/codegen/emit-types.test.js.map +1 -0
- package/build/src/codegen/group-routes.d.ts +23 -0
- package/build/src/codegen/group-routes.js +46 -0
- package/build/src/codegen/group-routes.js.map +1 -0
- package/build/src/codegen/group-routes.test.d.ts +1 -0
- package/build/src/codegen/group-routes.test.js +131 -0
- package/build/src/codegen/group-routes.test.js.map +1 -0
- package/build/src/codegen/index.d.ts +15 -0
- package/build/src/codegen/index.js +16 -0
- package/build/src/codegen/index.js.map +1 -0
- package/build/src/codegen/naming.d.ts +7 -0
- package/build/src/codegen/naming.js +21 -0
- package/build/src/codegen/naming.js.map +1 -0
- package/build/src/codegen/naming.test.d.ts +1 -0
- package/build/src/codegen/naming.test.js +40 -0
- package/build/src/codegen/naming.test.js.map +1 -0
- package/build/src/codegen/pipeline.d.ts +17 -0
- package/build/src/codegen/pipeline.js +78 -0
- package/build/src/codegen/pipeline.js.map +1 -0
- package/build/src/codegen/pipeline.test.d.ts +1 -0
- package/build/src/codegen/pipeline.test.js +269 -0
- package/build/src/codegen/pipeline.test.js.map +1 -0
- package/build/src/codegen/resolve-envelope.d.ts +7 -0
- package/build/src/codegen/resolve-envelope.js +46 -0
- package/build/src/codegen/resolve-envelope.js.map +1 -0
- package/build/src/codegen/resolve-envelope.test.d.ts +1 -0
- package/build/src/codegen/resolve-envelope.test.js +69 -0
- package/build/src/codegen/resolve-envelope.test.js.map +1 -0
- package/build/src/errors.d.ts +33 -0
- package/build/src/errors.js +91 -0
- package/build/src/errors.js.map +1 -0
- package/build/src/errors.test.d.ts +1 -0
- package/build/src/errors.test.js +122 -0
- package/build/src/errors.test.js.map +1 -0
- package/build/src/exports.d.ts +7 -0
- package/build/src/exports.js +8 -0
- package/build/src/exports.js.map +1 -0
- package/build/src/implementations/http/doc-registry.d.ts +12 -0
- package/build/src/implementations/http/doc-registry.js +114 -0
- package/build/src/implementations/http/doc-registry.js.map +1 -0
- package/build/src/implementations/http/doc-registry.test.d.ts +1 -0
- package/build/src/implementations/http/doc-registry.test.js +347 -0
- package/build/src/implementations/http/doc-registry.test.js.map +1 -0
- package/build/src/implementations/http/express-rpc/index.d.ts +94 -0
- package/build/src/implementations/http/express-rpc/index.js +185 -0
- package/build/src/implementations/http/express-rpc/index.js.map +1 -0
- package/build/src/implementations/http/express-rpc/index.test.d.ts +1 -0
- package/build/src/implementations/http/express-rpc/index.test.js +684 -0
- package/build/src/implementations/http/express-rpc/index.test.js.map +1 -0
- package/build/src/implementations/http/express-rpc/types.d.ts +11 -0
- package/build/src/implementations/http/express-rpc/types.js +2 -0
- package/build/src/implementations/http/express-rpc/types.js.map +1 -0
- package/build/src/implementations/http/hono-api/index.d.ts +102 -0
- package/build/src/implementations/http/hono-api/index.js +341 -0
- package/build/src/implementations/http/hono-api/index.js.map +1 -0
- package/build/src/implementations/http/hono-api/index.test.d.ts +1 -0
- package/build/src/implementations/http/hono-api/index.test.js +992 -0
- package/build/src/implementations/http/hono-api/index.test.js.map +1 -0
- package/build/src/implementations/http/hono-api/types.d.ts +13 -0
- package/build/src/implementations/http/hono-api/types.js +2 -0
- package/build/src/implementations/http/hono-api/types.js.map +1 -0
- package/build/src/implementations/http/hono-rpc/index.d.ts +92 -0
- package/build/src/implementations/http/hono-rpc/index.js +161 -0
- package/build/src/implementations/http/hono-rpc/index.js.map +1 -0
- package/build/src/implementations/http/hono-rpc/index.test.d.ts +1 -0
- package/build/src/implementations/http/hono-rpc/index.test.js +803 -0
- package/build/src/implementations/http/hono-rpc/index.test.js.map +1 -0
- package/build/src/implementations/http/hono-rpc/types.d.ts +11 -0
- package/build/src/implementations/http/hono-rpc/types.js +2 -0
- package/build/src/implementations/http/hono-rpc/types.js.map +1 -0
- package/build/src/implementations/http/hono-stream/index.d.ts +120 -0
- package/build/src/implementations/http/hono-stream/index.js +309 -0
- package/build/src/implementations/http/hono-stream/index.js.map +1 -0
- package/build/src/implementations/http/hono-stream/index.test.d.ts +1 -0
- package/build/src/implementations/http/hono-stream/index.test.js +1356 -0
- package/build/src/implementations/http/hono-stream/index.test.js.map +1 -0
- package/build/src/implementations/http/hono-stream/types.d.ts +15 -0
- package/build/src/implementations/http/hono-stream/types.js +2 -0
- package/build/src/implementations/http/hono-stream/types.js.map +1 -0
- package/build/src/implementations/types.d.ts +142 -0
- package/build/src/implementations/types.js +2 -0
- package/build/src/implementations/types.js.map +1 -0
- package/build/src/index.d.ts +165 -0
- package/build/src/index.js +253 -0
- package/build/src/index.js.map +1 -0
- package/build/src/index.test.d.ts +1 -0
- package/build/src/index.test.js +890 -0
- package/build/src/index.test.js.map +1 -0
- package/build/src/schema/compute-schema.d.ts +35 -0
- package/build/src/schema/compute-schema.js +41 -0
- package/build/src/schema/compute-schema.js.map +1 -0
- package/build/src/schema/compute-schema.test.d.ts +1 -0
- package/build/src/schema/compute-schema.test.js +107 -0
- package/build/src/schema/compute-schema.test.js.map +1 -0
- package/build/src/schema/extract-json-schema.d.ts +2 -0
- package/build/src/schema/extract-json-schema.js +12 -0
- package/build/src/schema/extract-json-schema.js.map +1 -0
- package/build/src/schema/extract-json-schema.test.d.ts +1 -0
- package/build/src/schema/extract-json-schema.test.js +23 -0
- package/build/src/schema/extract-json-schema.test.js.map +1 -0
- package/build/src/schema/parser.d.ts +28 -0
- package/build/src/schema/parser.js +170 -0
- package/build/src/schema/parser.js.map +1 -0
- package/build/src/schema/parser.test.d.ts +1 -0
- package/build/src/schema/parser.test.js +120 -0
- package/build/src/schema/parser.test.js.map +1 -0
- package/build/src/schema/resolve-schema-lib.d.ts +12 -0
- package/build/src/schema/resolve-schema-lib.js +11 -0
- package/build/src/schema/resolve-schema-lib.js.map +1 -0
- package/build/src/schema/resolve-schema-lib.test.d.ts +1 -0
- package/build/src/schema/resolve-schema-lib.test.js +17 -0
- package/build/src/schema/resolve-schema-lib.test.js.map +1 -0
- package/build/src/schema/types.d.ts +8 -0
- package/build/src/schema/types.js +2 -0
- package/build/src/schema/types.js.map +1 -0
- package/build/src/stack-utils.d.ts +25 -0
- package/build/src/stack-utils.js +95 -0
- package/build/src/stack-utils.js.map +1 -0
- package/build/src/stack-utils.test.d.ts +1 -0
- package/build/src/stack-utils.test.js +80 -0
- package/build/src/stack-utils.test.js.map +1 -0
- package/docs/ai-agent-setup.md +7 -6
- package/docs/core.md +5 -9
- package/docs/streaming.md +9 -9
- package/package.json +2 -13
- package/src/client/call.test.ts +162 -0
- package/src/client/errors.test.ts +43 -0
- package/src/client/fetch-adapter.test.ts +340 -0
- package/src/client/hooks.test.ts +191 -0
- package/src/client/index.test.ts +290 -0
- package/src/client/request-builder.test.ts +184 -0
- package/src/client/stream.test.ts +331 -0
- package/src/codegen/bin/cli.test.ts +260 -0
- package/src/codegen/bin/cli.ts +282 -0
- package/src/codegen/constants.ts +1 -0
- package/src/codegen/e2e.test.ts +565 -0
- package/src/codegen/emit-client-runtime.test.ts +93 -0
- package/src/codegen/emit-client-runtime.ts +114 -0
- package/src/codegen/emit-client-types.test.ts +39 -0
- package/src/codegen/emit-client-types.ts +27 -0
- package/src/codegen/emit-errors.test.ts +202 -0
- package/src/codegen/emit-errors.ts +80 -0
- package/src/codegen/emit-index.test.ts +127 -0
- package/src/codegen/emit-index.ts +58 -0
- package/src/codegen/emit-scope.test.ts +624 -0
- package/src/codegen/emit-scope.ts +389 -0
- package/src/codegen/emit-types.test.ts +205 -0
- package/src/codegen/emit-types.ts +158 -0
- package/src/codegen/group-routes.test.ts +159 -0
- package/src/codegen/group-routes.ts +61 -0
- package/src/codegen/index.ts +30 -0
- package/src/codegen/naming.test.ts +50 -0
- package/src/codegen/naming.ts +25 -0
- package/src/codegen/pipeline.test.ts +316 -0
- package/src/codegen/pipeline.ts +108 -0
- package/src/codegen/resolve-envelope.test.ts +76 -0
- package/src/codegen/resolve-envelope.ts +61 -0
- package/src/errors.test.ts +163 -0
- package/src/errors.ts +107 -0
- package/src/exports.ts +7 -0
- package/src/implementations/http/doc-registry.test.ts +415 -0
- package/src/implementations/http/doc-registry.ts +143 -0
- package/src/implementations/http/express-rpc/README.md +6 -6
- package/src/implementations/http/express-rpc/index.test.ts +957 -0
- package/src/implementations/http/express-rpc/index.ts +266 -0
- package/src/implementations/http/express-rpc/types.ts +16 -0
- package/src/implementations/http/hono-api/index.test.ts +1341 -0
- package/src/implementations/http/hono-api/index.ts +463 -0
- package/src/implementations/http/hono-api/types.ts +16 -0
- package/src/implementations/http/hono-rpc/README.md +6 -6
- package/src/implementations/http/hono-rpc/index.test.ts +1075 -0
- package/src/implementations/http/hono-rpc/index.ts +238 -0
- package/src/implementations/http/hono-rpc/types.ts +16 -0
- package/src/implementations/http/hono-stream/README.md +12 -12
- package/src/implementations/http/hono-stream/index.test.ts +1768 -0
- package/src/implementations/http/hono-stream/index.ts +456 -0
- package/src/implementations/http/hono-stream/types.ts +20 -0
- package/src/implementations/types.ts +174 -0
- package/src/index.test.ts +1185 -0
- package/src/index.ts +522 -0
- package/src/schema/compute-schema.test.ts +128 -0
- package/src/schema/compute-schema.ts +88 -0
- package/src/schema/extract-json-schema.test.ts +25 -0
- package/src/schema/extract-json-schema.ts +15 -0
- package/src/schema/parser.test.ts +182 -0
- package/src/schema/parser.ts +215 -0
- package/src/schema/resolve-schema-lib.test.ts +19 -0
- package/src/schema/resolve-schema-lib.ts +29 -0
- package/src/schema/types.ts +20 -0
- package/src/stack-utils.test.ts +94 -0
- package/src/stack-utils.ts +129 -0
- package/agent_config/claude-code/skills/review/SKILL.md +0 -53
- package/docs/superpowers/plans/2026-03-30-client-codegen.md +0 -2833
- package/docs/superpowers/specs/2026-03-30-client-codegen-design.md +0 -632
- /package/agent_config/claude-code/skills/{guide → ts-procedures}/patterns.md +0 -0
- /package/agent_config/claude-code/skills/{review → ts-procedures-review}/checklist.md +0 -0
- /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/express-rpc.md +0 -0
- /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/hono-api.md +0 -0
- /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/hono-rpc.md +0 -0
- /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/hono-stream.md +0 -0
- /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/procedure.md +0 -0
- /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/stream-procedure.md +0 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { executeCall } from './call.js'
|
|
3
|
+
import { ClientRequestError } from './errors.js'
|
|
4
|
+
import type {
|
|
5
|
+
ClientAdapter,
|
|
6
|
+
AdapterRequest,
|
|
7
|
+
AdapterResponse,
|
|
8
|
+
ClientHooks,
|
|
9
|
+
CallDescriptor,
|
|
10
|
+
} from './types.js'
|
|
11
|
+
|
|
12
|
+
// ── helpers ───────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
function makeDescriptor(overrides?: Partial<CallDescriptor>): CallDescriptor {
|
|
15
|
+
return {
|
|
16
|
+
name: 'GetUser',
|
|
17
|
+
scope: 'users',
|
|
18
|
+
path: '/users',
|
|
19
|
+
method: 'GET',
|
|
20
|
+
kind: 'rpc',
|
|
21
|
+
params: { id: '42' },
|
|
22
|
+
...overrides,
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function makeAdapter(response?: Partial<AdapterResponse>): ClientAdapter {
|
|
27
|
+
return {
|
|
28
|
+
request: vi.fn(async (_req: AdapterRequest): Promise<AdapterResponse> => ({
|
|
29
|
+
status: 200,
|
|
30
|
+
headers: {},
|
|
31
|
+
body: { id: '42', name: 'Alice' },
|
|
32
|
+
...response,
|
|
33
|
+
})),
|
|
34
|
+
stream: vi.fn(async () => {
|
|
35
|
+
throw new Error('stream not expected in call tests')
|
|
36
|
+
}),
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ── executeCall ───────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
describe('executeCall', () => {
|
|
43
|
+
it('calls adapter.request and returns body', async () => {
|
|
44
|
+
const adapter = makeAdapter({ body: { id: '1', name: 'Bob' } })
|
|
45
|
+
const result = await executeCall(makeDescriptor(), 'https://api.example.com', adapter, {}, undefined)
|
|
46
|
+
expect(adapter.request).toHaveBeenCalledOnce()
|
|
47
|
+
expect(result).toEqual({ id: '1', name: 'Bob' })
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('throws ClientRequestError on 4xx response', async () => {
|
|
51
|
+
const adapter = makeAdapter({ status: 404, body: { message: 'Not Found' } })
|
|
52
|
+
await expect(
|
|
53
|
+
executeCall(makeDescriptor(), 'https://api.example.com', adapter, {}, undefined)
|
|
54
|
+
).rejects.toThrow(ClientRequestError)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('throws ClientRequestError on 5xx response', async () => {
|
|
58
|
+
const adapter = makeAdapter({ status: 500, body: { message: 'Server Error' } })
|
|
59
|
+
await expect(
|
|
60
|
+
executeCall(makeDescriptor(), 'https://api.example.com', adapter, {}, undefined)
|
|
61
|
+
).rejects.toThrow(ClientRequestError)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('throws ClientRequestError on 199 response (below 200)', async () => {
|
|
65
|
+
const adapter = makeAdapter({ status: 199, body: null })
|
|
66
|
+
await expect(
|
|
67
|
+
executeCall(makeDescriptor(), 'https://api.example.com', adapter, {}, undefined)
|
|
68
|
+
).rejects.toThrow(ClientRequestError)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('does not throw on 2xx boundary responses (200, 201, 299)', async () => {
|
|
72
|
+
for (const status of [200, 201, 204, 299]) {
|
|
73
|
+
const adapter = makeAdapter({ status, body: null })
|
|
74
|
+
await expect(
|
|
75
|
+
executeCall(makeDescriptor(), 'https://api.example.com', adapter, {}, undefined)
|
|
76
|
+
).resolves.not.toThrow()
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('runs onBeforeRequest before calling adapter (headers are modified)', async () => {
|
|
81
|
+
const capturedHeaders: Record<string, string>[] = []
|
|
82
|
+
const adapter: ClientAdapter = {
|
|
83
|
+
request: vi.fn(async (req: AdapterRequest): Promise<AdapterResponse> => {
|
|
84
|
+
capturedHeaders.push(req.headers ?? {})
|
|
85
|
+
return { status: 200, headers: {}, body: {} }
|
|
86
|
+
}),
|
|
87
|
+
stream: vi.fn(async () => { throw new Error('not expected') }),
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const globalHooks: ClientHooks = {
|
|
91
|
+
onBeforeRequest: (ctx) => ({
|
|
92
|
+
...ctx,
|
|
93
|
+
request: {
|
|
94
|
+
...ctx.request,
|
|
95
|
+
headers: { ...ctx.request.headers, 'x-auth': 'token-123' },
|
|
96
|
+
},
|
|
97
|
+
}),
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
await executeCall(makeDescriptor(), 'https://api.example.com', adapter, globalHooks, undefined)
|
|
101
|
+
expect(capturedHeaders[0]?.['x-auth']).toBe('token-123')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('runs onAfterResponse after adapter returns', async () => {
|
|
105
|
+
const order: string[] = []
|
|
106
|
+
const adapter: ClientAdapter = {
|
|
107
|
+
request: vi.fn(async (): Promise<AdapterResponse> => {
|
|
108
|
+
order.push('adapter')
|
|
109
|
+
return { status: 200, headers: {}, body: {} }
|
|
110
|
+
}),
|
|
111
|
+
stream: vi.fn(async () => { throw new Error('not expected') }),
|
|
112
|
+
}
|
|
113
|
+
const globalHooks: ClientHooks = {
|
|
114
|
+
onAfterResponse: () => { order.push('afterResponse') },
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
await executeCall(makeDescriptor(), 'https://api.example.com', adapter, globalHooks, undefined)
|
|
118
|
+
expect(order).toEqual(['adapter', 'afterResponse'])
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('does not throw when onAfterResponse swallows non-2xx by mutating status', async () => {
|
|
122
|
+
const adapter = makeAdapter({ status: 401, body: { message: 'Unauthorized' } })
|
|
123
|
+
const globalHooks: ClientHooks = {
|
|
124
|
+
onAfterResponse: (ctx) => {
|
|
125
|
+
// Swallow the error by setting status to 200
|
|
126
|
+
ctx.response.status = 200
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
await expect(
|
|
131
|
+
executeCall(makeDescriptor(), 'https://api.example.com', adapter, globalHooks, undefined)
|
|
132
|
+
).resolves.not.toThrow()
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('runs onError on adapter failure and re-throws', async () => {
|
|
136
|
+
const adapterError = new Error('Network failure')
|
|
137
|
+
const adapter: ClientAdapter = {
|
|
138
|
+
request: vi.fn(async () => { throw adapterError }),
|
|
139
|
+
stream: vi.fn(async () => { throw new Error('not expected') }),
|
|
140
|
+
}
|
|
141
|
+
const receivedErrors: unknown[] = []
|
|
142
|
+
const globalHooks: ClientHooks = {
|
|
143
|
+
onError: (ctx) => { receivedErrors.push(ctx.error) },
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
await expect(
|
|
147
|
+
executeCall(makeDescriptor(), 'https://api.example.com', adapter, globalHooks, undefined)
|
|
148
|
+
).rejects.toThrow('Network failure')
|
|
149
|
+
expect(receivedErrors[0]).toBe(adapterError)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('passes per-procedure hooks as local hooks', async () => {
|
|
153
|
+
const adapter = makeAdapter()
|
|
154
|
+
const localOrder: string[] = []
|
|
155
|
+
const localHooks: ClientHooks = {
|
|
156
|
+
onBeforeRequest: (ctx) => { localOrder.push('local-before'); return ctx },
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
await executeCall(makeDescriptor(), 'https://api.example.com', adapter, {}, localHooks)
|
|
160
|
+
expect(localOrder).toContain('local-before')
|
|
161
|
+
})
|
|
162
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { ClientRequestError, ClientPathParamError, ClientStreamError } from './errors.js'
|
|
3
|
+
|
|
4
|
+
describe('ClientRequestError', () => {
|
|
5
|
+
it('includes status, headers, and body', () => {
|
|
6
|
+
const err = new ClientRequestError({
|
|
7
|
+
status: 401,
|
|
8
|
+
headers: { 'x-request-id': 'abc' },
|
|
9
|
+
body: { message: 'Unauthorized' },
|
|
10
|
+
procedureName: 'GetUser',
|
|
11
|
+
scope: 'users',
|
|
12
|
+
})
|
|
13
|
+
expect(err).toBeInstanceOf(Error)
|
|
14
|
+
expect(err.name).toBe('ClientRequestError')
|
|
15
|
+
expect(err.status).toBe(401)
|
|
16
|
+
expect(err.headers['x-request-id']).toBe('abc')
|
|
17
|
+
expect(err.body).toEqual({ message: 'Unauthorized' })
|
|
18
|
+
expect(err.procedureName).toBe('GetUser')
|
|
19
|
+
expect(err.scope).toBe('users')
|
|
20
|
+
expect(err.message).toBe('GetUser (users) failed with status 401')
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
describe('ClientPathParamError', () => {
|
|
25
|
+
it('reports missing param', () => {
|
|
26
|
+
const err = new ClientPathParamError('id', '/users/:id', 'GetUser')
|
|
27
|
+
expect(err).toBeInstanceOf(Error)
|
|
28
|
+
expect(err.name).toBe('ClientPathParamError')
|
|
29
|
+
expect(err.message).toContain('id')
|
|
30
|
+
expect(err.message).toContain('/users/:id')
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
describe('ClientStreamError', () => {
|
|
35
|
+
it('includes procedure context', () => {
|
|
36
|
+
const err = new ClientStreamError('stream interrupted', 'Watch', 'events')
|
|
37
|
+
expect(err).toBeInstanceOf(Error)
|
|
38
|
+
expect(err.name).toBe('ClientStreamError')
|
|
39
|
+
expect(err.procedureName).toBe('Watch')
|
|
40
|
+
expect(err.scope).toBe('events')
|
|
41
|
+
expect(err.message).toBe('stream interrupted')
|
|
42
|
+
})
|
|
43
|
+
})
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { createFetchAdapter } from './fetch-adapter.js'
|
|
3
|
+
|
|
4
|
+
// ── helpers ───────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
function mockSSEResponse(sseText: string): Response {
|
|
7
|
+
const encoder = new TextEncoder()
|
|
8
|
+
const stream = new ReadableStream({
|
|
9
|
+
start(controller) {
|
|
10
|
+
controller.enqueue(encoder.encode(sseText))
|
|
11
|
+
controller.close()
|
|
12
|
+
},
|
|
13
|
+
})
|
|
14
|
+
return new Response(stream, {
|
|
15
|
+
status: 200,
|
|
16
|
+
headers: { 'content-type': 'text/event-stream' },
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function mockSSEResponseChunked(chunks: string[]): Response {
|
|
21
|
+
const encoder = new TextEncoder()
|
|
22
|
+
const stream = new ReadableStream({
|
|
23
|
+
start(controller) {
|
|
24
|
+
for (const chunk of chunks) {
|
|
25
|
+
controller.enqueue(encoder.encode(chunk))
|
|
26
|
+
}
|
|
27
|
+
controller.close()
|
|
28
|
+
},
|
|
29
|
+
})
|
|
30
|
+
return new Response(stream, {
|
|
31
|
+
status: 200,
|
|
32
|
+
headers: { 'content-type': 'text/event-stream' },
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function collectStream(stream: AsyncIterable<unknown>): Promise<unknown[]> {
|
|
37
|
+
const items: unknown[] = []
|
|
38
|
+
for await (const item of stream) {
|
|
39
|
+
items.push(item)
|
|
40
|
+
}
|
|
41
|
+
return items
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── request() tests ───────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
describe('createFetchAdapter — request()', () => {
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
vi.stubGlobal('fetch', vi.fn())
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
vi.unstubAllGlobals()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('makes fetch call with correct URL, method, body', async () => {
|
|
56
|
+
const mockFetch = vi.mocked(globalThis.fetch)
|
|
57
|
+
mockFetch.mockResolvedValueOnce(
|
|
58
|
+
new Response(JSON.stringify({ id: 1 }), { status: 200 })
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
const adapter = createFetchAdapter()
|
|
62
|
+
await adapter.request({
|
|
63
|
+
url: 'https://api.example.com/users',
|
|
64
|
+
method: 'POST',
|
|
65
|
+
body: { name: 'Alice' },
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
expect(mockFetch).toHaveBeenCalledOnce()
|
|
69
|
+
const [url, init] = mockFetch.mock.calls[0]!
|
|
70
|
+
expect(url).toBe('https://api.example.com/users')
|
|
71
|
+
expect((init as Record<string, unknown>).method).toBe('POST')
|
|
72
|
+
expect((init as Record<string, unknown>).body).toBe(JSON.stringify({ name: 'Alice' }))
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('merges config headers with request headers (request wins)', async () => {
|
|
76
|
+
const mockFetch = vi.mocked(globalThis.fetch)
|
|
77
|
+
mockFetch.mockResolvedValueOnce(
|
|
78
|
+
new Response(JSON.stringify({}), { status: 200 })
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
const adapter = createFetchAdapter({
|
|
82
|
+
headers: {
|
|
83
|
+
'x-api-key': 'default-key',
|
|
84
|
+
'x-source': 'config',
|
|
85
|
+
},
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
await adapter.request({
|
|
89
|
+
url: 'https://api.example.com/data',
|
|
90
|
+
method: 'GET',
|
|
91
|
+
headers: {
|
|
92
|
+
'x-api-key': 'override-key', // should win
|
|
93
|
+
'x-request-id': 'abc123',
|
|
94
|
+
},
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
const [, init] = mockFetch.mock.calls[0]!
|
|
98
|
+
const headers = (init as Record<string, unknown>).headers as Record<string, string>
|
|
99
|
+
expect(headers['x-api-key']).toBe('override-key')
|
|
100
|
+
expect(headers['x-source']).toBe('config')
|
|
101
|
+
expect(headers['x-request-id']).toBe('abc123')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('parses JSON response body', async () => {
|
|
105
|
+
const mockFetch = vi.mocked(globalThis.fetch)
|
|
106
|
+
mockFetch.mockResolvedValueOnce(
|
|
107
|
+
new Response(JSON.stringify({ result: 'ok' }), { status: 200 })
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
const adapter = createFetchAdapter()
|
|
111
|
+
const response = await adapter.request({
|
|
112
|
+
url: 'https://api.example.com/ping',
|
|
113
|
+
method: 'GET',
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
expect(response.status).toBe(200)
|
|
117
|
+
expect(response.body).toEqual({ result: 'ok' })
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('handles empty/non-JSON response body gracefully (e.g. 204 No Content)', async () => {
|
|
121
|
+
const mockFetch = vi.mocked(globalThis.fetch)
|
|
122
|
+
mockFetch.mockResolvedValueOnce(new Response(null, { status: 204 }))
|
|
123
|
+
|
|
124
|
+
const adapter = createFetchAdapter()
|
|
125
|
+
const response = await adapter.request({
|
|
126
|
+
url: 'https://api.example.com/delete',
|
|
127
|
+
method: 'DELETE',
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
expect(response.status).toBe(204)
|
|
131
|
+
// body should be null (or empty string) — not throw
|
|
132
|
+
expect(response.body === null || response.body === '').toBe(true)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('passes signal to fetch', async () => {
|
|
136
|
+
const mockFetch = vi.mocked(globalThis.fetch)
|
|
137
|
+
mockFetch.mockResolvedValueOnce(
|
|
138
|
+
new Response(JSON.stringify({}), { status: 200 })
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
const controller = new AbortController()
|
|
142
|
+
const adapter = createFetchAdapter()
|
|
143
|
+
await adapter.request({
|
|
144
|
+
url: 'https://api.example.com/slow',
|
|
145
|
+
method: 'GET',
|
|
146
|
+
signal: controller.signal,
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
const [, init] = mockFetch.mock.calls[0]!
|
|
150
|
+
expect((init as Record<string, unknown>).signal).toBe(controller.signal)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('returns response headers as Record<string, string>', async () => {
|
|
154
|
+
const mockFetch = vi.mocked(globalThis.fetch)
|
|
155
|
+
mockFetch.mockResolvedValueOnce(
|
|
156
|
+
new Response(JSON.stringify({}), {
|
|
157
|
+
status: 200,
|
|
158
|
+
headers: { 'content-type': 'application/json', 'x-request-id': 'xyz' },
|
|
159
|
+
})
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
const adapter = createFetchAdapter()
|
|
163
|
+
const response = await adapter.request({
|
|
164
|
+
url: 'https://api.example.com/data',
|
|
165
|
+
method: 'GET',
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
expect(response.headers['content-type']).toBe('application/json')
|
|
169
|
+
expect(response.headers['x-request-id']).toBe('xyz')
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('sends no body when body is undefined', async () => {
|
|
173
|
+
const mockFetch = vi.mocked(globalThis.fetch)
|
|
174
|
+
mockFetch.mockResolvedValueOnce(new Response(JSON.stringify({}), { status: 200 }))
|
|
175
|
+
|
|
176
|
+
const adapter = createFetchAdapter()
|
|
177
|
+
await adapter.request({
|
|
178
|
+
url: 'https://api.example.com/items',
|
|
179
|
+
method: 'GET',
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
const [, init] = mockFetch.mock.calls[0]!
|
|
183
|
+
expect((init as Record<string, unknown>).body).toBeUndefined()
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
// ── stream() tests ────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
describe('createFetchAdapter — stream()', () => {
|
|
190
|
+
beforeEach(() => {
|
|
191
|
+
vi.stubGlobal('fetch', vi.fn())
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
afterEach(() => {
|
|
195
|
+
vi.unstubAllGlobals()
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('parses SSE events from stream', async () => {
|
|
199
|
+
const mockFetch = vi.mocked(globalThis.fetch)
|
|
200
|
+
const sseText =
|
|
201
|
+
'event: message\ndata: {"count":1}\nid: 0\n\nevent: message\ndata: {"count":2}\nid: 1\n\n'
|
|
202
|
+
mockFetch.mockResolvedValueOnce(mockSSEResponse(sseText))
|
|
203
|
+
|
|
204
|
+
const adapter = createFetchAdapter()
|
|
205
|
+
const response = await adapter.stream({
|
|
206
|
+
url: 'https://api.example.com/stream',
|
|
207
|
+
method: 'GET',
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
expect(response.status).toBe(200)
|
|
211
|
+
const items = await collectStream(response.body)
|
|
212
|
+
expect(items).toEqual([
|
|
213
|
+
{ data: { count: 1 }, event: 'message', id: '0' },
|
|
214
|
+
{ data: { count: 2 }, event: 'message', id: '1' },
|
|
215
|
+
])
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('handles multi-line data fields (multiple data: lines joined with newline)', async () => {
|
|
219
|
+
const mockFetch = vi.mocked(globalThis.fetch)
|
|
220
|
+
// Multi-line data: per SSE spec, multiple data: lines are joined with \n
|
|
221
|
+
const sseText = 'event: multi\ndata: {"line":\ndata: "hello"}\n\n'
|
|
222
|
+
mockFetch.mockResolvedValueOnce(mockSSEResponse(sseText))
|
|
223
|
+
|
|
224
|
+
const adapter = createFetchAdapter()
|
|
225
|
+
const response = await adapter.stream({
|
|
226
|
+
url: 'https://api.example.com/stream',
|
|
227
|
+
method: 'GET',
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
const items = await collectStream(response.body)
|
|
231
|
+
expect(items).toHaveLength(1)
|
|
232
|
+
const item = items[0] as { data: unknown; event: string }
|
|
233
|
+
expect(item.event).toBe('multi')
|
|
234
|
+
// data is parsed from joined lines: '{"line":\n"hello"}'
|
|
235
|
+
expect(item.data).toEqual({ line: 'hello' })
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
it('handles chunked SSE data (message split across multiple chunks)', async () => {
|
|
239
|
+
const mockFetch = vi.mocked(globalThis.fetch)
|
|
240
|
+
// Split the SSE message across 3 chunks
|
|
241
|
+
const chunks = [
|
|
242
|
+
'event: tick\nda',
|
|
243
|
+
'ta: {"n":42}\n',
|
|
244
|
+
'\n',
|
|
245
|
+
]
|
|
246
|
+
mockFetch.mockResolvedValueOnce(mockSSEResponseChunked(chunks))
|
|
247
|
+
|
|
248
|
+
const adapter = createFetchAdapter()
|
|
249
|
+
const response = await adapter.stream({
|
|
250
|
+
url: 'https://api.example.com/stream',
|
|
251
|
+
method: 'GET',
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
const items = await collectStream(response.body)
|
|
255
|
+
expect(items).toEqual([{ data: { n: 42 }, event: 'tick', id: undefined }])
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('yields parsed objects with event and id', async () => {
|
|
259
|
+
const mockFetch = vi.mocked(globalThis.fetch)
|
|
260
|
+
const sseText = 'event: update\ndata: {"value":7}\nid: evt-99\n\n'
|
|
261
|
+
mockFetch.mockResolvedValueOnce(mockSSEResponse(sseText))
|
|
262
|
+
|
|
263
|
+
const adapter = createFetchAdapter()
|
|
264
|
+
const response = await adapter.stream({
|
|
265
|
+
url: 'https://api.example.com/stream',
|
|
266
|
+
method: 'GET',
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
const items = await collectStream(response.body)
|
|
270
|
+
expect(items).toEqual([{ data: { value: 7 }, event: 'update', id: 'evt-99' }])
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
it('handles event: return correctly (just another SSE event)', async () => {
|
|
274
|
+
const mockFetch = vi.mocked(globalThis.fetch)
|
|
275
|
+
const sseText =
|
|
276
|
+
'event: message\ndata: {"n":1}\n\nevent: return\ndata: {"total":10}\n\n'
|
|
277
|
+
mockFetch.mockResolvedValueOnce(mockSSEResponse(sseText))
|
|
278
|
+
|
|
279
|
+
const adapter = createFetchAdapter()
|
|
280
|
+
const response = await adapter.stream({
|
|
281
|
+
url: 'https://api.example.com/stream',
|
|
282
|
+
method: 'GET',
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
// The adapter yields all SSE events as-is — createTypedStream handles 'return' semantics
|
|
286
|
+
const items = await collectStream(response.body)
|
|
287
|
+
expect(items).toEqual([
|
|
288
|
+
{ data: { n: 1 }, event: 'message', id: undefined },
|
|
289
|
+
{ data: { total: 10 }, event: 'return', id: undefined },
|
|
290
|
+
])
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
it('returns response headers from stream response', async () => {
|
|
294
|
+
const mockFetch = vi.mocked(globalThis.fetch)
|
|
295
|
+
mockFetch.mockResolvedValueOnce(
|
|
296
|
+
mockSSEResponse('event: ping\ndata: {}\n\n')
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
const adapter = createFetchAdapter()
|
|
300
|
+
const response = await adapter.stream({
|
|
301
|
+
url: 'https://api.example.com/stream',
|
|
302
|
+
method: 'GET',
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
expect(response.headers['content-type']).toContain('text/event-stream')
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
it('passes signal to fetch for streams', async () => {
|
|
309
|
+
const mockFetch = vi.mocked(globalThis.fetch)
|
|
310
|
+
mockFetch.mockResolvedValueOnce(mockSSEResponse('event: ping\ndata: {}\n\n'))
|
|
311
|
+
|
|
312
|
+
const controller = new AbortController()
|
|
313
|
+
const adapter = createFetchAdapter()
|
|
314
|
+
await adapter.stream({
|
|
315
|
+
url: 'https://api.example.com/stream',
|
|
316
|
+
method: 'GET',
|
|
317
|
+
signal: controller.signal,
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
const [, init] = mockFetch.mock.calls[0]!
|
|
321
|
+
expect((init as Record<string, unknown>).signal).toBe(controller.signal)
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
it('handles SSE events without data field gracefully (skips them)', async () => {
|
|
325
|
+
const mockFetch = vi.mocked(globalThis.fetch)
|
|
326
|
+
// Comment-only message (no data field) — should be skipped
|
|
327
|
+
const sseText = ': keep-alive\n\nevent: message\ndata: {"ok":true}\n\n'
|
|
328
|
+
mockFetch.mockResolvedValueOnce(mockSSEResponse(sseText))
|
|
329
|
+
|
|
330
|
+
const adapter = createFetchAdapter()
|
|
331
|
+
const response = await adapter.stream({
|
|
332
|
+
url: 'https://api.example.com/stream',
|
|
333
|
+
method: 'GET',
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
const items = await collectStream(response.body)
|
|
337
|
+
// Only the message with data is yielded; comment-only block is skipped
|
|
338
|
+
expect(items).toEqual([{ data: { ok: true }, event: 'message', id: undefined }])
|
|
339
|
+
})
|
|
340
|
+
})
|