ts-procedures 5.5.0 → 5.6.0-beta.1
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 +150 -0
- package/agent_config/claude-code/skills/guide/api-reference.md +177 -0
- package/agent_config/claude-code/skills/guide/patterns.md +107 -0
- package/agent_config/copilot/copilot-instructions.md +80 -0
- package/agent_config/cursor/cursorrules +80 -0
- package/build/client/call.d.ts +14 -0
- package/build/client/call.js +47 -0
- package/build/client/call.js.map +1 -0
- package/build/client/call.test.d.ts +1 -0
- package/build/client/call.test.js +124 -0
- package/build/client/call.test.js.map +1 -0
- package/build/client/errors.d.ts +25 -0
- package/build/client/errors.js +33 -0
- package/build/client/errors.js.map +1 -0
- package/build/client/errors.test.d.ts +1 -0
- package/build/client/errors.test.js +41 -0
- package/build/client/errors.test.js.map +1 -0
- package/build/client/fetch-adapter.d.ts +12 -0
- package/build/client/fetch-adapter.js +156 -0
- package/build/client/fetch-adapter.js.map +1 -0
- package/build/client/fetch-adapter.test.d.ts +1 -0
- package/build/client/fetch-adapter.test.js +271 -0
- package/build/client/fetch-adapter.test.js.map +1 -0
- package/build/client/hooks.d.ts +17 -0
- package/build/client/hooks.js +40 -0
- package/build/client/hooks.js.map +1 -0
- package/build/client/hooks.test.d.ts +1 -0
- package/build/client/hooks.test.js +163 -0
- package/build/client/hooks.test.js.map +1 -0
- package/build/client/index.d.ts +22 -0
- package/build/client/index.js +67 -0
- package/build/client/index.js.map +1 -0
- package/build/client/index.test.d.ts +1 -0
- package/build/client/index.test.js +231 -0
- package/build/client/index.test.js.map +1 -0
- package/build/client/request-builder.d.ts +13 -0
- package/build/client/request-builder.js +53 -0
- package/build/client/request-builder.js.map +1 -0
- package/build/client/request-builder.test.d.ts +1 -0
- package/build/client/request-builder.test.js +160 -0
- package/build/client/request-builder.test.js.map +1 -0
- package/build/client/stream.d.ts +27 -0
- package/build/client/stream.js +118 -0
- package/build/client/stream.js.map +1 -0
- package/build/client/stream.test.d.ts +1 -0
- package/build/client/stream.test.js +228 -0
- package/build/client/stream.test.js.map +1 -0
- package/build/client/types.d.ts +78 -0
- package/build/client/types.js +3 -0
- package/build/client/types.js.map +1 -0
- package/build/codegen/bin/cli.d.ts +17 -0
- package/build/codegen/bin/cli.js +149 -0
- package/build/codegen/bin/cli.js.map +1 -0
- package/build/codegen/bin/cli.test.d.ts +1 -0
- package/build/codegen/bin/cli.test.js +83 -0
- package/build/codegen/bin/cli.test.js.map +1 -0
- package/build/codegen/e2e.test.d.ts +1 -0
- package/build/codegen/e2e.test.js +321 -0
- package/build/codegen/e2e.test.js.map +1 -0
- package/build/codegen/emit-errors.d.ts +9 -0
- package/build/codegen/emit-errors.js +30 -0
- package/build/codegen/emit-errors.js.map +1 -0
- package/build/codegen/emit-errors.test.d.ts +1 -0
- package/build/codegen/emit-errors.test.js +110 -0
- package/build/codegen/emit-errors.test.js.map +1 -0
- package/build/codegen/emit-index.d.ts +6 -0
- package/build/codegen/emit-index.js +49 -0
- package/build/codegen/emit-index.js.map +1 -0
- package/build/codegen/emit-index.test.d.ts +1 -0
- package/build/codegen/emit-index.test.js +83 -0
- package/build/codegen/emit-index.test.js.map +1 -0
- package/build/codegen/emit-scope.d.ts +6 -0
- package/build/codegen/emit-scope.js +194 -0
- package/build/codegen/emit-scope.js.map +1 -0
- package/build/codegen/emit-scope.test.d.ts +1 -0
- package/build/codegen/emit-scope.test.js +276 -0
- package/build/codegen/emit-scope.test.js.map +1 -0
- package/build/codegen/emit-types.d.ts +14 -0
- package/build/codegen/emit-types.js +40 -0
- package/build/codegen/emit-types.js.map +1 -0
- package/build/codegen/emit-types.test.d.ts +1 -0
- package/build/codegen/emit-types.test.js +82 -0
- package/build/codegen/emit-types.test.js.map +1 -0
- package/build/codegen/group-routes.d.ts +23 -0
- package/build/codegen/group-routes.js +46 -0
- package/build/codegen/group-routes.js.map +1 -0
- package/build/codegen/group-routes.test.d.ts +1 -0
- package/build/codegen/group-routes.test.js +131 -0
- package/build/codegen/group-routes.test.js.map +1 -0
- package/build/codegen/index.d.ts +11 -0
- package/build/codegen/index.js +13 -0
- package/build/codegen/index.js.map +1 -0
- package/build/codegen/pipeline.d.ts +14 -0
- package/build/codegen/pipeline.js +49 -0
- package/build/codegen/pipeline.js.map +1 -0
- package/build/codegen/pipeline.test.d.ts +1 -0
- package/build/codegen/pipeline.test.js +151 -0
- package/build/codegen/pipeline.test.js.map +1 -0
- package/build/codegen/resolve-envelope.d.ts +7 -0
- package/build/codegen/resolve-envelope.js +26 -0
- package/build/codegen/resolve-envelope.js.map +1 -0
- package/build/codegen/resolve-envelope.test.d.ts +1 -0
- package/build/codegen/resolve-envelope.test.js +69 -0
- package/build/codegen/resolve-envelope.test.js.map +1 -0
- package/build/implementations/http/doc-registry.test.js +27 -1
- package/build/implementations/http/doc-registry.test.js.map +1 -1
- package/build/implementations/http/express-rpc/index.js +1 -0
- package/build/implementations/http/express-rpc/index.js.map +1 -1
- package/build/implementations/http/express-rpc/index.test.js +1 -1
- package/build/implementations/http/express-rpc/index.test.js.map +1 -1
- package/build/implementations/http/hono-api/index.js +2 -0
- package/build/implementations/http/hono-api/index.js.map +1 -1
- package/build/implementations/http/hono-api/index.test.js +9 -0
- package/build/implementations/http/hono-api/index.test.js.map +1 -1
- package/build/implementations/http/hono-rpc/index.js +1 -0
- package/build/implementations/http/hono-rpc/index.js.map +1 -1
- package/build/implementations/http/hono-rpc/index.test.js +1 -1
- package/build/implementations/http/hono-rpc/index.test.js.map +1 -1
- package/build/implementations/http/hono-stream/index.js +17 -1
- package/build/implementations/http/hono-stream/index.js.map +1 -1
- package/build/implementations/http/hono-stream/index.test.js +61 -0
- package/build/implementations/http/hono-stream/index.test.js.map +1 -1
- package/build/implementations/http/hono-stream/types.d.ts +4 -13
- package/build/implementations/types.d.ts +5 -0
- package/build/index.js +8 -1
- package/build/index.js.map +1 -1
- 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 {};
|