@richie-rpc/core 1.2.1 → 1.2.3
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 +161 -11
- package/dist/cjs/index.cjs +73 -1
- package/dist/cjs/index.cjs.map +3 -3
- package/dist/cjs/package.json +1 -1
- package/dist/mjs/index.mjs +56 -1
- package/dist/mjs/index.mjs.map +3 -3
- package/dist/mjs/package.json +1 -1
- package/dist/types/index.d.ts +81 -7
- package/dist/types/websocket.d.ts +66 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,17 +23,17 @@ const contract = defineContract({
|
|
|
23
23
|
params: z.object({ id: z.string() }),
|
|
24
24
|
responses: {
|
|
25
25
|
[Status.OK]: z.object({ id: z.string(), name: z.string() }),
|
|
26
|
-
[Status.NotFound]: z.object({ error: z.string() })
|
|
27
|
-
}
|
|
26
|
+
[Status.NotFound]: z.object({ error: z.string() }),
|
|
27
|
+
},
|
|
28
28
|
},
|
|
29
29
|
createUser: {
|
|
30
30
|
method: 'POST',
|
|
31
31
|
path: '/users',
|
|
32
32
|
body: z.object({ name: z.string(), email: z.string().email() }),
|
|
33
33
|
responses: {
|
|
34
|
-
[Status.Created]: z.object({ id: z.string(), name: z.string(), email: z.string() })
|
|
35
|
-
}
|
|
36
|
-
}
|
|
34
|
+
[Status.Created]: z.object({ id: z.string(), name: z.string(), email: z.string() }),
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
37
|
});
|
|
38
38
|
```
|
|
39
39
|
|
|
@@ -47,17 +47,102 @@ Each endpoint can have:
|
|
|
47
47
|
- `query` (optional): Zod schema for query parameters
|
|
48
48
|
- `headers` (optional): Zod schema for request headers
|
|
49
49
|
- `body` (optional): Zod schema for request body
|
|
50
|
+
- `contentType` (optional): Request content type (`'application/json'` or `'multipart/form-data'`)
|
|
50
51
|
- `responses` (required): Object mapping status codes to Zod schemas
|
|
51
52
|
|
|
53
|
+
### Streaming Endpoint
|
|
54
|
+
|
|
55
|
+
For AI-style streaming responses using NDJSON:
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
const contract = defineContract({
|
|
59
|
+
generateText: {
|
|
60
|
+
type: 'streaming',
|
|
61
|
+
method: 'POST',
|
|
62
|
+
path: '/generate',
|
|
63
|
+
body: z.object({ prompt: z.string() }),
|
|
64
|
+
chunk: z.object({ text: z.string() }),
|
|
65
|
+
finalResponse: z.object({ totalTokens: z.number() }),
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
- `type: 'streaming'` (required): Marks this as a streaming endpoint
|
|
71
|
+
- `method` (required): Must be `'POST'`
|
|
72
|
+
- `chunk` (required): Zod schema for each streamed chunk
|
|
73
|
+
- `finalResponse` (optional): Zod schema for the final response after stream ends
|
|
74
|
+
|
|
75
|
+
### SSE Endpoint
|
|
76
|
+
|
|
77
|
+
For server-to-client event streaming:
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
const contract = defineContract({
|
|
81
|
+
notifications: {
|
|
82
|
+
type: 'sse',
|
|
83
|
+
method: 'GET',
|
|
84
|
+
path: '/notifications',
|
|
85
|
+
query: z.object({ userId: z.string() }),
|
|
86
|
+
events: {
|
|
87
|
+
message: z.object({ text: z.string(), timestamp: z.string() }),
|
|
88
|
+
userJoined: z.object({ userId: z.string() }),
|
|
89
|
+
heartbeat: z.object({ timestamp: z.string() }),
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
- `type: 'sse'` (required): Marks this as an SSE endpoint
|
|
96
|
+
- `method` (required): Must be `'GET'`
|
|
97
|
+
- `events` (required): Object mapping event names to Zod schemas
|
|
98
|
+
|
|
99
|
+
### WebSocket Contract
|
|
100
|
+
|
|
101
|
+
For bidirectional real-time communication, use `defineWebSocketContract`:
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
import { defineWebSocketContract } from '@richie-rpc/core';
|
|
105
|
+
|
|
106
|
+
const wsContract = defineWebSocketContract({
|
|
107
|
+
chat: {
|
|
108
|
+
path: '/ws/chat/:roomId',
|
|
109
|
+
params: z.object({ roomId: z.string() }),
|
|
110
|
+
query: z.object({ token: z.string().optional() }),
|
|
111
|
+
clientMessages: {
|
|
112
|
+
sendMessage: { payload: z.object({ text: z.string() }) },
|
|
113
|
+
typing: { payload: z.object({ isTyping: z.boolean() }) },
|
|
114
|
+
},
|
|
115
|
+
serverMessages: {
|
|
116
|
+
message: { payload: z.object({ userId: z.string(), text: z.string() }) },
|
|
117
|
+
userTyping: { payload: z.object({ userId: z.string(), isTyping: z.boolean() }) },
|
|
118
|
+
error: { payload: z.object({ code: z.string(), message: z.string() }) },
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
- `path` (required): WebSocket endpoint path with optional parameters
|
|
125
|
+
- `params` (optional): Zod schema for path parameters
|
|
126
|
+
- `query` (optional): Zod schema for query parameters
|
|
127
|
+
- `clientMessages` (required): Messages the client can send to the server
|
|
128
|
+
- `serverMessages` (required): Messages the server can send to the client
|
|
129
|
+
|
|
130
|
+
Each message type has a `payload` field with a Zod schema for validation.
|
|
131
|
+
|
|
52
132
|
## Features
|
|
53
133
|
|
|
54
134
|
- ✅ Type-safe contract definitions
|
|
55
135
|
- ✅ Zod v4+ schema validation
|
|
136
|
+
- ✅ HTTP Streaming endpoints (NDJSON)
|
|
137
|
+
- ✅ Server-Sent Events (SSE) endpoints
|
|
138
|
+
- ✅ WebSocket contracts with typed messages
|
|
56
139
|
- ✅ Path parameter parsing and interpolation
|
|
57
140
|
- ✅ Query parameter handling
|
|
58
141
|
- ✅ Multiple response types per endpoint
|
|
59
142
|
- ✅ Full TypeScript inference
|
|
60
143
|
- ✅ Status code constants for cleaner code
|
|
144
|
+
- ✅ File uploads with `multipart/form-data` support
|
|
145
|
+
- ✅ Nested file structures in request bodies
|
|
61
146
|
|
|
62
147
|
## Utilities
|
|
63
148
|
|
|
@@ -94,6 +179,71 @@ buildUrl('http://api.example.com/api', '/users');
|
|
|
94
179
|
|
|
95
180
|
The `buildUrl` function properly concatenates the baseUrl with the path, supporting basePath prefixes in the baseUrl.
|
|
96
181
|
|
|
182
|
+
### File Upload / FormData Utilities
|
|
183
|
+
|
|
184
|
+
The core package provides utilities for handling `multipart/form-data` requests with nested file structures.
|
|
185
|
+
|
|
186
|
+
#### Defining File Upload Endpoints
|
|
187
|
+
|
|
188
|
+
Use `contentType: 'multipart/form-data'` and `z.instanceof(File)` in your body schema:
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
import { defineContract, Status } from '@richie-rpc/core';
|
|
192
|
+
import { z } from 'zod';
|
|
193
|
+
|
|
194
|
+
const contract = defineContract({
|
|
195
|
+
uploadDocuments: {
|
|
196
|
+
method: 'POST',
|
|
197
|
+
path: '/upload',
|
|
198
|
+
contentType: 'multipart/form-data',
|
|
199
|
+
body: z.object({
|
|
200
|
+
documents: z.array(
|
|
201
|
+
z.object({
|
|
202
|
+
file: z.instanceof(File),
|
|
203
|
+
name: z.string(),
|
|
204
|
+
tags: z.array(z.string()).optional(),
|
|
205
|
+
}),
|
|
206
|
+
),
|
|
207
|
+
category: z.string(),
|
|
208
|
+
}),
|
|
209
|
+
responses: {
|
|
210
|
+
[Status.Created]: z.object({
|
|
211
|
+
uploadedCount: z.number(),
|
|
212
|
+
filenames: z.array(z.string()),
|
|
213
|
+
}),
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
#### How It Works
|
|
220
|
+
|
|
221
|
+
FormData is inherently flat, but Richie RPC supports nested structures using a hybrid JSON + Files approach:
|
|
222
|
+
|
|
223
|
+
1. **Client-side**: Files are extracted from the object and replaced with `{ __fileRef__: "path" }` placeholders. The JSON structure is sent as `__json__` and files are sent as separate FormData entries.
|
|
224
|
+
|
|
225
|
+
2. **Server-side**: The JSON is parsed, and `__fileRef__` placeholders are replaced with actual File objects from the FormData.
|
|
226
|
+
|
|
227
|
+
#### Utility Functions
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
import { objectToFormData, formDataToObject } from '@richie-rpc/core';
|
|
231
|
+
|
|
232
|
+
// Client-side: Convert object with Files to FormData
|
|
233
|
+
const formData = objectToFormData({
|
|
234
|
+
documents: [
|
|
235
|
+
{ file: file1, name: 'doc1.pdf' },
|
|
236
|
+
{ file: file2, name: 'doc2.pdf' },
|
|
237
|
+
],
|
|
238
|
+
category: 'reports',
|
|
239
|
+
});
|
|
240
|
+
// Result: FormData with __json__ + files at "documents.0.file", "documents.1.file"
|
|
241
|
+
|
|
242
|
+
// Server-side: Convert FormData back to object with Files
|
|
243
|
+
const obj = formDataToObject(formData);
|
|
244
|
+
// Result: { documents: [{ file: File, name: 'doc1.pdf' }, ...], category: 'reports' }
|
|
245
|
+
```
|
|
246
|
+
|
|
97
247
|
## Status Codes
|
|
98
248
|
|
|
99
249
|
Use the `Status` const object for type-safe status codes:
|
|
@@ -108,9 +258,9 @@ const contract = defineContract({
|
|
|
108
258
|
params: z.object({ id: z.string() }),
|
|
109
259
|
responses: {
|
|
110
260
|
[Status.OK]: UserSchema,
|
|
111
|
-
[Status.NotFound]: ErrorSchema
|
|
112
|
-
}
|
|
113
|
-
}
|
|
261
|
+
[Status.NotFound]: ErrorSchema,
|
|
262
|
+
},
|
|
263
|
+
},
|
|
114
264
|
});
|
|
115
265
|
```
|
|
116
266
|
|
|
@@ -122,6 +272,7 @@ return { status: Status.NotFound, body: { error: 'Not found' } };
|
|
|
122
272
|
```
|
|
123
273
|
|
|
124
274
|
**Available constants:**
|
|
275
|
+
|
|
125
276
|
- Success: `OK` (200), `Created` (201), `Accepted` (202), `NoContent` (204)
|
|
126
277
|
- Client Errors: `BadRequest` (400), `Unauthorized` (401), `Forbidden` (403), `NotFound` (404), `Conflict` (409), `UnprocessableEntity` (422), `TooManyRequests` (429)
|
|
127
278
|
- Server Errors: `InternalServerError` (500), `BadGateway` (502), `ServiceUnavailable` (503)
|
|
@@ -139,8 +290,8 @@ const contract = defineContract({
|
|
|
139
290
|
[Status.OK]: z.object({ message: z.string() }),
|
|
140
291
|
418: z.object({ message: z.string() }), // I'm a teapot
|
|
141
292
|
451: z.object({ reason: z.string() }), // Unavailable for legal reasons
|
|
142
|
-
}
|
|
143
|
-
}
|
|
293
|
+
},
|
|
294
|
+
},
|
|
144
295
|
});
|
|
145
296
|
```
|
|
146
297
|
|
|
@@ -169,4 +320,3 @@ The package exports several utility types for extracting types from endpoint def
|
|
|
169
320
|
## License
|
|
170
321
|
|
|
171
322
|
MIT
|
|
172
|
-
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -3,6 +3,23 @@
|
|
|
3
3
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
5
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __reExport = (target, mod, secondTarget) => {
|
|
7
|
+
for (let key of __getOwnPropNames(mod))
|
|
8
|
+
if (!__hasOwnProp.call(target, key) && key !== "default")
|
|
9
|
+
__defProp(target, key, {
|
|
10
|
+
get: () => mod[key],
|
|
11
|
+
enumerable: true
|
|
12
|
+
});
|
|
13
|
+
if (secondTarget) {
|
|
14
|
+
for (let key of __getOwnPropNames(mod))
|
|
15
|
+
if (!__hasOwnProp.call(secondTarget, key) && key !== "default")
|
|
16
|
+
__defProp(secondTarget, key, {
|
|
17
|
+
get: () => mod[key],
|
|
18
|
+
enumerable: true
|
|
19
|
+
});
|
|
20
|
+
return secondTarget;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
6
23
|
var __moduleCache = /* @__PURE__ */ new WeakMap;
|
|
7
24
|
var __toCommonJS = (from) => {
|
|
8
25
|
var entry = __moduleCache.get(from), desc;
|
|
@@ -32,13 +49,16 @@ var exports_core = {};
|
|
|
32
49
|
__export(exports_core, {
|
|
33
50
|
parseQuery: () => parseQuery,
|
|
34
51
|
parsePathParams: () => parsePathParams,
|
|
52
|
+
objectToFormData: () => objectToFormData,
|
|
35
53
|
matchPath: () => matchPath,
|
|
36
54
|
interpolatePath: () => interpolatePath,
|
|
55
|
+
formDataToObject: () => formDataToObject,
|
|
37
56
|
defineContract: () => defineContract,
|
|
38
57
|
buildUrl: () => buildUrl,
|
|
39
58
|
Status: () => Status
|
|
40
59
|
});
|
|
41
60
|
module.exports = __toCommonJS(exports_core);
|
|
61
|
+
__reExport(exports_core, require("./websocket.cjs"), module.exports);
|
|
42
62
|
var Status = {
|
|
43
63
|
OK: 200,
|
|
44
64
|
Created: 201,
|
|
@@ -126,6 +146,58 @@ function parseQuery(searchParams) {
|
|
|
126
146
|
function defineContract(contract) {
|
|
127
147
|
return contract;
|
|
128
148
|
}
|
|
149
|
+
function objectToFormData(obj) {
|
|
150
|
+
const formData = new FormData;
|
|
151
|
+
const files = [];
|
|
152
|
+
function traverse(value, path) {
|
|
153
|
+
if (value instanceof File) {
|
|
154
|
+
files.push({ path, file: value });
|
|
155
|
+
return { __fileRef__: path };
|
|
156
|
+
}
|
|
157
|
+
if (Array.isArray(value)) {
|
|
158
|
+
return value.map((item, i) => traverse(item, `${path}.${i}`));
|
|
159
|
+
}
|
|
160
|
+
if (value && typeof value === "object") {
|
|
161
|
+
const result = {};
|
|
162
|
+
for (const [k, v] of Object.entries(value)) {
|
|
163
|
+
result[k] = traverse(v, path ? `${path}.${k}` : k);
|
|
164
|
+
}
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
return value;
|
|
168
|
+
}
|
|
169
|
+
const jsonWithRefs = traverse(obj, "");
|
|
170
|
+
formData.append("__json__", JSON.stringify(jsonWithRefs));
|
|
171
|
+
for (const { path, file } of files) {
|
|
172
|
+
formData.append(path, file);
|
|
173
|
+
}
|
|
174
|
+
return formData;
|
|
175
|
+
}
|
|
176
|
+
function formDataToObject(formData) {
|
|
177
|
+
const jsonStr = formData.get("__json__");
|
|
178
|
+
if (typeof jsonStr !== "string") {
|
|
179
|
+
return Object.fromEntries(formData.entries());
|
|
180
|
+
}
|
|
181
|
+
const obj = JSON.parse(jsonStr);
|
|
182
|
+
function replaceRefs(value) {
|
|
183
|
+
if (value && typeof value === "object" && "__fileRef__" in value) {
|
|
184
|
+
const path = value.__fileRef__;
|
|
185
|
+
return formData.get(path);
|
|
186
|
+
}
|
|
187
|
+
if (Array.isArray(value)) {
|
|
188
|
+
return value.map(replaceRefs);
|
|
189
|
+
}
|
|
190
|
+
if (value && typeof value === "object") {
|
|
191
|
+
const result = {};
|
|
192
|
+
for (const [k, v] of Object.entries(value)) {
|
|
193
|
+
result[k] = replaceRefs(v);
|
|
194
|
+
}
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
return value;
|
|
198
|
+
}
|
|
199
|
+
return replaceRefs(obj);
|
|
200
|
+
}
|
|
129
201
|
})
|
|
130
202
|
|
|
131
|
-
//# debugId=
|
|
203
|
+
//# debugId=7092E09755013A0164756E2164756E21
|
package/dist/cjs/index.cjs.map
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../index.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"import type { z } from 'zod';\n\n// HTTP methods supported\nexport type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';\n\n// HTTP status codes as const object for type-safe responses without 'as const'\nexport const Status = {\n // Success responses\n OK: 200 as const,\n Created: 201 as const,\n Accepted: 202 as const,\n NoContent: 204 as const,\n\n // Redirection\n MovedPermanently: 301 as const,\n Found: 302 as const,\n NotModified: 304 as const,\n\n // Client errors\n BadRequest: 400 as const,\n Unauthorized: 401 as const,\n Forbidden: 403 as const,\n NotFound: 404 as const,\n MethodNotAllowed: 405 as const,\n Conflict: 409 as const,\n UnprocessableEntity: 422 as const,\n TooManyRequests: 429 as const,\n\n // Server errors\n InternalServerError: 500 as const,\n NotImplemented: 501 as const,\n BadGateway: 502 as const,\n ServiceUnavailable: 503 as const,\n GatewayTimeout: 504 as const,\n} as const;\n\n// Endpoint definition structure\nexport interface EndpointDefinition {\n method: HttpMethod;\n path: string;\n params?: z.ZodTypeAny;\n query?: z.ZodTypeAny;\n headers?: z.ZodTypeAny;\n body?: z.ZodTypeAny;\n responses: Record<number, z.ZodTypeAny>;\n}\n\n// Contract is a collection of named endpoints\nexport type Contract = Record<string, EndpointDefinition>;\n\n// Extract the Zod type from a schema\nexport type InferZodType<T> = T extends z.ZodTypeAny ? z.infer<T> : never;\n\n// Extract params type from endpoint\nexport type ExtractParams<T extends EndpointDefinition> = T['params'] extends z.ZodTypeAny\n ? InferZodType<T['params']>\n : never;\n\n// Extract query type from endpoint\nexport type ExtractQuery<T extends EndpointDefinition> = T['query'] extends z.ZodTypeAny\n ? InferZodType<T['query']>\n : never;\n\n// Extract headers type from endpoint\nexport type ExtractHeaders<T extends EndpointDefinition> = T['headers'] extends z.ZodTypeAny\n ? InferZodType<T['headers']>\n : never;\n\n// Extract body type from endpoint\nexport type ExtractBody<T extends EndpointDefinition> = T['body'] extends z.ZodTypeAny\n ? InferZodType<T['body']>\n : never;\n\n// Extract response types for all status codes\nexport type ExtractResponses<T extends EndpointDefinition> = {\n [K in keyof T['responses']]: T['responses'][K] extends z.ZodTypeAny\n ? InferZodType<T['responses'][K]>\n : never;\n};\n\n// Extract a specific response type by status code\nexport type ExtractResponse<\n T extends EndpointDefinition,\n Status extends number,\n> = Status extends keyof T['responses']\n ? T['responses'][Status] extends z.ZodTypeAny\n ? InferZodType<T['responses'][Status]>\n : never\n : never;\n\n// Path parameter extraction utilities\nexport type ExtractPathParams<T extends string> =\n T extends `${infer _Start}:${infer Param}/${infer Rest}`\n ? Param | ExtractPathParams<`/${Rest}`>\n : T extends `${infer _Start}:${infer Param}`\n ? Param\n : never;\n\n// Convert path params to object type\nexport type PathParamsObject<T extends string> = {\n [K in ExtractPathParams<T>]: string;\n};\n\n/**\n * Parse path parameters from a URL path pattern\n * e.g., \"/users/:id/posts/:postId\" => [\"id\", \"postId\"]\n */\nexport function parsePathParams(path: string): string[] {\n const matches = path.match(/:([^/]+)/g);\n if (!matches) return [];\n return matches.map((match) => match.slice(1));\n}\n\n/**\n * Match a URL path against a pattern and extract parameters\n * e.g., matchPath(\"/users/:id\", \"/users/123\") => { id: \"123\" }\n */\nexport function matchPath(pattern: string, path: string): Record<string, string> | null {\n const paramNames = parsePathParams(pattern);\n\n // Convert pattern to regex\n const regexPattern = pattern.replace(/:[^/]+/g, '([^/]+)').replace(/\\//g, '\\\\/');\n\n const regex = new RegExp(`^${regexPattern}$`);\n const match = path.match(regex);\n\n if (!match) return null;\n\n const params: Record<string, string> = {};\n paramNames.forEach((name, index) => {\n params[name] = match[index + 1] ?? '';\n });\n\n return params;\n}\n\n/**\n * Interpolate path parameters into a URL pattern\n * e.g., interpolatePath(\"/users/:id\", { id: \"123\" }) => \"/users/123\"\n */\nexport function interpolatePath(pattern: string, params: Record<string, string | number>): string {\n let result = pattern;\n for (const [key, value] of Object.entries(params)) {\n result = result.replace(`:${key}`, String(value));\n }\n return result;\n}\n\n/**\n * Build a complete URL with query parameters\n */\nexport function buildUrl(\n baseUrl: string,\n path: string,\n query?: Record<string, string | number | boolean | string[]>,\n): string {\n // Normalize baseUrl - remove trailing slash\n const normalizedBase = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;\n\n // Ensure path starts with /\n const normalizedPath = path.startsWith('/') ? path : `/${path}`;\n\n // Concatenate base and path\n const fullPath = normalizedBase + normalizedPath;\n\n const url = new URL(fullPath);\n\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n if (value !== undefined && value !== null) {\n if (Array.isArray(value)) {\n for (const v of value) {\n url.searchParams.append(key, String(v));\n }\n } else {\n url.searchParams.append(key, String(value));\n }\n }\n }\n }\n\n return url.toString();\n}\n\n/**\n * Parse query parameters from URLSearchParams\n */\nexport function parseQuery(searchParams: URLSearchParams): Record<string, string | string[]> {\n const result: Record<string, string | string[]> = {};\n\n for (const [key, value] of searchParams.entries()) {\n const existing = result[key];\n if (existing) {\n if (Array.isArray(existing)) {\n existing.push(value);\n } else {\n result[key] = [existing, value];\n }\n } else {\n result[key] = value;\n }\n }\n\n return result;\n}\n\n// Type helper to ensure a value is a valid contract\nexport function defineContract<T extends Contract>(contract: T): T {\n return contract;\n}\n"
|
|
5
|
+
"import type { z } from 'zod';\n\n// HTTP methods supported\nexport type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';\n\n// Content types supported for request bodies\nexport type ContentType = 'application/json' | 'multipart/form-data';\n\n// HTTP status codes as const object for type-safe responses without 'as const'\nexport const Status = {\n // Success responses\n OK: 200 as const,\n Created: 201 as const,\n Accepted: 202 as const,\n NoContent: 204 as const,\n\n // Redirection\n MovedPermanently: 301 as const,\n Found: 302 as const,\n NotModified: 304 as const,\n\n // Client errors\n BadRequest: 400 as const,\n Unauthorized: 401 as const,\n Forbidden: 403 as const,\n NotFound: 404 as const,\n MethodNotAllowed: 405 as const,\n Conflict: 409 as const,\n UnprocessableEntity: 422 as const,\n TooManyRequests: 429 as const,\n\n // Server errors\n InternalServerError: 500 as const,\n NotImplemented: 501 as const,\n BadGateway: 502 as const,\n ServiceUnavailable: 503 as const,\n GatewayTimeout: 504 as const,\n} as const;\n\n// Base fields shared by all endpoint types\ninterface BaseEndpointFields {\n path: string;\n params?: z.ZodTypeAny;\n query?: z.ZodTypeAny;\n headers?: z.ZodTypeAny;\n}\n\n// Standard HTTP endpoint\nexport interface StandardEndpointDefinition extends BaseEndpointFields {\n type: 'standard';\n method: HttpMethod;\n body?: z.ZodTypeAny;\n contentType?: ContentType;\n responses: Record<number, z.ZodTypeAny>;\n}\n\n// Streaming response endpoint (NDJSON)\nexport interface StreamingEndpointDefinition extends BaseEndpointFields {\n type: 'streaming';\n method: 'POST';\n body?: z.ZodTypeAny;\n contentType?: ContentType;\n /** Schema for each NDJSON chunk (type inference only, not validated) */\n chunk: z.ZodTypeAny;\n /** Optional final response after stream ends */\n finalResponse?: z.ZodTypeAny;\n /** Error responses for non-streaming failures */\n errorResponses?: Record<number, z.ZodTypeAny>;\n}\n\n// SSE endpoint\nexport interface SSEEndpointDefinition extends BaseEndpointFields {\n type: 'sse';\n method: 'GET';\n /** Event types: key = event name, value = data schema (type inference only) */\n events: Record<string, z.ZodTypeAny>;\n /** Error responses for connection failures */\n errorResponses?: Record<number, z.ZodTypeAny>;\n}\n\n// Download endpoint (binary file response)\nexport interface DownloadEndpointDefinition extends BaseEndpointFields {\n type: 'download';\n method: 'GET'; // Downloads are GET-only\n /** Error responses (non-2xx status codes) */\n errorResponses?: Record<number, z.ZodTypeAny>;\n}\n\n// Union of all endpoint types\nexport type AnyEndpointDefinition =\n | StandardEndpointDefinition\n | StreamingEndpointDefinition\n | SSEEndpointDefinition\n | DownloadEndpointDefinition;\n\n// Alias for backwards compatibility in type utilities\nexport type EndpointDefinition = AnyEndpointDefinition;\n\n// Contract is a collection of named endpoints\nexport type Contract = Record<string, AnyEndpointDefinition>;\n\n// Extract the Zod type from a schema\nexport type InferZodType<T> = T extends z.ZodTypeAny ? z.infer<T> : never;\n\n// Extract params type from endpoint\nexport type ExtractParams<T extends EndpointDefinition> = T['params'] extends z.ZodTypeAny\n ? InferZodType<T['params']>\n : never;\n\n// Extract query type from endpoint\nexport type ExtractQuery<T extends EndpointDefinition> = T['query'] extends z.ZodTypeAny\n ? InferZodType<T['query']>\n : never;\n\n// Extract headers type from endpoint\nexport type ExtractHeaders<T extends EndpointDefinition> = T['headers'] extends z.ZodTypeAny\n ? InferZodType<T['headers']>\n : never;\n\n// Extract body type from endpoint (only standard and streaming have body)\nexport type ExtractBody<T extends EndpointDefinition> = T extends { body: z.ZodTypeAny }\n ? InferZodType<T['body']>\n : never;\n\n// Extract response types for all status codes (only standard has responses)\nexport type ExtractResponses<T extends EndpointDefinition> = T extends {\n responses: Record<number, z.ZodTypeAny>;\n}\n ? {\n [K in keyof T['responses']]: T['responses'][K] extends z.ZodTypeAny\n ? InferZodType<T['responses'][K]>\n : never;\n }\n : never;\n\n// Extract a specific response type by status code (only standard has responses)\nexport type ExtractResponse<T extends EndpointDefinition, Status extends number> = T extends {\n responses: Record<number, z.ZodTypeAny>;\n}\n ? Status extends keyof T['responses']\n ? T['responses'][Status] extends z.ZodTypeAny\n ? InferZodType<T['responses'][Status]>\n : never\n : never\n : never;\n\n// Extract chunk type from streaming endpoint\nexport type ExtractChunk<T extends StreamingEndpointDefinition> = T['chunk'] extends z.ZodTypeAny\n ? InferZodType<T['chunk']>\n : never;\n\n// Extract final response type from streaming endpoint\nexport type ExtractFinalResponse<T extends StreamingEndpointDefinition> =\n T['finalResponse'] extends z.ZodTypeAny ? InferZodType<T['finalResponse']> : undefined;\n\n// Extract SSE event union type\nexport type ExtractSSEEvents<T extends SSEEndpointDefinition> = {\n [K in keyof T['events']]: {\n event: K;\n data: T['events'][K] extends z.ZodTypeAny ? InferZodType<T['events'][K]> : never;\n id?: string;\n };\n}[keyof T['events']];\n\n// Extract specific SSE event data type\nexport type ExtractSSEEventData<\n T extends SSEEndpointDefinition,\n K extends keyof T['events'],\n> = T['events'][K] extends z.ZodTypeAny ? InferZodType<T['events'][K]> : never;\n\n// Extract error responses for download endpoint\nexport type ExtractDownloadErrorResponse<\n T extends DownloadEndpointDefinition,\n Status extends keyof T['errorResponses'],\n> =\n T['errorResponses'] extends Record<number, z.ZodTypeAny>\n ? Status extends keyof T['errorResponses']\n ? T['errorResponses'][Status] extends z.ZodTypeAny\n ? InferZodType<T['errorResponses'][Status]>\n : never\n : never\n : never;\n\n// Upload progress event\nexport interface UploadProgressEvent {\n loaded: number;\n total: number;\n progress: number; // 0-1 (percentage as decimal)\n}\n\n// Download progress event\nexport interface DownloadProgressEvent {\n loaded: number;\n total: number;\n progress: number; // 0-1 (percentage as decimal), NaN if total unknown\n}\n\n// Path parameter extraction utilities\nexport type ExtractPathParams<T extends string> =\n T extends `${infer _Start}:${infer Param}/${infer Rest}`\n ? Param | ExtractPathParams<`/${Rest}`>\n : T extends `${infer _Start}:${infer Param}`\n ? Param\n : never;\n\n// Convert path params to object type\nexport type PathParamsObject<T extends string> = {\n [K in ExtractPathParams<T>]: string;\n};\n\n/**\n * Parse path parameters from a URL path pattern\n * e.g., \"/users/:id/posts/:postId\" => [\"id\", \"postId\"]\n */\nexport function parsePathParams(path: string): string[] {\n const matches = path.match(/:([^/]+)/g);\n if (!matches) return [];\n return matches.map((match) => match.slice(1));\n}\n\n/**\n * Match a URL path against a pattern and extract parameters\n * e.g., matchPath(\"/users/:id\", \"/users/123\") => { id: \"123\" }\n */\nexport function matchPath(pattern: string, path: string): Record<string, string> | null {\n const paramNames = parsePathParams(pattern);\n\n // Convert pattern to regex\n const regexPattern = pattern.replace(/:[^/]+/g, '([^/]+)').replace(/\\//g, '\\\\/');\n\n const regex = new RegExp(`^${regexPattern}$`);\n const match = path.match(regex);\n\n if (!match) return null;\n\n const params: Record<string, string> = {};\n paramNames.forEach((name, index) => {\n params[name] = match[index + 1] ?? '';\n });\n\n return params;\n}\n\n/**\n * Interpolate path parameters into a URL pattern\n * e.g., interpolatePath(\"/users/:id\", { id: \"123\" }) => \"/users/123\"\n */\nexport function interpolatePath(pattern: string, params: Record<string, string | number>): string {\n let result = pattern;\n for (const [key, value] of Object.entries(params)) {\n result = result.replace(`:${key}`, String(value));\n }\n return result;\n}\n\n/**\n * Build a complete URL with query parameters\n */\nexport function buildUrl(\n baseUrl: string,\n path: string,\n query?: Record<string, string | number | boolean | string[]>,\n): string {\n // Normalize baseUrl - remove trailing slash\n const normalizedBase = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;\n\n // Ensure path starts with /\n const normalizedPath = path.startsWith('/') ? path : `/${path}`;\n\n // Concatenate base and path\n const fullPath = normalizedBase + normalizedPath;\n\n const url = new URL(fullPath);\n\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n if (value !== undefined && value !== null) {\n if (Array.isArray(value)) {\n for (const v of value) {\n url.searchParams.append(key, String(v));\n }\n } else {\n url.searchParams.append(key, String(value));\n }\n }\n }\n }\n\n return url.toString();\n}\n\n/**\n * Parse query parameters from URLSearchParams\n */\nexport function parseQuery(searchParams: URLSearchParams): Record<string, string | string[]> {\n const result: Record<string, string | string[]> = {};\n\n for (const [key, value] of searchParams.entries()) {\n const existing = result[key];\n if (existing) {\n if (Array.isArray(existing)) {\n existing.push(value);\n } else {\n result[key] = [existing, value];\n }\n } else {\n result[key] = value;\n }\n }\n\n return result;\n}\n\n// Type helper to ensure a value is a valid contract\nexport function defineContract<T extends Contract>(contract: T): T {\n return contract;\n}\n\n/**\n * Convert an object to FormData using the hybrid JSON + Files approach.\n * Files are extracted and replaced with { __fileRef__: \"path\" } placeholders.\n * The resulting FormData contains __json__ with the serialized structure\n * and individual file entries at their path keys.\n */\nexport function objectToFormData(obj: Record<string, unknown>): FormData {\n const formData = new FormData();\n const files: Array<{ path: string; file: File }> = [];\n\n function traverse(value: unknown, path: string): unknown {\n if (value instanceof File) {\n files.push({ path, file: value });\n return { __fileRef__: path };\n }\n if (Array.isArray(value)) {\n return value.map((item, i) => traverse(item, `${path}.${i}`));\n }\n if (value && typeof value === 'object') {\n const result: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value)) {\n result[k] = traverse(v, path ? `${path}.${k}` : k);\n }\n return result;\n }\n return value;\n }\n\n const jsonWithRefs = traverse(obj, '');\n formData.append('__json__', JSON.stringify(jsonWithRefs));\n\n for (const { path, file } of files) {\n formData.append(path, file);\n }\n\n return formData;\n}\n\n/**\n * Parse FormData back to an object, reconstructing the structure with File objects.\n * Expects FormData created by objectToFormData with __json__ and file entries.\n * Falls back to simple Object.fromEntries for FormData without __json__.\n */\nexport function formDataToObject(formData: FormData): Record<string, unknown> {\n const jsonStr = formData.get('__json__');\n if (typeof jsonStr !== 'string') {\n return Object.fromEntries(formData.entries());\n }\n\n const obj = JSON.parse(jsonStr);\n\n function replaceRefs(value: unknown): unknown {\n if (value && typeof value === 'object' && '__fileRef__' in value) {\n const path = (value as { __fileRef__: string }).__fileRef__;\n return formData.get(path);\n }\n if (Array.isArray(value)) {\n return value.map(replaceRefs);\n }\n if (value && typeof value === 'object') {\n const result: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value)) {\n result[k] = replaceRefs(v);\n }\n return result;\n }\n return value;\n }\n\n return replaceRefs(obj) as Record<string, unknown>;\n}\n\n// Re-export WebSocket types\nexport * from './websocket.cjs';\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": "
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "7092E09755013A0164756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
package/dist/cjs/package.json
CHANGED
package/dist/mjs/index.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// packages/core/index.ts
|
|
3
|
+
export * from "./websocket.mjs";
|
|
3
4
|
var Status = {
|
|
4
5
|
OK: 200,
|
|
5
6
|
Created: 201,
|
|
@@ -87,14 +88,68 @@ function parseQuery(searchParams) {
|
|
|
87
88
|
function defineContract(contract) {
|
|
88
89
|
return contract;
|
|
89
90
|
}
|
|
91
|
+
function objectToFormData(obj) {
|
|
92
|
+
const formData = new FormData;
|
|
93
|
+
const files = [];
|
|
94
|
+
function traverse(value, path) {
|
|
95
|
+
if (value instanceof File) {
|
|
96
|
+
files.push({ path, file: value });
|
|
97
|
+
return { __fileRef__: path };
|
|
98
|
+
}
|
|
99
|
+
if (Array.isArray(value)) {
|
|
100
|
+
return value.map((item, i) => traverse(item, `${path}.${i}`));
|
|
101
|
+
}
|
|
102
|
+
if (value && typeof value === "object") {
|
|
103
|
+
const result = {};
|
|
104
|
+
for (const [k, v] of Object.entries(value)) {
|
|
105
|
+
result[k] = traverse(v, path ? `${path}.${k}` : k);
|
|
106
|
+
}
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
return value;
|
|
110
|
+
}
|
|
111
|
+
const jsonWithRefs = traverse(obj, "");
|
|
112
|
+
formData.append("__json__", JSON.stringify(jsonWithRefs));
|
|
113
|
+
for (const { path, file } of files) {
|
|
114
|
+
formData.append(path, file);
|
|
115
|
+
}
|
|
116
|
+
return formData;
|
|
117
|
+
}
|
|
118
|
+
function formDataToObject(formData) {
|
|
119
|
+
const jsonStr = formData.get("__json__");
|
|
120
|
+
if (typeof jsonStr !== "string") {
|
|
121
|
+
return Object.fromEntries(formData.entries());
|
|
122
|
+
}
|
|
123
|
+
const obj = JSON.parse(jsonStr);
|
|
124
|
+
function replaceRefs(value) {
|
|
125
|
+
if (value && typeof value === "object" && "__fileRef__" in value) {
|
|
126
|
+
const path = value.__fileRef__;
|
|
127
|
+
return formData.get(path);
|
|
128
|
+
}
|
|
129
|
+
if (Array.isArray(value)) {
|
|
130
|
+
return value.map(replaceRefs);
|
|
131
|
+
}
|
|
132
|
+
if (value && typeof value === "object") {
|
|
133
|
+
const result = {};
|
|
134
|
+
for (const [k, v] of Object.entries(value)) {
|
|
135
|
+
result[k] = replaceRefs(v);
|
|
136
|
+
}
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
return value;
|
|
140
|
+
}
|
|
141
|
+
return replaceRefs(obj);
|
|
142
|
+
}
|
|
90
143
|
export {
|
|
91
144
|
parseQuery,
|
|
92
145
|
parsePathParams,
|
|
146
|
+
objectToFormData,
|
|
93
147
|
matchPath,
|
|
94
148
|
interpolatePath,
|
|
149
|
+
formDataToObject,
|
|
95
150
|
defineContract,
|
|
96
151
|
buildUrl,
|
|
97
152
|
Status
|
|
98
153
|
};
|
|
99
154
|
|
|
100
|
-
//# debugId=
|
|
155
|
+
//# debugId=D33EA4F40F08382064756E2164756E21
|
package/dist/mjs/index.mjs.map
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../index.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"import type { z } from 'zod';\n\n// HTTP methods supported\nexport type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';\n\n// HTTP status codes as const object for type-safe responses without 'as const'\nexport const Status = {\n // Success responses\n OK: 200 as const,\n Created: 201 as const,\n Accepted: 202 as const,\n NoContent: 204 as const,\n\n // Redirection\n MovedPermanently: 301 as const,\n Found: 302 as const,\n NotModified: 304 as const,\n\n // Client errors\n BadRequest: 400 as const,\n Unauthorized: 401 as const,\n Forbidden: 403 as const,\n NotFound: 404 as const,\n MethodNotAllowed: 405 as const,\n Conflict: 409 as const,\n UnprocessableEntity: 422 as const,\n TooManyRequests: 429 as const,\n\n // Server errors\n InternalServerError: 500 as const,\n NotImplemented: 501 as const,\n BadGateway: 502 as const,\n ServiceUnavailable: 503 as const,\n GatewayTimeout: 504 as const,\n} as const;\n\n// Endpoint definition structure\nexport interface EndpointDefinition {\n method: HttpMethod;\n path: string;\n params?: z.ZodTypeAny;\n query?: z.ZodTypeAny;\n headers?: z.ZodTypeAny;\n body?: z.ZodTypeAny;\n responses: Record<number, z.ZodTypeAny>;\n}\n\n// Contract is a collection of named endpoints\nexport type Contract = Record<string, EndpointDefinition>;\n\n// Extract the Zod type from a schema\nexport type InferZodType<T> = T extends z.ZodTypeAny ? z.infer<T> : never;\n\n// Extract params type from endpoint\nexport type ExtractParams<T extends EndpointDefinition> = T['params'] extends z.ZodTypeAny\n ? InferZodType<T['params']>\n : never;\n\n// Extract query type from endpoint\nexport type ExtractQuery<T extends EndpointDefinition> = T['query'] extends z.ZodTypeAny\n ? InferZodType<T['query']>\n : never;\n\n// Extract headers type from endpoint\nexport type ExtractHeaders<T extends EndpointDefinition> = T['headers'] extends z.ZodTypeAny\n ? InferZodType<T['headers']>\n : never;\n\n// Extract body type from endpoint\nexport type ExtractBody<T extends EndpointDefinition> = T['body'] extends z.ZodTypeAny\n ? InferZodType<T['body']>\n : never;\n\n// Extract response types for all status codes\nexport type ExtractResponses<T extends EndpointDefinition> = {\n [K in keyof T['responses']]: T['responses'][K] extends z.ZodTypeAny\n ? InferZodType<T['responses'][K]>\n : never;\n};\n\n// Extract a specific response type by status code\nexport type ExtractResponse<\n T extends EndpointDefinition,\n Status extends number,\n> = Status extends keyof T['responses']\n ? T['responses'][Status] extends z.ZodTypeAny\n ? InferZodType<T['responses'][Status]>\n : never\n : never;\n\n// Path parameter extraction utilities\nexport type ExtractPathParams<T extends string> =\n T extends `${infer _Start}:${infer Param}/${infer Rest}`\n ? Param | ExtractPathParams<`/${Rest}`>\n : T extends `${infer _Start}:${infer Param}`\n ? Param\n : never;\n\n// Convert path params to object type\nexport type PathParamsObject<T extends string> = {\n [K in ExtractPathParams<T>]: string;\n};\n\n/**\n * Parse path parameters from a URL path pattern\n * e.g., \"/users/:id/posts/:postId\" => [\"id\", \"postId\"]\n */\nexport function parsePathParams(path: string): string[] {\n const matches = path.match(/:([^/]+)/g);\n if (!matches) return [];\n return matches.map((match) => match.slice(1));\n}\n\n/**\n * Match a URL path against a pattern and extract parameters\n * e.g., matchPath(\"/users/:id\", \"/users/123\") => { id: \"123\" }\n */\nexport function matchPath(pattern: string, path: string): Record<string, string> | null {\n const paramNames = parsePathParams(pattern);\n\n // Convert pattern to regex\n const regexPattern = pattern.replace(/:[^/]+/g, '([^/]+)').replace(/\\//g, '\\\\/');\n\n const regex = new RegExp(`^${regexPattern}$`);\n const match = path.match(regex);\n\n if (!match) return null;\n\n const params: Record<string, string> = {};\n paramNames.forEach((name, index) => {\n params[name] = match[index + 1] ?? '';\n });\n\n return params;\n}\n\n/**\n * Interpolate path parameters into a URL pattern\n * e.g., interpolatePath(\"/users/:id\", { id: \"123\" }) => \"/users/123\"\n */\nexport function interpolatePath(pattern: string, params: Record<string, string | number>): string {\n let result = pattern;\n for (const [key, value] of Object.entries(params)) {\n result = result.replace(`:${key}`, String(value));\n }\n return result;\n}\n\n/**\n * Build a complete URL with query parameters\n */\nexport function buildUrl(\n baseUrl: string,\n path: string,\n query?: Record<string, string | number | boolean | string[]>,\n): string {\n // Normalize baseUrl - remove trailing slash\n const normalizedBase = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;\n\n // Ensure path starts with /\n const normalizedPath = path.startsWith('/') ? path : `/${path}`;\n\n // Concatenate base and path\n const fullPath = normalizedBase + normalizedPath;\n\n const url = new URL(fullPath);\n\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n if (value !== undefined && value !== null) {\n if (Array.isArray(value)) {\n for (const v of value) {\n url.searchParams.append(key, String(v));\n }\n } else {\n url.searchParams.append(key, String(value));\n }\n }\n }\n }\n\n return url.toString();\n}\n\n/**\n * Parse query parameters from URLSearchParams\n */\nexport function parseQuery(searchParams: URLSearchParams): Record<string, string | string[]> {\n const result: Record<string, string | string[]> = {};\n\n for (const [key, value] of searchParams.entries()) {\n const existing = result[key];\n if (existing) {\n if (Array.isArray(existing)) {\n existing.push(value);\n } else {\n result[key] = [existing, value];\n }\n } else {\n result[key] = value;\n }\n }\n\n return result;\n}\n\n// Type helper to ensure a value is a valid contract\nexport function defineContract<T extends Contract>(contract: T): T {\n return contract;\n}\n"
|
|
5
|
+
"import type { z } from 'zod';\n\n// HTTP methods supported\nexport type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';\n\n// Content types supported for request bodies\nexport type ContentType = 'application/json' | 'multipart/form-data';\n\n// HTTP status codes as const object for type-safe responses without 'as const'\nexport const Status = {\n // Success responses\n OK: 200 as const,\n Created: 201 as const,\n Accepted: 202 as const,\n NoContent: 204 as const,\n\n // Redirection\n MovedPermanently: 301 as const,\n Found: 302 as const,\n NotModified: 304 as const,\n\n // Client errors\n BadRequest: 400 as const,\n Unauthorized: 401 as const,\n Forbidden: 403 as const,\n NotFound: 404 as const,\n MethodNotAllowed: 405 as const,\n Conflict: 409 as const,\n UnprocessableEntity: 422 as const,\n TooManyRequests: 429 as const,\n\n // Server errors\n InternalServerError: 500 as const,\n NotImplemented: 501 as const,\n BadGateway: 502 as const,\n ServiceUnavailable: 503 as const,\n GatewayTimeout: 504 as const,\n} as const;\n\n// Base fields shared by all endpoint types\ninterface BaseEndpointFields {\n path: string;\n params?: z.ZodTypeAny;\n query?: z.ZodTypeAny;\n headers?: z.ZodTypeAny;\n}\n\n// Standard HTTP endpoint\nexport interface StandardEndpointDefinition extends BaseEndpointFields {\n type: 'standard';\n method: HttpMethod;\n body?: z.ZodTypeAny;\n contentType?: ContentType;\n responses: Record<number, z.ZodTypeAny>;\n}\n\n// Streaming response endpoint (NDJSON)\nexport interface StreamingEndpointDefinition extends BaseEndpointFields {\n type: 'streaming';\n method: 'POST';\n body?: z.ZodTypeAny;\n contentType?: ContentType;\n /** Schema for each NDJSON chunk (type inference only, not validated) */\n chunk: z.ZodTypeAny;\n /** Optional final response after stream ends */\n finalResponse?: z.ZodTypeAny;\n /** Error responses for non-streaming failures */\n errorResponses?: Record<number, z.ZodTypeAny>;\n}\n\n// SSE endpoint\nexport interface SSEEndpointDefinition extends BaseEndpointFields {\n type: 'sse';\n method: 'GET';\n /** Event types: key = event name, value = data schema (type inference only) */\n events: Record<string, z.ZodTypeAny>;\n /** Error responses for connection failures */\n errorResponses?: Record<number, z.ZodTypeAny>;\n}\n\n// Download endpoint (binary file response)\nexport interface DownloadEndpointDefinition extends BaseEndpointFields {\n type: 'download';\n method: 'GET'; // Downloads are GET-only\n /** Error responses (non-2xx status codes) */\n errorResponses?: Record<number, z.ZodTypeAny>;\n}\n\n// Union of all endpoint types\nexport type AnyEndpointDefinition =\n | StandardEndpointDefinition\n | StreamingEndpointDefinition\n | SSEEndpointDefinition\n | DownloadEndpointDefinition;\n\n// Alias for backwards compatibility in type utilities\nexport type EndpointDefinition = AnyEndpointDefinition;\n\n// Contract is a collection of named endpoints\nexport type Contract = Record<string, AnyEndpointDefinition>;\n\n// Extract the Zod type from a schema\nexport type InferZodType<T> = T extends z.ZodTypeAny ? z.infer<T> : never;\n\n// Extract params type from endpoint\nexport type ExtractParams<T extends EndpointDefinition> = T['params'] extends z.ZodTypeAny\n ? InferZodType<T['params']>\n : never;\n\n// Extract query type from endpoint\nexport type ExtractQuery<T extends EndpointDefinition> = T['query'] extends z.ZodTypeAny\n ? InferZodType<T['query']>\n : never;\n\n// Extract headers type from endpoint\nexport type ExtractHeaders<T extends EndpointDefinition> = T['headers'] extends z.ZodTypeAny\n ? InferZodType<T['headers']>\n : never;\n\n// Extract body type from endpoint (only standard and streaming have body)\nexport type ExtractBody<T extends EndpointDefinition> = T extends { body: z.ZodTypeAny }\n ? InferZodType<T['body']>\n : never;\n\n// Extract response types for all status codes (only standard has responses)\nexport type ExtractResponses<T extends EndpointDefinition> = T extends {\n responses: Record<number, z.ZodTypeAny>;\n}\n ? {\n [K in keyof T['responses']]: T['responses'][K] extends z.ZodTypeAny\n ? InferZodType<T['responses'][K]>\n : never;\n }\n : never;\n\n// Extract a specific response type by status code (only standard has responses)\nexport type ExtractResponse<T extends EndpointDefinition, Status extends number> = T extends {\n responses: Record<number, z.ZodTypeAny>;\n}\n ? Status extends keyof T['responses']\n ? T['responses'][Status] extends z.ZodTypeAny\n ? InferZodType<T['responses'][Status]>\n : never\n : never\n : never;\n\n// Extract chunk type from streaming endpoint\nexport type ExtractChunk<T extends StreamingEndpointDefinition> = T['chunk'] extends z.ZodTypeAny\n ? InferZodType<T['chunk']>\n : never;\n\n// Extract final response type from streaming endpoint\nexport type ExtractFinalResponse<T extends StreamingEndpointDefinition> =\n T['finalResponse'] extends z.ZodTypeAny ? InferZodType<T['finalResponse']> : undefined;\n\n// Extract SSE event union type\nexport type ExtractSSEEvents<T extends SSEEndpointDefinition> = {\n [K in keyof T['events']]: {\n event: K;\n data: T['events'][K] extends z.ZodTypeAny ? InferZodType<T['events'][K]> : never;\n id?: string;\n };\n}[keyof T['events']];\n\n// Extract specific SSE event data type\nexport type ExtractSSEEventData<\n T extends SSEEndpointDefinition,\n K extends keyof T['events'],\n> = T['events'][K] extends z.ZodTypeAny ? InferZodType<T['events'][K]> : never;\n\n// Extract error responses for download endpoint\nexport type ExtractDownloadErrorResponse<\n T extends DownloadEndpointDefinition,\n Status extends keyof T['errorResponses'],\n> =\n T['errorResponses'] extends Record<number, z.ZodTypeAny>\n ? Status extends keyof T['errorResponses']\n ? T['errorResponses'][Status] extends z.ZodTypeAny\n ? InferZodType<T['errorResponses'][Status]>\n : never\n : never\n : never;\n\n// Upload progress event\nexport interface UploadProgressEvent {\n loaded: number;\n total: number;\n progress: number; // 0-1 (percentage as decimal)\n}\n\n// Download progress event\nexport interface DownloadProgressEvent {\n loaded: number;\n total: number;\n progress: number; // 0-1 (percentage as decimal), NaN if total unknown\n}\n\n// Path parameter extraction utilities\nexport type ExtractPathParams<T extends string> =\n T extends `${infer _Start}:${infer Param}/${infer Rest}`\n ? Param | ExtractPathParams<`/${Rest}`>\n : T extends `${infer _Start}:${infer Param}`\n ? Param\n : never;\n\n// Convert path params to object type\nexport type PathParamsObject<T extends string> = {\n [K in ExtractPathParams<T>]: string;\n};\n\n/**\n * Parse path parameters from a URL path pattern\n * e.g., \"/users/:id/posts/:postId\" => [\"id\", \"postId\"]\n */\nexport function parsePathParams(path: string): string[] {\n const matches = path.match(/:([^/]+)/g);\n if (!matches) return [];\n return matches.map((match) => match.slice(1));\n}\n\n/**\n * Match a URL path against a pattern and extract parameters\n * e.g., matchPath(\"/users/:id\", \"/users/123\") => { id: \"123\" }\n */\nexport function matchPath(pattern: string, path: string): Record<string, string> | null {\n const paramNames = parsePathParams(pattern);\n\n // Convert pattern to regex\n const regexPattern = pattern.replace(/:[^/]+/g, '([^/]+)').replace(/\\//g, '\\\\/');\n\n const regex = new RegExp(`^${regexPattern}$`);\n const match = path.match(regex);\n\n if (!match) return null;\n\n const params: Record<string, string> = {};\n paramNames.forEach((name, index) => {\n params[name] = match[index + 1] ?? '';\n });\n\n return params;\n}\n\n/**\n * Interpolate path parameters into a URL pattern\n * e.g., interpolatePath(\"/users/:id\", { id: \"123\" }) => \"/users/123\"\n */\nexport function interpolatePath(pattern: string, params: Record<string, string | number>): string {\n let result = pattern;\n for (const [key, value] of Object.entries(params)) {\n result = result.replace(`:${key}`, String(value));\n }\n return result;\n}\n\n/**\n * Build a complete URL with query parameters\n */\nexport function buildUrl(\n baseUrl: string,\n path: string,\n query?: Record<string, string | number | boolean | string[]>,\n): string {\n // Normalize baseUrl - remove trailing slash\n const normalizedBase = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;\n\n // Ensure path starts with /\n const normalizedPath = path.startsWith('/') ? path : `/${path}`;\n\n // Concatenate base and path\n const fullPath = normalizedBase + normalizedPath;\n\n const url = new URL(fullPath);\n\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n if (value !== undefined && value !== null) {\n if (Array.isArray(value)) {\n for (const v of value) {\n url.searchParams.append(key, String(v));\n }\n } else {\n url.searchParams.append(key, String(value));\n }\n }\n }\n }\n\n return url.toString();\n}\n\n/**\n * Parse query parameters from URLSearchParams\n */\nexport function parseQuery(searchParams: URLSearchParams): Record<string, string | string[]> {\n const result: Record<string, string | string[]> = {};\n\n for (const [key, value] of searchParams.entries()) {\n const existing = result[key];\n if (existing) {\n if (Array.isArray(existing)) {\n existing.push(value);\n } else {\n result[key] = [existing, value];\n }\n } else {\n result[key] = value;\n }\n }\n\n return result;\n}\n\n// Type helper to ensure a value is a valid contract\nexport function defineContract<T extends Contract>(contract: T): T {\n return contract;\n}\n\n/**\n * Convert an object to FormData using the hybrid JSON + Files approach.\n * Files are extracted and replaced with { __fileRef__: \"path\" } placeholders.\n * The resulting FormData contains __json__ with the serialized structure\n * and individual file entries at their path keys.\n */\nexport function objectToFormData(obj: Record<string, unknown>): FormData {\n const formData = new FormData();\n const files: Array<{ path: string; file: File }> = [];\n\n function traverse(value: unknown, path: string): unknown {\n if (value instanceof File) {\n files.push({ path, file: value });\n return { __fileRef__: path };\n }\n if (Array.isArray(value)) {\n return value.map((item, i) => traverse(item, `${path}.${i}`));\n }\n if (value && typeof value === 'object') {\n const result: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value)) {\n result[k] = traverse(v, path ? `${path}.${k}` : k);\n }\n return result;\n }\n return value;\n }\n\n const jsonWithRefs = traverse(obj, '');\n formData.append('__json__', JSON.stringify(jsonWithRefs));\n\n for (const { path, file } of files) {\n formData.append(path, file);\n }\n\n return formData;\n}\n\n/**\n * Parse FormData back to an object, reconstructing the structure with File objects.\n * Expects FormData created by objectToFormData with __json__ and file entries.\n * Falls back to simple Object.fromEntries for FormData without __json__.\n */\nexport function formDataToObject(formData: FormData): Record<string, unknown> {\n const jsonStr = formData.get('__json__');\n if (typeof jsonStr !== 'string') {\n return Object.fromEntries(formData.entries());\n }\n\n const obj = JSON.parse(jsonStr);\n\n function replaceRefs(value: unknown): unknown {\n if (value && typeof value === 'object' && '__fileRef__' in value) {\n const path = (value as { __fileRef__: string }).__fileRef__;\n return formData.get(path);\n }\n if (Array.isArray(value)) {\n return value.map(replaceRefs);\n }\n if (value && typeof value === 'object') {\n const result: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value)) {\n result[k] = replaceRefs(v);\n }\n return result;\n }\n return value;\n }\n\n return replaceRefs(obj) as Record<string, unknown>;\n}\n\n// Re-export WebSocket types\nexport * from './websocket.mjs';\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": ";;
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "D33EA4F40F08382064756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
package/dist/mjs/package.json
CHANGED
package/dist/types/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { z } from 'zod';
|
|
2
2
|
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
|
|
3
|
+
export type ContentType = 'application/json' | 'multipart/form-data';
|
|
3
4
|
export declare const Status: {
|
|
4
5
|
readonly OK: 200;
|
|
5
6
|
readonly Created: 201;
|
|
@@ -22,25 +23,84 @@ export declare const Status: {
|
|
|
22
23
|
readonly ServiceUnavailable: 503;
|
|
23
24
|
readonly GatewayTimeout: 504;
|
|
24
25
|
};
|
|
25
|
-
|
|
26
|
-
method: HttpMethod;
|
|
26
|
+
interface BaseEndpointFields {
|
|
27
27
|
path: string;
|
|
28
28
|
params?: z.ZodTypeAny;
|
|
29
29
|
query?: z.ZodTypeAny;
|
|
30
30
|
headers?: z.ZodTypeAny;
|
|
31
|
+
}
|
|
32
|
+
export interface StandardEndpointDefinition extends BaseEndpointFields {
|
|
33
|
+
type: 'standard';
|
|
34
|
+
method: HttpMethod;
|
|
31
35
|
body?: z.ZodTypeAny;
|
|
36
|
+
contentType?: ContentType;
|
|
32
37
|
responses: Record<number, z.ZodTypeAny>;
|
|
33
38
|
}
|
|
34
|
-
export
|
|
39
|
+
export interface StreamingEndpointDefinition extends BaseEndpointFields {
|
|
40
|
+
type: 'streaming';
|
|
41
|
+
method: 'POST';
|
|
42
|
+
body?: z.ZodTypeAny;
|
|
43
|
+
contentType?: ContentType;
|
|
44
|
+
/** Schema for each NDJSON chunk (type inference only, not validated) */
|
|
45
|
+
chunk: z.ZodTypeAny;
|
|
46
|
+
/** Optional final response after stream ends */
|
|
47
|
+
finalResponse?: z.ZodTypeAny;
|
|
48
|
+
/** Error responses for non-streaming failures */
|
|
49
|
+
errorResponses?: Record<number, z.ZodTypeAny>;
|
|
50
|
+
}
|
|
51
|
+
export interface SSEEndpointDefinition extends BaseEndpointFields {
|
|
52
|
+
type: 'sse';
|
|
53
|
+
method: 'GET';
|
|
54
|
+
/** Event types: key = event name, value = data schema (type inference only) */
|
|
55
|
+
events: Record<string, z.ZodTypeAny>;
|
|
56
|
+
/** Error responses for connection failures */
|
|
57
|
+
errorResponses?: Record<number, z.ZodTypeAny>;
|
|
58
|
+
}
|
|
59
|
+
export interface DownloadEndpointDefinition extends BaseEndpointFields {
|
|
60
|
+
type: 'download';
|
|
61
|
+
method: 'GET';
|
|
62
|
+
/** Error responses (non-2xx status codes) */
|
|
63
|
+
errorResponses?: Record<number, z.ZodTypeAny>;
|
|
64
|
+
}
|
|
65
|
+
export type AnyEndpointDefinition = StandardEndpointDefinition | StreamingEndpointDefinition | SSEEndpointDefinition | DownloadEndpointDefinition;
|
|
66
|
+
export type EndpointDefinition = AnyEndpointDefinition;
|
|
67
|
+
export type Contract = Record<string, AnyEndpointDefinition>;
|
|
35
68
|
export type InferZodType<T> = T extends z.ZodTypeAny ? z.infer<T> : never;
|
|
36
69
|
export type ExtractParams<T extends EndpointDefinition> = T['params'] extends z.ZodTypeAny ? InferZodType<T['params']> : never;
|
|
37
70
|
export type ExtractQuery<T extends EndpointDefinition> = T['query'] extends z.ZodTypeAny ? InferZodType<T['query']> : never;
|
|
38
71
|
export type ExtractHeaders<T extends EndpointDefinition> = T['headers'] extends z.ZodTypeAny ? InferZodType<T['headers']> : never;
|
|
39
|
-
export type ExtractBody<T extends EndpointDefinition> = T
|
|
40
|
-
|
|
72
|
+
export type ExtractBody<T extends EndpointDefinition> = T extends {
|
|
73
|
+
body: z.ZodTypeAny;
|
|
74
|
+
} ? InferZodType<T['body']> : never;
|
|
75
|
+
export type ExtractResponses<T extends EndpointDefinition> = T extends {
|
|
76
|
+
responses: Record<number, z.ZodTypeAny>;
|
|
77
|
+
} ? {
|
|
41
78
|
[K in keyof T['responses']]: T['responses'][K] extends z.ZodTypeAny ? InferZodType<T['responses'][K]> : never;
|
|
42
|
-
};
|
|
43
|
-
export type ExtractResponse<T extends EndpointDefinition, Status extends number> =
|
|
79
|
+
} : never;
|
|
80
|
+
export type ExtractResponse<T extends EndpointDefinition, Status extends number> = T extends {
|
|
81
|
+
responses: Record<number, z.ZodTypeAny>;
|
|
82
|
+
} ? Status extends keyof T['responses'] ? T['responses'][Status] extends z.ZodTypeAny ? InferZodType<T['responses'][Status]> : never : never : never;
|
|
83
|
+
export type ExtractChunk<T extends StreamingEndpointDefinition> = T['chunk'] extends z.ZodTypeAny ? InferZodType<T['chunk']> : never;
|
|
84
|
+
export type ExtractFinalResponse<T extends StreamingEndpointDefinition> = T['finalResponse'] extends z.ZodTypeAny ? InferZodType<T['finalResponse']> : undefined;
|
|
85
|
+
export type ExtractSSEEvents<T extends SSEEndpointDefinition> = {
|
|
86
|
+
[K in keyof T['events']]: {
|
|
87
|
+
event: K;
|
|
88
|
+
data: T['events'][K] extends z.ZodTypeAny ? InferZodType<T['events'][K]> : never;
|
|
89
|
+
id?: string;
|
|
90
|
+
};
|
|
91
|
+
}[keyof T['events']];
|
|
92
|
+
export type ExtractSSEEventData<T extends SSEEndpointDefinition, K extends keyof T['events']> = T['events'][K] extends z.ZodTypeAny ? InferZodType<T['events'][K]> : never;
|
|
93
|
+
export type ExtractDownloadErrorResponse<T extends DownloadEndpointDefinition, Status extends keyof T['errorResponses']> = T['errorResponses'] extends Record<number, z.ZodTypeAny> ? Status extends keyof T['errorResponses'] ? T['errorResponses'][Status] extends z.ZodTypeAny ? InferZodType<T['errorResponses'][Status]> : never : never : never;
|
|
94
|
+
export interface UploadProgressEvent {
|
|
95
|
+
loaded: number;
|
|
96
|
+
total: number;
|
|
97
|
+
progress: number;
|
|
98
|
+
}
|
|
99
|
+
export interface DownloadProgressEvent {
|
|
100
|
+
loaded: number;
|
|
101
|
+
total: number;
|
|
102
|
+
progress: number;
|
|
103
|
+
}
|
|
44
104
|
export type ExtractPathParams<T extends string> = T extends `${infer _Start}:${infer Param}/${infer Rest}` ? Param | ExtractPathParams<`/${Rest}`> : T extends `${infer _Start}:${infer Param}` ? Param : never;
|
|
45
105
|
export type PathParamsObject<T extends string> = {
|
|
46
106
|
[K in ExtractPathParams<T>]: string;
|
|
@@ -69,3 +129,17 @@ export declare function buildUrl(baseUrl: string, path: string, query?: Record<s
|
|
|
69
129
|
*/
|
|
70
130
|
export declare function parseQuery(searchParams: URLSearchParams): Record<string, string | string[]>;
|
|
71
131
|
export declare function defineContract<T extends Contract>(contract: T): T;
|
|
132
|
+
/**
|
|
133
|
+
* Convert an object to FormData using the hybrid JSON + Files approach.
|
|
134
|
+
* Files are extracted and replaced with { __fileRef__: "path" } placeholders.
|
|
135
|
+
* The resulting FormData contains __json__ with the serialized structure
|
|
136
|
+
* and individual file entries at their path keys.
|
|
137
|
+
*/
|
|
138
|
+
export declare function objectToFormData(obj: Record<string, unknown>): FormData;
|
|
139
|
+
/**
|
|
140
|
+
* Parse FormData back to an object, reconstructing the structure with File objects.
|
|
141
|
+
* Expects FormData created by objectToFormData with __json__ and file entries.
|
|
142
|
+
* Falls back to simple Object.fromEntries for FormData without __json__.
|
|
143
|
+
*/
|
|
144
|
+
export declare function formDataToObject(formData: FormData): Record<string, unknown>;
|
|
145
|
+
export * from './websocket';
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Definition for a single message type in WebSocket communication
|
|
4
|
+
*/
|
|
5
|
+
export interface WebSocketMessageDefinition {
|
|
6
|
+
payload: z.ZodTypeAny;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Definition for a WebSocket endpoint contract
|
|
10
|
+
*/
|
|
11
|
+
export interface WebSocketContractDefinition {
|
|
12
|
+
path: string;
|
|
13
|
+
params?: z.ZodTypeAny;
|
|
14
|
+
query?: z.ZodTypeAny;
|
|
15
|
+
headers?: z.ZodTypeAny;
|
|
16
|
+
/** Messages client sends to server (validated with Zod on server) */
|
|
17
|
+
clientMessages: Record<string, WebSocketMessageDefinition>;
|
|
18
|
+
/** Messages server sends to client (type inference only, NOT validated) */
|
|
19
|
+
serverMessages: Record<string, WebSocketMessageDefinition>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A collection of named WebSocket endpoints
|
|
23
|
+
*/
|
|
24
|
+
export type WebSocketContract = Record<string, WebSocketContractDefinition>;
|
|
25
|
+
/**
|
|
26
|
+
* Extract the union type of all client messages for a WebSocket endpoint
|
|
27
|
+
*/
|
|
28
|
+
export type ExtractClientMessage<T extends WebSocketContractDefinition> = {
|
|
29
|
+
[K in keyof T['clientMessages']]: {
|
|
30
|
+
type: K;
|
|
31
|
+
payload: z.infer<T['clientMessages'][K]['payload']>;
|
|
32
|
+
};
|
|
33
|
+
}[keyof T['clientMessages']];
|
|
34
|
+
/**
|
|
35
|
+
* Extract the union type of all server messages for a WebSocket endpoint
|
|
36
|
+
*/
|
|
37
|
+
export type ExtractServerMessage<T extends WebSocketContractDefinition> = {
|
|
38
|
+
[K in keyof T['serverMessages']]: {
|
|
39
|
+
type: K;
|
|
40
|
+
payload: z.infer<T['serverMessages'][K]['payload']>;
|
|
41
|
+
};
|
|
42
|
+
}[keyof T['serverMessages']];
|
|
43
|
+
/**
|
|
44
|
+
* Extract the payload type for a specific client message
|
|
45
|
+
*/
|
|
46
|
+
export type ExtractClientMessagePayload<T extends WebSocketContractDefinition, K extends keyof T['clientMessages']> = z.infer<T['clientMessages'][K]['payload']>;
|
|
47
|
+
/**
|
|
48
|
+
* Extract the payload type for a specific server message
|
|
49
|
+
*/
|
|
50
|
+
export type ExtractServerMessagePayload<T extends WebSocketContractDefinition, K extends keyof T['serverMessages']> = z.infer<T['serverMessages'][K]['payload']>;
|
|
51
|
+
/**
|
|
52
|
+
* Extract params type from WebSocket endpoint
|
|
53
|
+
*/
|
|
54
|
+
export type ExtractWSParams<T extends WebSocketContractDefinition> = T['params'] extends z.ZodTypeAny ? z.infer<T['params']> : never;
|
|
55
|
+
/**
|
|
56
|
+
* Extract query type from WebSocket endpoint
|
|
57
|
+
*/
|
|
58
|
+
export type ExtractWSQuery<T extends WebSocketContractDefinition> = T['query'] extends z.ZodTypeAny ? z.infer<T['query']> : never;
|
|
59
|
+
/**
|
|
60
|
+
* Extract headers type from WebSocket endpoint
|
|
61
|
+
*/
|
|
62
|
+
export type ExtractWSHeaders<T extends WebSocketContractDefinition> = T['headers'] extends z.ZodTypeAny ? z.infer<T['headers']> : never;
|
|
63
|
+
/**
|
|
64
|
+
* Type helper to ensure a value is a valid WebSocket contract
|
|
65
|
+
*/
|
|
66
|
+
export declare function defineWebSocketContract<T extends WebSocketContract>(contract: T): T;
|