ts-procedures 5.5.0 → 5.6.0-beta.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.
Files changed (127) hide show
  1. package/README.md +150 -0
  2. package/agent_config/claude-code/skills/guide/api-reference.md +177 -0
  3. package/agent_config/claude-code/skills/guide/patterns.md +107 -0
  4. package/agent_config/copilot/copilot-instructions.md +80 -0
  5. package/agent_config/cursor/cursorrules +80 -0
  6. package/build/client/call.d.ts +14 -0
  7. package/build/client/call.js +47 -0
  8. package/build/client/call.js.map +1 -0
  9. package/build/client/call.test.d.ts +1 -0
  10. package/build/client/call.test.js +124 -0
  11. package/build/client/call.test.js.map +1 -0
  12. package/build/client/errors.d.ts +25 -0
  13. package/build/client/errors.js +33 -0
  14. package/build/client/errors.js.map +1 -0
  15. package/build/client/errors.test.d.ts +1 -0
  16. package/build/client/errors.test.js +41 -0
  17. package/build/client/errors.test.js.map +1 -0
  18. package/build/client/fetch-adapter.d.ts +12 -0
  19. package/build/client/fetch-adapter.js +156 -0
  20. package/build/client/fetch-adapter.js.map +1 -0
  21. package/build/client/fetch-adapter.test.d.ts +1 -0
  22. package/build/client/fetch-adapter.test.js +271 -0
  23. package/build/client/fetch-adapter.test.js.map +1 -0
  24. package/build/client/hooks.d.ts +17 -0
  25. package/build/client/hooks.js +40 -0
  26. package/build/client/hooks.js.map +1 -0
  27. package/build/client/hooks.test.d.ts +1 -0
  28. package/build/client/hooks.test.js +163 -0
  29. package/build/client/hooks.test.js.map +1 -0
  30. package/build/client/index.d.ts +22 -0
  31. package/build/client/index.js +67 -0
  32. package/build/client/index.js.map +1 -0
  33. package/build/client/index.test.d.ts +1 -0
  34. package/build/client/index.test.js +231 -0
  35. package/build/client/index.test.js.map +1 -0
  36. package/build/client/request-builder.d.ts +13 -0
  37. package/build/client/request-builder.js +53 -0
  38. package/build/client/request-builder.js.map +1 -0
  39. package/build/client/request-builder.test.d.ts +1 -0
  40. package/build/client/request-builder.test.js +160 -0
  41. package/build/client/request-builder.test.js.map +1 -0
  42. package/build/client/stream.d.ts +27 -0
  43. package/build/client/stream.js +118 -0
  44. package/build/client/stream.js.map +1 -0
  45. package/build/client/stream.test.d.ts +1 -0
  46. package/build/client/stream.test.js +228 -0
  47. package/build/client/stream.test.js.map +1 -0
  48. package/build/client/types.d.ts +78 -0
  49. package/build/client/types.js +3 -0
  50. package/build/client/types.js.map +1 -0
  51. package/build/codegen/bin/cli.d.ts +17 -0
  52. package/build/codegen/bin/cli.js +148 -0
  53. package/build/codegen/bin/cli.js.map +1 -0
  54. package/build/codegen/bin/cli.test.d.ts +1 -0
  55. package/build/codegen/bin/cli.test.js +83 -0
  56. package/build/codegen/bin/cli.test.js.map +1 -0
  57. package/build/codegen/e2e.test.d.ts +1 -0
  58. package/build/codegen/e2e.test.js +321 -0
  59. package/build/codegen/e2e.test.js.map +1 -0
  60. package/build/codegen/emit-errors.d.ts +9 -0
  61. package/build/codegen/emit-errors.js +30 -0
  62. package/build/codegen/emit-errors.js.map +1 -0
  63. package/build/codegen/emit-errors.test.d.ts +1 -0
  64. package/build/codegen/emit-errors.test.js +110 -0
  65. package/build/codegen/emit-errors.test.js.map +1 -0
  66. package/build/codegen/emit-index.d.ts +6 -0
  67. package/build/codegen/emit-index.js +49 -0
  68. package/build/codegen/emit-index.js.map +1 -0
  69. package/build/codegen/emit-index.test.d.ts +1 -0
  70. package/build/codegen/emit-index.test.js +83 -0
  71. package/build/codegen/emit-index.test.js.map +1 -0
  72. package/build/codegen/emit-scope.d.ts +6 -0
  73. package/build/codegen/emit-scope.js +194 -0
  74. package/build/codegen/emit-scope.js.map +1 -0
  75. package/build/codegen/emit-scope.test.d.ts +1 -0
  76. package/build/codegen/emit-scope.test.js +276 -0
  77. package/build/codegen/emit-scope.test.js.map +1 -0
  78. package/build/codegen/emit-types.d.ts +14 -0
  79. package/build/codegen/emit-types.js +40 -0
  80. package/build/codegen/emit-types.js.map +1 -0
  81. package/build/codegen/emit-types.test.d.ts +1 -0
  82. package/build/codegen/emit-types.test.js +82 -0
  83. package/build/codegen/emit-types.test.js.map +1 -0
  84. package/build/codegen/group-routes.d.ts +23 -0
  85. package/build/codegen/group-routes.js +46 -0
  86. package/build/codegen/group-routes.js.map +1 -0
  87. package/build/codegen/group-routes.test.d.ts +1 -0
  88. package/build/codegen/group-routes.test.js +131 -0
  89. package/build/codegen/group-routes.test.js.map +1 -0
  90. package/build/codegen/index.d.ts +11 -0
  91. package/build/codegen/index.js +13 -0
  92. package/build/codegen/index.js.map +1 -0
  93. package/build/codegen/pipeline.d.ts +14 -0
  94. package/build/codegen/pipeline.js +49 -0
  95. package/build/codegen/pipeline.js.map +1 -0
  96. package/build/codegen/pipeline.test.d.ts +1 -0
  97. package/build/codegen/pipeline.test.js +151 -0
  98. package/build/codegen/pipeline.test.js.map +1 -0
  99. package/build/codegen/resolve-envelope.d.ts +7 -0
  100. package/build/codegen/resolve-envelope.js +26 -0
  101. package/build/codegen/resolve-envelope.js.map +1 -0
  102. package/build/codegen/resolve-envelope.test.d.ts +1 -0
  103. package/build/codegen/resolve-envelope.test.js +69 -0
  104. package/build/codegen/resolve-envelope.test.js.map +1 -0
  105. package/build/implementations/http/doc-registry.test.js +27 -1
  106. package/build/implementations/http/doc-registry.test.js.map +1 -1
  107. package/build/implementations/http/express-rpc/index.js +1 -0
  108. package/build/implementations/http/express-rpc/index.js.map +1 -1
  109. package/build/implementations/http/express-rpc/index.test.js +1 -1
  110. package/build/implementations/http/express-rpc/index.test.js.map +1 -1
  111. package/build/implementations/http/hono-api/index.js +2 -0
  112. package/build/implementations/http/hono-api/index.js.map +1 -1
  113. package/build/implementations/http/hono-api/index.test.js +9 -0
  114. package/build/implementations/http/hono-api/index.test.js.map +1 -1
  115. package/build/implementations/http/hono-rpc/index.js +1 -0
  116. package/build/implementations/http/hono-rpc/index.js.map +1 -1
  117. package/build/implementations/http/hono-rpc/index.test.js +1 -1
  118. package/build/implementations/http/hono-rpc/index.test.js.map +1 -1
  119. package/build/implementations/http/hono-stream/index.js +17 -1
  120. package/build/implementations/http/hono-stream/index.js.map +1 -1
  121. package/build/implementations/http/hono-stream/index.test.js +61 -0
  122. package/build/implementations/http/hono-stream/index.test.js.map +1 -1
  123. package/build/implementations/http/hono-stream/types.d.ts +4 -13
  124. package/build/implementations/types.d.ts +5 -0
  125. package/build/index.js +8 -1
  126. package/build/index.js.map +1 -1
  127. package/package.json +12 -2
