ts-procedures 5.7.0 → 5.7.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.
Files changed (40) hide show
  1. package/README.md +185 -29
  2. package/agent_config/claude-code/skills/guide/SKILL.md +1 -1
  3. package/agent_config/claude-code/skills/guide/api-reference.md +203 -3
  4. package/agent_config/claude-code/skills/guide/patterns.md +108 -0
  5. package/agent_config/copilot/copilot-instructions.md +87 -0
  6. package/agent_config/cursor/cursorrules +87 -0
  7. package/build/implementations/http/doc-registry.test.js +27 -1
  8. package/build/implementations/http/doc-registry.test.js.map +1 -1
  9. package/build/implementations/http/express-rpc/index.js +1 -0
  10. package/build/implementations/http/express-rpc/index.js.map +1 -1
  11. package/build/implementations/http/express-rpc/index.test.js +1 -1
  12. package/build/implementations/http/express-rpc/index.test.js.map +1 -1
  13. package/build/implementations/http/hono-api/index.js +2 -0
  14. package/build/implementations/http/hono-api/index.js.map +1 -1
  15. package/build/implementations/http/hono-api/index.test.js +9 -0
  16. package/build/implementations/http/hono-api/index.test.js.map +1 -1
  17. package/build/implementations/http/hono-rpc/index.js +1 -0
  18. package/build/implementations/http/hono-rpc/index.js.map +1 -1
  19. package/build/implementations/http/hono-rpc/index.test.js +1 -1
  20. package/build/implementations/http/hono-rpc/index.test.js.map +1 -1
  21. package/build/implementations/http/hono-stream/index.js +17 -1
  22. package/build/implementations/http/hono-stream/index.js.map +1 -1
  23. package/build/implementations/http/hono-stream/index.test.js +75 -6
  24. package/build/implementations/http/hono-stream/index.test.js.map +1 -1
  25. package/build/implementations/http/hono-stream/types.d.ts +4 -13
  26. package/build/implementations/types.d.ts +5 -0
  27. package/build/index.d.ts +2 -6
  28. package/build/index.js +8 -1
  29. package/build/index.js.map +1 -1
  30. package/build/index.test.js +4 -10
  31. package/build/index.test.js.map +1 -1
  32. package/package.json +21 -3
  33. package/src/client/call.ts +74 -0
  34. package/src/client/errors.ts +43 -0
  35. package/src/client/fetch-adapter.ts +191 -0
  36. package/src/client/hooks.ts +65 -0
  37. package/src/client/index.ts +121 -0
  38. package/src/client/request-builder.ts +73 -0
  39. package/src/client/stream.ts +164 -0
  40. package/src/client/types.ts +103 -0
@@ -0,0 +1,121 @@
1
+ import { executeCall } from './call.js'
2
+ import { executeStream, createTypedStream } from './stream.js'
3
+ import type {
4
+ CreateClientConfig,
5
+ ClientInstance,
6
+ CallDescriptor,
7
+ StreamDescriptor,
8
+ ProcedureCallOptions,
9
+ TypedStream,
10
+ } from './types.js'
11
+
12
+ // ── createClient ──────────────────────────────────────────
13
+
14
+ /**
15
+ * Creates a typed client from a config object.
16
+ *
17
+ * The `scopes` callback receives a `ClientInstance` and returns the typed
18
+ * scope bindings (e.g., `{ users: { getUser, createUser }, posts: { ... } }`).
19
+ * The return value of `createClient` is the scopes object.
20
+ *
21
+ * `client.stream()` must return `TypedStream` synchronously even though
22
+ * `executeStream` is async. We achieve this by creating a deferred TypedStream:
23
+ * - A deferred async generator awaits `executeStream` internally, then forwards
24
+ * yields from the inner stream.
25
+ * - The outer `.result` is wired up to the inner stream's `.result`.
26
+ */
27
+ export function createClient<TScopes>(config: CreateClientConfig<TScopes>): TScopes {
28
+ const { adapter, basePath, hooks: globalHooks = {}, scopes } = config
29
+
30
+ const instance: ClientInstance = {
31
+ basePath,
32
+ adapter,
33
+ hooks: globalHooks,
34
+
35
+ call<TResponse>(
36
+ descriptor: CallDescriptor,
37
+ options?: ProcedureCallOptions
38
+ ): Promise<TResponse> {
39
+ return executeCall<TResponse>(descriptor, basePath, adapter, globalHooks, options)
40
+ },
41
+
42
+ stream<TYield, TReturn>(
43
+ descriptor: StreamDescriptor,
44
+ options?: ProcedureCallOptions
45
+ ): TypedStream<TYield, TReturn> {
46
+ // executeStream is async but stream() must be synchronous.
47
+ // Create a deferred TypedStream that wraps the async executeStream call.
48
+
49
+ let resolveResult: (value: TReturn) => void
50
+ let rejectResult: (reason: unknown) => void
51
+
52
+ const resultPromise = new Promise<TReturn>((resolve, reject) => {
53
+ resolveResult = resolve
54
+ rejectResult = reject
55
+ })
56
+
57
+ // The deferred async generator: awaits executeStream, then forwards
58
+ async function* deferredGenerator(): AsyncGenerator<TYield> {
59
+ let innerStream: TypedStream<TYield, TReturn>
60
+ try {
61
+ innerStream = await executeStream<TYield, TReturn>(
62
+ descriptor,
63
+ basePath,
64
+ adapter,
65
+ globalHooks,
66
+ options
67
+ )
68
+ } catch (err) {
69
+ rejectResult(err)
70
+ throw err
71
+ }
72
+
73
+ // Wire up .result from the inner stream
74
+ innerStream.result.then(resolveResult, rejectResult)
75
+
76
+ for await (const item of innerStream) {
77
+ yield item
78
+ }
79
+ }
80
+
81
+ const iterator = deferredGenerator()
82
+
83
+ return {
84
+ [Symbol.asyncIterator]() {
85
+ return iterator
86
+ },
87
+ result: resultPromise,
88
+ }
89
+ },
90
+ }
91
+
92
+ return scopes(instance)
93
+ }
94
+
95
+ // ── Barrel exports ────────────────────────────────────────
96
+
97
+ export type {
98
+ ClientAdapter,
99
+ AdapterRequest,
100
+ AdapterResponse,
101
+ AdapterStreamResponse,
102
+ ClientHooks,
103
+ BeforeRequestContext,
104
+ AfterResponseContext,
105
+ ErrorContext,
106
+ CallDescriptor,
107
+ StreamDescriptor,
108
+ TypedStream,
109
+ ClientInstance,
110
+ ProcedureCallOptions,
111
+ CreateClientConfig,
112
+ } from './types.js'
113
+
114
+ export { ClientRequestError, ClientPathParamError, ClientStreamError } from './errors.js'
115
+
116
+ export { createTypedStream } from './stream.js'
117
+ export { executeCall } from './call.js'
118
+ export { executeStream } from './stream.js'
119
+
120
+ export { createFetchAdapter } from './fetch-adapter.js'
121
+ export type { FetchAdapterConfig } from './fetch-adapter.js'
@@ -0,0 +1,73 @@
1
+ import { ClientPathParamError } from './errors.js'
2
+ import type { AdapterRequest, CallDescriptor } from './types.js'
3
+
4
+ /**
5
+ * Replaces `:paramName` segments in `path` with URI-encoded values from `params`.
6
+ * Throws `ClientPathParamError` if a required segment is missing from `params`.
7
+ */
8
+ export function interpolatePath(
9
+ path: string,
10
+ params: Record<string, unknown>,
11
+ procedureName: string
12
+ ): string {
13
+ return path.replace(/:([A-Za-z_][A-Za-z0-9_]*)/g, (_match, key: string) => {
14
+ const value = params[key]
15
+ if (value === undefined || value === null) {
16
+ throw new ClientPathParamError(key, path, procedureName)
17
+ }
18
+ return encodeURIComponent(String(value))
19
+ })
20
+ }
21
+
22
+ /**
23
+ * Builds an `AdapterRequest` from a `CallDescriptor` and a base URL.
24
+ *
25
+ * - `kind === 'rpc'` or `kind === 'stream'`: params are flat — sent as the JSON body.
26
+ * - `kind === 'api'`: params are structured channels — `pathParams`, `query`, `body`, `headers`.
27
+ */
28
+ export function buildAdapterRequest(descriptor: CallDescriptor, basePath: string): AdapterRequest {
29
+ const { name, path, method, kind, params } = descriptor
30
+
31
+ if (kind === 'rpc' || kind === 'stream') {
32
+ return {
33
+ url: `${basePath}${path}`,
34
+ method,
35
+ body: params,
36
+ }
37
+ }
38
+
39
+ // kind === 'api' — params are structured channels
40
+ const structured = (params ?? {}) as {
41
+ pathParams?: Record<string, unknown>
42
+ query?: Record<string, unknown>
43
+ body?: unknown
44
+ headers?: Record<string, string>
45
+ }
46
+
47
+ // Interpolate path params
48
+ const interpolatedPath = structured.pathParams
49
+ ? interpolatePath(path, structured.pathParams, name)
50
+ : path
51
+
52
+ // Build query string
53
+ let url = `${basePath}${interpolatedPath}`
54
+ if (structured.query && Object.keys(structured.query).length > 0) {
55
+ const searchParams = new URLSearchParams(
56
+ Object.entries(structured.query).map(([k, v]) => [k, String(v)])
57
+ )
58
+ url = `${url}?${searchParams.toString()}`
59
+ }
60
+
61
+ // Build headers
62
+ const headers =
63
+ structured.headers && Object.keys(structured.headers).length > 0
64
+ ? structured.headers
65
+ : undefined
66
+
67
+ return {
68
+ url,
69
+ method,
70
+ headers,
71
+ body: structured.body,
72
+ }
73
+ }
@@ -0,0 +1,164 @@
1
+ import { buildAdapterRequest } from './request-builder.js'
2
+ import { runBeforeRequest, runAfterResponse, runOnError } from './hooks.js'
3
+ import { ClientRequestError } from './errors.js'
4
+ import type {
5
+ ClientAdapter,
6
+ ClientHooks,
7
+ StreamDescriptor,
8
+ TypedStream,
9
+ AdapterResponse,
10
+ } from './types.js'
11
+
12
+ // ── SSE item shape ────────────────────────────────────────
13
+
14
+ interface SSEItem {
15
+ data: unknown
16
+ event?: string
17
+ id?: string
18
+ }
19
+
20
+ // ── createTypedStream ─────────────────────────────────────
21
+
22
+ /**
23
+ * Wraps an AsyncIterable into a TypedStream.
24
+ *
25
+ * SSE mode: each item is `{ data, event?, id? }`.
26
+ * - If `event === 'return'`, the data resolves `.result` and is NOT yielded.
27
+ * - Otherwise, `data` is yielded.
28
+ *
29
+ * Text mode: each item is yielded as-is.
30
+ * - `.result` resolves to `void` on completion.
31
+ *
32
+ * On error: `.result` rejects and the error is re-thrown from the async iterator.
33
+ */
34
+ export function createTypedStream<TYield, TReturn = void>(
35
+ source: AsyncIterable<unknown>,
36
+ streamMode: 'sse' | 'text'
37
+ ): TypedStream<TYield, TReturn> {
38
+ let resolveResult: (value: TReturn) => void
39
+ let rejectResult: (reason: unknown) => void
40
+
41
+ const resultPromise = new Promise<TReturn>((resolve, reject) => {
42
+ resolveResult = resolve
43
+ rejectResult = reject
44
+ })
45
+
46
+ async function* generate(): AsyncGenerator<TYield> {
47
+ try {
48
+ if (streamMode === 'sse') {
49
+ let returnValue: TReturn | undefined
50
+ let hasReturn = false
51
+
52
+ for await (const item of source) {
53
+ const sseItem = item as SSEItem
54
+ if (sseItem.event === 'return') {
55
+ returnValue = sseItem.data as TReturn
56
+ hasReturn = true
57
+ } else {
58
+ yield sseItem.data as TYield
59
+ }
60
+ }
61
+
62
+ // Resolve result after iteration completes
63
+ if (hasReturn) {
64
+ resolveResult(returnValue as TReturn)
65
+ } else {
66
+ resolveResult(undefined as TReturn)
67
+ }
68
+ } else {
69
+ // text mode: yield each item as-is
70
+ for await (const item of source) {
71
+ yield item as TYield
72
+ }
73
+ resolveResult(undefined as TReturn)
74
+ }
75
+ } catch (err) {
76
+ rejectResult(err)
77
+ throw err
78
+ }
79
+ }
80
+
81
+ const iterator = generate()
82
+
83
+ return {
84
+ [Symbol.asyncIterator]() {
85
+ return iterator
86
+ },
87
+ result: resultPromise,
88
+ }
89
+ }
90
+
91
+ // ── executeStream ─────────────────────────────────────────
92
+
93
+ /**
94
+ * Executes a streaming procedure call through the adapter.
95
+ *
96
+ * Flow:
97
+ * 1. Build AdapterRequest from descriptor
98
+ * 2. Run onBeforeRequest hooks
99
+ * 3. Call adapter.stream()
100
+ * 4. On adapter error: run onError hooks, re-throw
101
+ * 5. Run onAfterResponse immediately (before iteration), body is null
102
+ * 6. If non-2xx: throw ClientRequestError
103
+ * 7. Return createTypedStream(streamResponse.body, descriptor.streamMode)
104
+ */
105
+ export async function executeStream<TYield, TReturn = void>(
106
+ descriptor: StreamDescriptor,
107
+ basePath: string,
108
+ adapter: ClientAdapter,
109
+ globalHooks: ClientHooks,
110
+ localHooks: ClientHooks | undefined
111
+ ): Promise<TypedStream<TYield, TReturn>> {
112
+ // 1. Build the initial request
113
+ let request = buildAdapterRequest(descriptor, basePath)
114
+
115
+ // 2. Run before-request hooks
116
+ const beforeCtx = await runBeforeRequest(
117
+ { procedureName: descriptor.name, scope: descriptor.scope, request },
118
+ globalHooks,
119
+ localHooks
120
+ )
121
+ request = beforeCtx.request
122
+
123
+ // 3. Call the adapter
124
+ let streamResponse
125
+ try {
126
+ streamResponse = await adapter.stream(request)
127
+ } catch (err) {
128
+ // 4. On adapter error: run error hooks, re-throw
129
+ await runOnError(
130
+ { procedureName: descriptor.name, scope: descriptor.scope, request, error: err },
131
+ globalHooks,
132
+ localHooks
133
+ )
134
+ throw err
135
+ }
136
+
137
+ // Build an AdapterResponse shape for the hooks (body is null for streams at this point)
138
+ const responseForHooks: AdapterResponse = {
139
+ status: streamResponse.status,
140
+ headers: streamResponse.headers,
141
+ body: null,
142
+ }
143
+
144
+ // 5. Run after-response hooks immediately (before iteration)
145
+ await runAfterResponse(
146
+ { procedureName: descriptor.name, scope: descriptor.scope, request, response: responseForHooks },
147
+ globalHooks,
148
+ localHooks
149
+ )
150
+
151
+ // 6. Check status after hooks (hooks may mutate responseForHooks.status)
152
+ if (responseForHooks.status < 200 || responseForHooks.status >= 300) {
153
+ throw new ClientRequestError({
154
+ status: responseForHooks.status,
155
+ headers: responseForHooks.headers,
156
+ body: responseForHooks.body,
157
+ procedureName: descriptor.name,
158
+ scope: descriptor.scope,
159
+ })
160
+ }
161
+
162
+ // 7. Return the typed stream
163
+ return createTypedStream<TYield, TReturn>(streamResponse.body, descriptor.streamMode)
164
+ }
@@ -0,0 +1,103 @@
1
+ // ── Adapter ──────────────────────────────────────────────
2
+
3
+ export interface ClientAdapter {
4
+ request(config: AdapterRequest): Promise<AdapterResponse>
5
+ stream(config: AdapterRequest): Promise<AdapterStreamResponse>
6
+ }
7
+
8
+ export interface AdapterRequest {
9
+ url: string
10
+ method: string
11
+ headers?: Record<string, string>
12
+ body?: unknown
13
+ signal?: AbortSignal
14
+ }
15
+
16
+ export interface AdapterResponse {
17
+ status: number
18
+ headers: Record<string, string>
19
+ body: unknown
20
+ }
21
+
22
+ export interface AdapterStreamResponse {
23
+ status: number
24
+ headers: Record<string, string>
25
+ body: AsyncIterable<unknown>
26
+ }
27
+
28
+ // ── Hooks ────────────────────────────────────────────────
29
+
30
+ export interface ClientHooks {
31
+ onBeforeRequest?(context: BeforeRequestContext): BeforeRequestContext | Promise<BeforeRequestContext>
32
+ onAfterResponse?(context: AfterResponseContext): void | Promise<void>
33
+ onError?(context: ErrorContext): void | Promise<void>
34
+ }
35
+
36
+ export interface BeforeRequestContext {
37
+ procedureName: string
38
+ scope: string
39
+ request: AdapterRequest
40
+ }
41
+
42
+ export interface AfterResponseContext {
43
+ procedureName: string
44
+ scope: string
45
+ request: AdapterRequest
46
+ response: AdapterResponse
47
+ }
48
+
49
+ export interface ErrorContext {
50
+ procedureName: string
51
+ scope: string
52
+ request: AdapterRequest
53
+ error: unknown
54
+ }
55
+
56
+ // ── Descriptors ──────────────────────────────────────────
57
+
58
+ export interface CallDescriptor {
59
+ name: string
60
+ scope: string
61
+ path: string
62
+ method: string
63
+ kind: 'rpc' | 'api' | 'stream'
64
+ params: unknown
65
+ }
66
+
67
+ export interface StreamDescriptor extends CallDescriptor {
68
+ kind: 'stream'
69
+ streamMode: 'sse' | 'text'
70
+ }
71
+
72
+ // ── TypedStream ──────────────────────────────────────────
73
+
74
+ export interface TypedStream<TYield, TReturn = void> extends AsyncIterable<TYield> {
75
+ /**
76
+ * Resolves when the stream completes with the final return value.
77
+ * Rejects if the stream errors before completing.
78
+ * Note: iteration must begin (via for-await) for this promise to settle,
79
+ * since resolution depends on the async generator running to completion.
80
+ */
81
+ result: Promise<TReturn>
82
+ }
83
+
84
+ // ── Client Instance ──────────────────────────────────────
85
+
86
+ export type ProcedureCallOptions = ClientHooks
87
+
88
+ export interface ClientInstance {
89
+ basePath: string
90
+ adapter: ClientAdapter
91
+ hooks: ClientHooks
92
+ call<TResponse>(descriptor: CallDescriptor, options?: ProcedureCallOptions): Promise<TResponse>
93
+ stream<TYield, TReturn>(descriptor: StreamDescriptor, options?: ProcedureCallOptions): TypedStream<TYield, TReturn>
94
+ }
95
+
96
+ // ── createClient Config ──────────────────────────────────
97
+
98
+ export interface CreateClientConfig<TScopes> {
99
+ adapter: ClientAdapter
100
+ basePath: string
101
+ scopes: (client: ClientInstance) => TScopes
102
+ hooks?: ClientHooks
103
+ }