package/README.md CHANGED
@@ -880,6 +880,156 @@ import type { RPCConfig, RPCHttpRouteDoc, StreamHttpRouteDoc, StreamMode, APICon
880
880
  // Hono API (REST-style)
881
881
  import { HonoAPIAppBuilder } from 'ts-procedures/hono-api'
882
882
  import type { APIConfig, APIHttpRouteDoc, APIInput, HttpMethod, QueryParser } from 'ts-procedures/hono-api'
883
+
884
+ // Client Runtime
885
+ import { createClient, createFetchAdapter } from 'ts-procedures/client'
886
+ import type { ClientAdapter, ClientHooks, TypedStream, ClientInstance } from 'ts-procedures/client'
887
+
888
+ // Code Generation
889
+ import { generateClient } from 'ts-procedures/codegen'
890
+ ```
891
+
892
+ ## Client Code Generation
893
+
894
+ ts-procedures can generate type-safe client SDKs directly from your server's `DocRegistry` output. Generated files include TypeScript types and callable functions for every registered procedure, organized by scope — no manual type duplication required.
895
+
896
+ ### Quick Start
897
+
898
+ **Step 1 — Serve your docs endpoint:**
899
+
900
+ ```typescript
901
+ app.get('/docs', (c) => c.json(docs.toJSON()))
902
+ ```
903
+
904
+ **Step 2 — Generate the client:**
905
+
906
+ ```bash
907
+ npx ts-procedures-codegen --url http://localhost:3000/docs --out ./src/generated/api
908
+ ```
909
+
910
+ **Step 3 — Use the client:**
911
+
912
+ ```typescript
913
+ import { createClient, createFetchAdapter } from 'ts-procedures/client'
914
+ import { createScopeBindings } from './generated/api'
915
+
916
+ const client = createClient({
917
+ adapter: createFetchAdapter(),
918
+ basePath: 'http://localhost:3000',
919
+ scopes: createScopeBindings,
920
+ hooks: {
921
+ onBeforeRequest(ctx) {
922
+ ctx.request.headers = { ...ctx.request.headers, Authorization: `Bearer ${getToken()}` }
923
+ return ctx
924
+ },
925
+ },
926
+ })
927
+
928
+ // Fully typed — params and response inferred from server schemas
929
+ const user = await client.users.GetUser({ pathParams: { id: '123' } })
930
+ ```
931
+
932
+ ### Generated File Structure
933
+
934
+ Running the codegen command produces one file per scope, plus shared error types and a barrel export:
935
+
936
+ ```
937
+ generated/
938
+ users.ts # Types + callables for "users" scope
939
+ billing.ts # Types + callables for "billing" scope
940
+ notifications.ts # Types + callables for stream procedures
941
+ _errors.ts # Typed error classes + ProcedureErrorUnion
942
+ index.ts # Barrel exports + createScopeBindings
943
+ ```
944
+
945
+ ### CLI Reference
946
+
947
+ | Flag | Description | Required |
948
+ |------|-------------|----------|
949
+ | `--url <url>` | Fetch DocEnvelope from URL | One of `--url` or `--file` |
950
+ | `--file <path>` | Read DocEnvelope from JSON file | One of `--url` or `--file` |
951
+ | `--out <dir>` | Output directory | Yes |
952
+ | `--watch` | Poll for changes and regenerate | No |
953
+ | `--interval <ms>` | Watch poll interval (default: 3000) | No |
954
+ | `--dry-run` | Preview without writing files | No |
955
+ | `--client-import-path <path>` | Override import path (default: `ts-procedures/client`) | No |
956
+ | `--enum-style <union\|enum>` | TypeScript enum style | No |
957
+ | `--depluralize` | Singularize array item type names | No |
958
+
959
+ ### Adapter Interface
960
+
961
+ The client requires an adapter that handles the actual HTTP transport. A built-in fetch adapter is included, and you can implement your own for any HTTP library:
962
+
963
+ ```typescript
964
+ import { createFetchAdapter } from 'ts-procedures/client'
965
+
966
+ // Use the built-in fetch adapter
967
+ const adapter = createFetchAdapter({ headers: { 'X-API-Key': 'my-key' } })
968
+
969
+ // Or implement your own (e.g., for axios)
970
+ const axiosAdapter: ClientAdapter = {
971
+ async request({ url, method, headers, body }) {
972
+ const res = await axios({ url, method, headers, data: body })
973
+ return { status: res.status, headers: res.headers, body: res.data }
974
+ },
975
+ async stream({ url, method, headers, body }) {
976
+ // Return AsyncIterable of SSE events
977
+ },
978
+ }
979
+ ```
980
+
981
+ ### Hooks
982
+
983
+ Hooks let you intercept requests and responses globally or per-procedure call. Global hooks apply to every call made through the client instance; per-procedure hooks override or extend them for a single invocation.
984
+
985
+ ```typescript
986
+ // Global hooks (apply to all calls)
987
+ const client = createClient({
988
+ adapter,
989
+ basePath: 'http://localhost:3000',
990
+ scopes: createScopeBindings,
991
+ hooks: {
992
+ onBeforeRequest(ctx) { /* add auth headers */ return ctx },
993
+ onAfterResponse(ctx) { /* handle errors, logging */ },
994
+ onError(ctx) { /* error reporting */ },
995
+ },
996
+ })
997
+
998
+ // Per-procedure hook override
999
+ await client.users.GetUser({ pathParams: { id: '123' } }, {
1000
+ onAfterResponse(ctx) {
1001
+ const rateLimit = ctx.response.headers['x-rate-limit-remaining']
1002
+ },
1003
+ })
1004
+ ```
1005
+
1006
+ ### Streaming
1007
+
1008
+ Stream procedures return a `TypedStream` — an async iterable for yield values, with a `.result` promise for the final return value:
1009
+
1010
+ ```typescript
1011
+ const stream = client.events.WatchNotifications({ filter: 'all' })
1012
+
1013
+ for await (const event of stream) {
1014
+ console.log(event) // typed as WatchNotificationsYield
1015
+ }
1016
+
1017
+ const result = await stream.result // typed as WatchNotificationsReturn
1018
+ ```
1019
+
1020
+ ### Programmatic API
1021
+
1022
+ For build pipelines or custom tooling, `generateClient` can be called directly without the CLI:
1023
+
1024
+ ```typescript
1025
+ import { generateClient } from 'ts-procedures/codegen'
1026
+
1027
+ await generateClient({
1028
+ url: 'http://localhost:3000/docs',
1029
+ outDir: './src/generated/api',
1030
+ clientImportPath: '@my-app/procedures-client', // optional
1031
+ dryRun: false, // optional
1032
+ })
883
1033
  ```
884
1034
 
885
1035
  ## AI Agent Setup
@@ -751,6 +751,183 @@ type AnyHttpRouteDoc = RPCHttpRouteDoc | APIHttpRouteDoc | StreamHttpRouteDoc
751
751
 
752
752
  ---
753
753
 
754
+ ## createClient(config)
755
+
756
+ Creates a typed client instance bound to generated scope bindings.
757
+
758
+ ```typescript
759
+ function createClient<TScopes>(config: {
760
+ adapter: ClientAdapter
761
+ basePath: string
762
+ scopes: (client: ClientInstance) => TScopes
763
+ hooks?: ClientHooks
764
+ }): TScopes
765
+ ```
766
+
767
+ ### Parameters
768
+
769
+ - `config.adapter` — Transport adapter implementing `ClientAdapter`. Use `createFetchAdapter()` for fetch-based transport.
770
+ - `config.basePath` — Base URL prepended to all request paths (e.g., `'http://localhost:3000'`).
771
+ - `config.scopes` — Factory function that receives a raw `ClientInstance` and returns the typed scope object. Typically the `createScopeBindings` export from generated code.
772
+ - `config.hooks` — Optional global hooks applied to every call. Can be overridden per call.
773
+
774
+ ### Return Value
775
+
776
+ Returns the result of `config.scopes(clientInstance)` — a typed object where each key is a scope and each value is a record of callable procedure functions.
777
+
778
+ ### ClientHooks
779
+
780
+ ```typescript
781
+ interface ClientHooks {
782
+ onBeforeRequest?: (ctx: { request: ClientRequest }) => { request: ClientRequest } | void
783
+ onAfterResponse?: (ctx: { request: ClientRequest; response: ClientResponse }) => void
784
+ }
785
+ ```
786
+
787
+ ### Example
788
+
789
+ ```typescript
790
+ import { createClient, createFetchAdapter } from 'ts-procedures/client'
791
+ import { createScopeBindings } from './generated/api'
792
+
793
+ const client = createClient({
794
+ adapter: createFetchAdapter(),
795
+ basePath: 'http://localhost:3000',
796
+ scopes: createScopeBindings,
797
+ hooks: {
798
+ onBeforeRequest(ctx) {
799
+ ctx.request.headers = { ...ctx.request.headers, Authorization: `Bearer ${getToken()}` }
800
+ return ctx
801
+ },
802
+ },
803
+ })
804
+
805
+ const user = await client.users.GetUser({ pathParams: { id: '123' } })
806
+ ```
807
+
808
+ ---
809
+
810
+ ## createFetchAdapter()
811
+
812
+ Creates a `ClientAdapter` using the global `fetch` API.
813
+
814
+ ```typescript
815
+ function createFetchAdapter(options?: {
816
+ fetch?: typeof globalThis.fetch
817
+ }): ClientAdapter
818
+ ```
819
+
820
+ ### Parameters
821
+
822
+ - `options.fetch` — Optional custom fetch implementation. Defaults to `globalThis.fetch`.
823
+
824
+ ### Return Value
825
+
826
+ A `ClientAdapter` that supports both request/response and SSE streaming.
827
+
828
+ ### Example
829
+
830
+ ```typescript
831
+ import { createFetchAdapter } from 'ts-procedures/client'
832
+
833
+ // Default (uses globalThis.fetch)
834
+ const adapter = createFetchAdapter()
835
+
836
+ // Custom fetch (e.g., node-fetch, undici)
837
+ const adapter = createFetchAdapter({ fetch: customFetch })
838
+ ```
839
+
840
+ ---
841
+
842
+ ## generateClient(options)
843
+
844
+ Build-time CLI and programmatic API for generating typed client files from a `DocEnvelope`.
845
+
846
+ ```typescript
847
+ // CLI usage (preferred)
848
+ // npx ts-procedures-codegen --url <url> --out <dir> [--fetch]
849
+
850
+ // Programmatic
851
+ async function generateClient(options: {
852
+ envelope: DocEnvelope
853
+ outDir: string
854
+ }): Promise<void>
855
+ ```
856
+
857
+ ### CLI Options
858
+
859
+ | Flag | Description |
860
+ |------|-------------|
861
+ | `--url <url>` | URL to fetch the `DocEnvelope` JSON from (e.g., `http://localhost:3000/docs`) |
862
+ | `--out <dir>` | Output directory for generated files |
863
+ | `--fetch` | Fetch the envelope at runtime rather than from a local file |
864
+
865
+ ### Generated Output
866
+
867
+ - One `.ts` file per scope (e.g., `users.ts`, `events.ts`)
868
+ - A root `index.ts` exporting `createScopeBindings`
869
+ - Each scope file exports fully-typed callable functions and TypeScript types
870
+
871
+ ### Example
872
+
873
+ ```typescript
874
+ // CLI
875
+ // npx ts-procedures-codegen --url http://localhost:3000/docs --out ./src/generated/api
876
+
877
+ // Programmatic
878
+ import { generateClient } from 'ts-procedures/codegen'
879
+
880
+ await generateClient({
881
+ envelope: await fetch('http://localhost:3000/docs').then(r => r.json()),
882
+ outDir: './src/generated/api',
883
+ })
884
+ ```
885
+
886
+ ---
887
+
888
+ ## TypedStream\<TYield, TReturn\>
889
+
890
+ Async iterable returned by stream procedure calls on the generated client. Yields typed values and exposes a `result` promise for the stream's final return value.
891
+
892
+ ```typescript
893
+ interface TypedStream<TYield, TReturn> extends AsyncIterable<TYield> {
894
+ result: Promise<TReturn>
895
+ [Symbol.asyncIterator](): AsyncIterator<TYield>
896
+ }
897
+ ```
898
+
899
+ ### Type Parameters
900
+
901
+ - `TYield` — Type of each yielded value (from `schema.yieldType`)
902
+ - `TReturn` — Type of the stream's return value (from `schema.returnType`). Sent as `event: 'return'` SSE message by `HonoStreamAppBuilder`.
903
+
904
+ ### Key Behavior
905
+
906
+ - Iterating with `for await...of` yields each SSE data event as `TYield`
907
+ - `stream.result` resolves when the stream closes with the final return value
908
+ - If the stream has no return value, `TReturn` is `void` and `result` resolves to `undefined`
909
+
910
+ ### Example
911
+
912
+ ```typescript
913
+ import { createClient, createFetchAdapter } from 'ts-procedures/client'
914
+ import { createScopeBindings } from './generated/api'
915
+
916
+ const client = createClient({ adapter: createFetchAdapter(), basePath: '...', scopes: createScopeBindings })
917
+
918
+ const stream = client.events.WatchNotifications({ filter: 'all' })
919
+
920
+ // Consume yielded values
921
+ for await (const event of stream) {
922
+ console.log(event) // Typed as WatchNotificationsYield
923
+ }
924
+
925
+ // Access return value after iteration completes
926
+ const summary = await stream.result // Typed as WatchNotificationsReturn
927
+ ```
928
+
929
+ ---
930
+
754
931
  ## Stack Utilities
755
932
 
756
933
  ### captureDefinitionInfo()
@@ -758,3 +758,110 @@ onRequestStart → factoryContext() → params validation
758
758
  → onStreamStart → handler yields → onStreamEnd → onRequestEnd
759
759
  → onMidStreamError (if throw) → onStreamEnd → onRequestEnd
760
760
  ```
761
+
762
+ ---
763
+
764
+ ## Client Code Generation Setup
765
+
766
+ ```typescript
767
+ import { DocRegistry } from 'ts-procedures/http-docs'
768
+
769
+ // Server: serve docs endpoint
770
+ const docs = new DocRegistry({
771
+ basePath: '/api',
772
+ headers: [{ name: 'Authorization', description: 'Bearer token' }],
773
+ errors: DocRegistry.defaultErrors(),
774
+ })
775
+ .from(rpcBuilder)
776
+ .from(apiBuilder)
777
+ .from(streamBuilder)
778
+
779
+ app.get('/docs', (c) => c.json(docs.toJSON()))
780
+
781
+ // Then generate client:
782
+ // npx ts-procedures-codegen --url http://localhost:3000/docs --out ./src/generated/api
783
+ ```
784
+
785
+ ---
786
+
787
+ ## Type-Safe Client Usage
788
+
789
+ ```typescript
790
+ import { createClient, createFetchAdapter } from 'ts-procedures/client'
791
+ import { createScopeBindings } from './generated/api'
792
+
793
+ const client = createClient({
794
+ adapter: createFetchAdapter(),
795
+ basePath: 'http://localhost:3000',
796
+ scopes: createScopeBindings,
797
+ hooks: {
798
+ onBeforeRequest(ctx) {
799
+ ctx.request.headers = {
800
+ ...ctx.request.headers,
801
+ Authorization: `Bearer ${getToken()}`,
802
+ }
803
+ return ctx
804
+ },
805
+ onAfterResponse(ctx) {
806
+ if (ctx.response.status === 401) {
807
+ redirect('/login')
808
+ }
809
+ },
810
+ },
811
+ })
812
+
813
+ // Fully typed — params and response inferred from server schemas
814
+ const user = await client.users.GetUser({ pathParams: { id: '123' } })
815
+ ```
816
+
817
+ ---
818
+
819
+ ## Streaming Client Usage
820
+
821
+ ```typescript
822
+ const stream = client.events.WatchNotifications({ filter: 'all' })
823
+
824
+ for await (const event of stream) {
825
+ console.log(event) // Typed as WatchNotificationsYield
826
+ }
827
+
828
+ // Access the stream's return value
829
+ const result = await stream.result // Typed as WatchNotificationsReturn
830
+ ```
831
+
832
+ ---
833
+
834
+ ## Per-Procedure Hook Override
835
+
836
+ ```typescript
837
+ // Override hooks for a specific call
838
+ await client.users.GetUser({ pathParams: { id: '123' } }, {
839
+ onAfterResponse(ctx) {
840
+ const rateLimit = ctx.response.headers['x-rate-limit-remaining']
841
+ console.log(`Rate limit remaining: ${rateLimit}`)
842
+ },
843
+ })
844
+ ```
845
+
846
+ ---
847
+
848
+ ## Custom Client Adapter (Axios)
849
+
850
+ ```typescript
851
+ import type { ClientAdapter } from 'ts-procedures/client'
852
+ import axios from 'axios'
853
+
854
+ const axiosAdapter: ClientAdapter = {
855
+ async request({ url, method, headers, body, signal }) {
856
+ const res = await axios({ url, method, headers, data: body, signal })
857
+ return {
858
+ status: res.status,
859
+ headers: Object.fromEntries(Object.entries(res.headers).filter(([, v]) => typeof v === 'string')) as Record<string, string>,
860
+ body: res.data,
861
+ }
862
+ },
863
+ async stream() {
864
+ throw new Error('Streaming not supported with axios adapter')
865
+ },
866
+ }
867
+ ```
@@ -292,3 +292,83 @@ describe('GetUser', () => {
292
292
  - `scope` can be string or string[] (joined as path segments)
293
293
  - Procedure name auto-converted to kebab-case
294
294
  - Stream routes support both GET (query params) and POST (JSON body)
295
+
296
+ ## Client Code Generation
297
+
298
+ Generate type-safe client SDKs from a running DocRegistry endpoint.
299
+
300
+ ### Imports
301
+
302
+ ```typescript
303
+ // Client Runtime
304
+ import { createClient, createFetchAdapter } from 'ts-procedures/client'
305
+ import type { ClientAdapter, ClientHooks, TypedStream, ClientInstance } from 'ts-procedures/client'
306
+
307
+ // Code Generation (build-time only)
308
+ import { generateClient } from 'ts-procedures/codegen'
309
+ ```
310
+
311
+ ### CLI
312
+
313
+ ```bash
314
+ npx ts-procedures-codegen --url http://localhost:3000/docs --out ./src/generated/api
315
+ ```
316
+
317
+ Generates one `.ts` file per scope plus a root `index.ts` exporting `createScopeBindings`.
318
+
319
+ ### createClient Example
320
+
321
+ ```typescript
322
+ import { createClient, createFetchAdapter } from 'ts-procedures/client'
323
+ import { createScopeBindings } from './generated/api'
324
+
325
+ const client = createClient({
326
+ adapter: createFetchAdapter(),
327
+ basePath: 'http://localhost:3000',
328
+ scopes: createScopeBindings,
329
+ hooks: {
330
+ onBeforeRequest(ctx) {
331
+ ctx.request.headers = { ...ctx.request.headers, Authorization: `Bearer ${getToken()}` }
332
+ return ctx
333
+ },
334
+ onAfterResponse(ctx) {
335
+ if (ctx.response.status === 401) redirect('/login')
336
+ },
337
+ },
338
+ })
339
+
340
+ // Fully typed — params and response inferred from server schemas
341
+ const user = await client.users.GetUser({ pathParams: { id: '123' } })
342
+
343
+ // Per-call hook override
344
+ await client.users.GetUser({ pathParams: { id: '123' } }, {
345
+ onAfterResponse(ctx) {
346
+ console.log(ctx.response.headers['x-rate-limit-remaining'])
347
+ },
348
+ })
349
+ ```
350
+
351
+ ### Hook Types
352
+
353
+ ```typescript
354
+ interface ClientHooks {
355
+ onBeforeRequest?: (ctx: { request: ClientRequest }) => { request: ClientRequest } | void
356
+ onAfterResponse?: (ctx: { request: ClientRequest; response: ClientResponse }) => void
357
+ }
358
+ ```
359
+
360
+ ### TypedStream Usage
361
+
362
+ ```typescript
363
+ const stream = client.events.WatchNotifications({ filter: 'all' })
364
+
365
+ for await (const event of stream) {
366
+ console.log(event) // Typed as WatchNotificationsYield
367
+ }
368
+
369
+ const result = await stream.result // Typed as WatchNotificationsReturn
370
+ ```
371
+
372
+ - `TypedStream<TYield, TReturn>` extends `AsyncIterable<TYield>`
373
+ - `stream.result` resolves with the final return value sent as `event: 'return'` SSE message
374
+ - Stream procedures that return `void` have `result` resolve to `undefined`
@@ -292,3 +292,83 @@ describe('GetUser', () => {
292
292
  - `scope` can be string or string[] (joined as path segments)
293
293
  - Procedure name auto-converted to kebab-case
294
294
  - Stream routes support both GET (query params) and POST (JSON body)
295
+
296
+ ## Client Code Generation
297
+
298
+ Generate type-safe client SDKs from a running DocRegistry endpoint.
299
+
300
+ ### Imports
301
+
302
+ ```typescript
303
+ // Client Runtime
304
+ import { createClient, createFetchAdapter } from 'ts-procedures/client'
305
+ import type { ClientAdapter, ClientHooks, TypedStream, ClientInstance } from 'ts-procedures/client'
306
+
307
+ // Code Generation (build-time only)
308
+ import { generateClient } from 'ts-procedures/codegen'
309
+ ```
310
+
311
+ ### CLI
312
+
313
+ ```bash
314
+ npx ts-procedures-codegen --url http://localhost:3000/docs --out ./src/generated/api
315
+ ```
316
+
317
+ Generates one `.ts` file per scope plus a root `index.ts` exporting `createScopeBindings`.
318
+
319
+ ### createClient Example
320
+
321
+ ```typescript
322
+ import { createClient, createFetchAdapter } from 'ts-procedures/client'
323
+ import { createScopeBindings } from './generated/api'
324
+
325
+ const client = createClient({
326
+ adapter: createFetchAdapter(),
327
+ basePath: 'http://localhost:3000',
328
+ scopes: createScopeBindings,
329
+ hooks: {
330
+ onBeforeRequest(ctx) {
331
+ ctx.request.headers = { ...ctx.request.headers, Authorization: `Bearer ${getToken()}` }
332
+ return ctx
333
+ },
334
+ onAfterResponse(ctx) {
335
+ if (ctx.response.status === 401) redirect('/login')
336
+ },
337
+ },
338
+ })
339
+
340
+ // Fully typed — params and response inferred from server schemas
341
+ const user = await client.users.GetUser({ pathParams: { id: '123' } })
342
+
343
+ // Per-call hook override
344
+ await client.users.GetUser({ pathParams: { id: '123' } }, {
345
+ onAfterResponse(ctx) {
346
+ console.log(ctx.response.headers['x-rate-limit-remaining'])
347
+ },
348
+ })
349
+ ```
350
+
351
+ ### Hook Types
352
+
353
+ ```typescript
354
+ interface ClientHooks {
355
+ onBeforeRequest?: (ctx: { request: ClientRequest }) => { request: ClientRequest } | void
356
+ onAfterResponse?: (ctx: { request: ClientRequest; response: ClientResponse }) => void
357
+ }
358
+ ```
359
+
360
+ ### TypedStream Usage
361
+
362
+ ```typescript
363
+ const stream = client.events.WatchNotifications({ filter: 'all' })
364
+
365
+ for await (const event of stream) {
366
+ console.log(event) // Typed as WatchNotificationsYield
367
+ }
368
+
369
+ const result = await stream.result // Typed as WatchNotificationsReturn
370
+ ```
371
+
372
+ - `TypedStream<TYield, TReturn>` extends `AsyncIterable<TYield>`
373
+ - `stream.result` resolves with the final return value sent as `event: 'return'` SSE message
374
+ - Stream procedures that return `void` have `result` resolve to `undefined`
@@ -0,0 +1,14 @@
1
+ import type { ClientAdapter, ClientHooks, CallDescriptor } from './types.js';
2
+ /**
3
+ * Executes a single procedure call through the adapter.
4
+ *
5
+ * Flow:
6
+ * 1. Build AdapterRequest from descriptor
7
+ * 2. Run onBeforeRequest hooks (global then local)
8
+ * 3. Call adapter.request()
9
+ * 4. On adapter error: run onError hooks, re-throw
10
+ * 5. Run onAfterResponse hooks (hooks may mutate response.status)
11
+ * 6. If response status is non-2xx: throw ClientRequestError
12
+ * 7. Return response.body as TResponse
13
+ */
14
+ export declare function executeCall<TResponse>(descriptor: CallDescriptor, basePath: string, adapter: ClientAdapter, globalHooks: ClientHooks, localHooks: ClientHooks | undefined): Promise<TResponse>;
@@ -0,0 +1,47 @@
1
+ import { buildAdapterRequest } from './request-builder.js';
2
+ import { runBeforeRequest, runAfterResponse, runOnError } from './hooks.js';
3
+ import { ClientRequestError } from './errors.js';
4
+ /**
5
+ * Executes a single procedure call through the adapter.
6
+ *
7
+ * Flow:
8
+ * 1. Build AdapterRequest from descriptor
9
+ * 2. Run onBeforeRequest hooks (global then local)
10
+ * 3. Call adapter.request()
11
+ * 4. On adapter error: run onError hooks, re-throw
12
+ * 5. Run onAfterResponse hooks (hooks may mutate response.status)
13
+ * 6. If response status is non-2xx: throw ClientRequestError
14
+ * 7. Return response.body as TResponse
15
+ */
16
+ export async function executeCall(descriptor, basePath, adapter, globalHooks, localHooks) {
17
+ // 1. Build the initial request
18
+ let request = buildAdapterRequest(descriptor, basePath);
19
+ // 2. Run before-request hooks — they may mutate the request
20
+ const beforeCtx = await runBeforeRequest({ procedureName: descriptor.name, scope: descriptor.scope, request }, globalHooks, localHooks);
21
+ request = beforeCtx.request;
22
+ // 3. Call the adapter
23
+ let response;
24
+ try {
25
+ response = await adapter.request(request);
26
+ }
27
+ catch (err) {
28
+ // 4. On adapter error: run error hooks, re-throw
29
+ await runOnError({ procedureName: descriptor.name, scope: descriptor.scope, request, error: err }, globalHooks, localHooks);
30
+ throw err;
31
+ }
32
+ // 5. Run after-response hooks — they may mutate response.status to swallow errors
33
+ await runAfterResponse({ procedureName: descriptor.name, scope: descriptor.scope, request, response }, globalHooks, localHooks);
34
+ // 6. Check status AFTER hooks (hooks may have swallowed the error by mutating status)
35
+ if (response.status < 200 || response.status >= 300) {
36
+ throw new ClientRequestError({
37
+ status: response.status,
38
+ headers: response.headers,
39
+ body: response.body,
40
+ procedureName: descriptor.name,
41
+ scope: descriptor.scope,
42
+ });
43
+ }
44
+ // 7. Return the body
45
+ return response.body;
46
+ }
47
+ //# sourceMappingURL=call.js.map
@@ -0,0 +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;AAOhD;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,UAA0B,EAC1B,QAAgB,EAChB,OAAsB,EACtB,WAAwB,EACxB,UAAmC;IAEnC,+BAA+B;IAC/B,IAAI,OAAO,GAAG,mBAAmB,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAA;IAEvD,4DAA4D;IAC5D,MAAM,SAAS,GAAG,MAAM,gBAAgB,CACtC,EAAE,aAAa,EAAE,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,UAAU,CAAC,KAAK,EAAE,OAAO,EAAE,EACpE,WAAW,EACX,UAAU,CACX,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,WAAW,EACX,UAAU,CACX,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,WAAW,EACX,UAAU,CACX,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"}
@@ -0,0 +1 @@
1
+ export {};