@epoch32/visage 0.0.0-stage → 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +373 -0
- package/README.md +110 -2
- package/dist/bundle.dev.js +4875 -0
- package/dist/index.js +9633 -0
- package/dist/src/api/index.js +4002 -0
- package/dist/src/charts/index.js +2717 -0
- package/dist/src/devtools/index.js +4677 -0
- package/dist/src/dom/index.js +1550 -0
- package/dist/src/form/index.js +1415 -0
- package/dist/src/index.js +11826 -0
- package/dist/src/jsx-runtime/index.js +1630 -0
- package/dist/src/query/index.js +2422 -0
- package/dist/src/router/index.js +1856 -0
- package/dist/src/rpc/index.js +3484 -0
- package/dist/src/server/index.js +1173 -0
- package/dist/src/signals/index.js +1077 -0
- package/dist/src/style/index.js +2340 -0
- package/dist/src/template/index.js +1149 -0
- package/dist/src/ui/index.js +4155 -0
- package/dist/src/virtual/index.js +1955 -0
- package/dist/src/vite/index.js +984 -0
- package/dist/vite/index.js +707 -0
- package/package.json +140 -4
- package/src/api/client.ts +371 -0
- package/src/api/file-router.ts +183 -0
- package/src/api/handler.ts +326 -0
- package/src/api/index.ts +71 -0
- package/src/api/middleware.ts +54 -0
- package/src/api/pragma.ts +155 -0
- package/src/api/procedure.ts +172 -0
- package/src/api/types.ts +249 -0
- package/src/charts/Area.tsx +111 -0
- package/src/charts/Axis.tsx +126 -0
- package/src/charts/Bar.tsx +86 -0
- package/src/charts/CartesianGrid.tsx +72 -0
- package/src/charts/Chart.tsx +269 -0
- package/src/charts/Cursor.tsx +42 -0
- package/src/charts/Legend.tsx +56 -0
- package/src/charts/Line.tsx +106 -0
- package/src/charts/Pie.tsx +88 -0
- package/src/charts/Tooltip.tsx +127 -0
- package/src/charts/context.ts +30 -0
- package/src/charts/index.ts +14 -0
- package/src/charts/path.ts +155 -0
- package/src/charts/scale.ts +133 -0
- package/src/charts/types.ts +50 -0
- package/src/devtools/DomTab.tsx +134 -0
- package/src/devtools/GraphInspector.tsx +196 -0
- package/src/devtools/GraphTab.tsx +315 -0
- package/src/devtools/QueryTab.tsx +200 -0
- package/src/devtools/RpcTab.tsx +190 -0
- package/src/devtools/SignalsTab.tsx +129 -0
- package/src/devtools/component.tsx +243 -0
- package/src/devtools/highlighter.ts +134 -0
- package/src/devtools/hub.ts +175 -0
- package/src/devtools/index.ts +20 -0
- package/src/devtools/styles.ts +313 -0
- package/src/devtools/types.ts +45 -0
- package/src/dom/async.ts +232 -0
- package/src/dom/control-flow.ts +353 -0
- package/src/dom/index.ts +32 -0
- package/src/dom/lazy.ts +82 -0
- package/src/dom/render.ts +286 -0
- package/src/dom/resource.ts +120 -0
- package/src/form/form.ts +218 -0
- package/src/form/index.ts +17 -0
- package/src/form/schema.ts +299 -0
- package/src/index.ts +413 -0
- package/src/jsx-dev-runtime/index.ts +1 -0
- package/src/jsx-runtime/index.ts +112 -0
- package/src/jsx-runtime/jsx.d.ts +278 -0
- package/src/query/create-mutation.ts +64 -0
- package/src/query/create-query.ts +426 -0
- package/src/query/focus-manager.ts +68 -0
- package/src/query/index.ts +63 -0
- package/src/query/mutation-cache.ts +138 -0
- package/src/query/online-manager.ts +67 -0
- package/src/query/query-cache.ts +369 -0
- package/src/query/query-client.ts +248 -0
- package/src/query/types.ts +221 -0
- package/src/query/utils.ts +132 -0
- package/src/router/index.ts +19 -0
- package/src/router/router.tsx +352 -0
- package/src/rpc/client.ts +317 -0
- package/src/rpc/error.ts +70 -0
- package/src/rpc/index.ts +52 -0
- package/src/rpc/procedure.ts +88 -0
- package/src/rpc/router.ts +105 -0
- package/src/rpc/server.ts +511 -0
- package/src/rpc/types.ts +328 -0
- package/src/server/hydrate.ts +21 -0
- package/src/server/index.ts +10 -0
- package/src/server/render-to-stream.ts +31 -0
- package/src/server/render-to-string.ts +125 -0
- package/src/signals/computed.ts +217 -0
- package/src/signals/context.ts +89 -0
- package/src/signals/effect.ts +161 -0
- package/src/signals/index.ts +57 -0
- package/src/signals/scope.ts +132 -0
- package/src/signals/signal.ts +203 -0
- package/src/signals/store.ts +163 -0
- package/src/signals/system.ts +124 -0
- package/src/signals/types.ts +64 -0
- package/src/style/component.tsx +122 -0
- package/src/style/create-style.ts +229 -0
- package/src/style/cx.ts +75 -0
- package/src/style/hash.ts +19 -0
- package/src/style/index.ts +8 -0
- package/src/style/layout.tsx +406 -0
- package/src/style/manager.ts +87 -0
- package/src/style/parser.ts +257 -0
- package/src/style/scale.ts +237 -0
- package/src/template/index.ts +4 -0
- package/src/template/template.ts +126 -0
- package/src/ui/accordion.tsx +286 -0
- package/src/ui/command-dialog.tsx +49 -0
- package/src/ui/command.tsx +400 -0
- package/src/ui/data-table.tsx +322 -0
- package/src/ui/dialog.tsx +362 -0
- package/src/ui/dropdown-menu.tsx +381 -0
- package/src/ui/icon.tsx +69 -0
- package/src/ui/index.ts +138 -0
- package/src/ui/layout.tsx +13 -0
- package/src/ui/popover.tsx +249 -0
- package/src/ui/switch.tsx +114 -0
- package/src/ui/tabs.tsx +297 -0
- package/src/ui/toast.tsx +280 -0
- package/src/ui/tooltip.tsx +262 -0
- package/src/ui/utils.ts +110 -0
- package/src/virtual/index.ts +12 -0
- package/src/virtual/virtual-list.tsx +185 -0
- package/src/virtual/virtualizer.ts +274 -0
- package/src/vite/hmr.ts +38 -0
- package/src/vite/index.ts +3 -0
- package/src/vite/lucide.ts +124 -0
- package/src/vite/plugin.ts +102 -0
- package/src/vite/rpc-middleware.ts +82 -0
- package/src/vite/types.ts +65 -0
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
import { ERROR_CODE_TO_STATUS, RPCError } from "./error";
|
|
2
|
+
import { executeProcedure } from "./procedure";
|
|
3
|
+
import type { CORSOptions, FetchHandlerOptions, Procedure, Router, RPCResponse } from "./types";
|
|
4
|
+
|
|
5
|
+
function getCORSHeaders(cors: boolean | CORSOptions | undefined, req: Request): Headers {
|
|
6
|
+
const headers = new Headers();
|
|
7
|
+
if (!cors) return headers;
|
|
8
|
+
|
|
9
|
+
const opts: CORSOptions = typeof cors === "object" ? cors : {};
|
|
10
|
+
const originHeader = req.headers.get("origin") ?? "*";
|
|
11
|
+
|
|
12
|
+
let allowOrigin = "*";
|
|
13
|
+
if (typeof opts.origin === "function") {
|
|
14
|
+
const res = opts.origin(originHeader);
|
|
15
|
+
if (typeof res === "string") allowOrigin = res;
|
|
16
|
+
else if (res) allowOrigin = originHeader;
|
|
17
|
+
} else if (typeof opts.origin === "string") {
|
|
18
|
+
allowOrigin = opts.origin;
|
|
19
|
+
} else if (Array.isArray(opts.origin)) {
|
|
20
|
+
if (opts.origin.includes(originHeader)) allowOrigin = originHeader;
|
|
21
|
+
} else {
|
|
22
|
+
allowOrigin = originHeader || "*";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
headers.set("Access-Control-Allow-Origin", allowOrigin);
|
|
26
|
+
headers.set(
|
|
27
|
+
"Access-Control-Allow-Methods",
|
|
28
|
+
(opts.methods ?? ["GET", "POST", "OPTIONS", "HEAD"]).join(", ")
|
|
29
|
+
);
|
|
30
|
+
headers.set(
|
|
31
|
+
"Access-Control-Allow-Headers",
|
|
32
|
+
(opts.allowedHeaders ?? ["Content-Type", "Authorization", "X-Requested-With"]).join(", ")
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
if (opts.exposedHeaders) {
|
|
36
|
+
headers.set("Access-Control-Expose-Headers", opts.exposedHeaders.join(", "));
|
|
37
|
+
}
|
|
38
|
+
if (opts.credentials) {
|
|
39
|
+
headers.set("Access-Control-Allow-Credentials", "true");
|
|
40
|
+
}
|
|
41
|
+
if (opts.maxAge !== undefined) {
|
|
42
|
+
headers.set("Access-Control-Max-Age", opts.maxAge.toString());
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return headers;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function resolveProcedureFromPath(
|
|
49
|
+
router: Router<any, any>,
|
|
50
|
+
path: string
|
|
51
|
+
): Procedure<any, any, any, any> {
|
|
52
|
+
const parts = path.split(/[./]/).filter(Boolean);
|
|
53
|
+
let current: any = router._def.record;
|
|
54
|
+
|
|
55
|
+
for (let i = 0; i < parts.length; i++) {
|
|
56
|
+
const part = parts[i]!;
|
|
57
|
+
if (!current || typeof current !== "object") {
|
|
58
|
+
throw new RPCError({
|
|
59
|
+
code: "NOT_FOUND",
|
|
60
|
+
message: `Procedure '${path}' not found`
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (i === parts.length - 1) {
|
|
65
|
+
if (part in current && "type" in current[part]) {
|
|
66
|
+
return current[part];
|
|
67
|
+
}
|
|
68
|
+
throw new RPCError({
|
|
69
|
+
code: "NOT_FOUND",
|
|
70
|
+
message: `Procedure '${path}' not found`
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (part in current && "_def" in current[part]) {
|
|
75
|
+
current = current[part]._def.record;
|
|
76
|
+
} else if (part in current && typeof current[part] === "object") {
|
|
77
|
+
current = current[part];
|
|
78
|
+
} else {
|
|
79
|
+
throw new RPCError({
|
|
80
|
+
code: "NOT_FOUND",
|
|
81
|
+
message: `Procedure '${path}' not found`
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
throw new RPCError({
|
|
87
|
+
code: "NOT_FOUND",
|
|
88
|
+
message: `Procedure '${path}' not found`
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function matchesRulePattern(
|
|
93
|
+
pattern: string,
|
|
94
|
+
urlPath: string,
|
|
95
|
+
procedurePath: string
|
|
96
|
+
): boolean {
|
|
97
|
+
if (pattern === "*") return true;
|
|
98
|
+
|
|
99
|
+
const cleanUrl = urlPath.replace(/^\/+|\/+$/g, "");
|
|
100
|
+
const cleanPattern = pattern.replace(/^\/+|\/+$/g, "");
|
|
101
|
+
const cleanProc = procedurePath.replace(/^\/+|\/+$/g, "");
|
|
102
|
+
|
|
103
|
+
if (
|
|
104
|
+
cleanUrl === cleanPattern ||
|
|
105
|
+
cleanProc === cleanPattern ||
|
|
106
|
+
cleanProc === cleanPattern.replace(/\//g, ".")
|
|
107
|
+
) {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (cleanPattern.endsWith("*")) {
|
|
112
|
+
const prefix = cleanPattern.slice(0, -1);
|
|
113
|
+
if (cleanUrl.startsWith(prefix) || cleanProc.startsWith(prefix)) {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
const dotPrefix = prefix.replace(/\//g, ".");
|
|
117
|
+
if (cleanProc.startsWith(dotPrefix)) {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const regexStr = cleanPattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
124
|
+
const regex = new RegExp(`^${regexStr}$`);
|
|
125
|
+
return regex.test(cleanUrl) || regex.test(cleanProc);
|
|
126
|
+
} catch {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function createFetchHandler<TRouter extends Router<any, any>, TContext = any>(
|
|
132
|
+
options: FetchHandlerOptions<TRouter, TContext>
|
|
133
|
+
): (request: Request, ...extra: any[]) => Promise<Response> {
|
|
134
|
+
const {
|
|
135
|
+
router,
|
|
136
|
+
endpoint = "",
|
|
137
|
+
createContext,
|
|
138
|
+
onError,
|
|
139
|
+
cors = true,
|
|
140
|
+
securityHeaders = true,
|
|
141
|
+
rules
|
|
142
|
+
} = options;
|
|
143
|
+
|
|
144
|
+
return async (request: Request, ...extra: any[]): Promise<Response> => {
|
|
145
|
+
const corsHeaders = getCORSHeaders(cors, request);
|
|
146
|
+
|
|
147
|
+
if (request.method === "OPTIONS") {
|
|
148
|
+
return new Response(null, {
|
|
149
|
+
status: 204,
|
|
150
|
+
headers: corsHeaders
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const baseHeaders = new Headers(corsHeaders);
|
|
155
|
+
if (securityHeaders) {
|
|
156
|
+
if (typeof securityHeaders === "object") {
|
|
157
|
+
for (const [k, v] of Object.entries(securityHeaders)) {
|
|
158
|
+
baseHeaders.set(k, v);
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
baseHeaders.set("X-Content-Type-Options", "nosniff");
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const url = new URL(request.url);
|
|
166
|
+
let pathname = url.pathname;
|
|
167
|
+
if (endpoint && pathname.startsWith(endpoint)) {
|
|
168
|
+
pathname = pathname.slice(endpoint.length);
|
|
169
|
+
}
|
|
170
|
+
pathname = pathname.replace(/^\/+|\/+$/g, "");
|
|
171
|
+
|
|
172
|
+
const procedurePath = pathname || url.searchParams.get("procedure") || "";
|
|
173
|
+
|
|
174
|
+
let ctx: TContext = {} as TContext;
|
|
175
|
+
try {
|
|
176
|
+
if (createContext) {
|
|
177
|
+
ctx = await createContext({ req: request, extra });
|
|
178
|
+
}
|
|
179
|
+
} catch (err: any) {
|
|
180
|
+
const error =
|
|
181
|
+
err instanceof RPCError
|
|
182
|
+
? err
|
|
183
|
+
: new RPCError({
|
|
184
|
+
code: "UNAUTHORIZED",
|
|
185
|
+
message: err.message ?? "Context initialization failed",
|
|
186
|
+
cause: err
|
|
187
|
+
});
|
|
188
|
+
onError?.({ error, req: request });
|
|
189
|
+
baseHeaders.set("Content-Type", "application/json");
|
|
190
|
+
return new Response(
|
|
191
|
+
JSON.stringify({
|
|
192
|
+
error: {
|
|
193
|
+
code: error.code,
|
|
194
|
+
message: error.message,
|
|
195
|
+
issues: error.issues
|
|
196
|
+
}
|
|
197
|
+
}),
|
|
198
|
+
{
|
|
199
|
+
status: error.status,
|
|
200
|
+
headers: baseHeaders
|
|
201
|
+
}
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Evaluate declarative pattern rules
|
|
206
|
+
const rulesList: Array<{ match: string } & import("./types").RouteRule> = [];
|
|
207
|
+
if (rules) {
|
|
208
|
+
if (Array.isArray(rules)) {
|
|
209
|
+
rulesList.push(...rules);
|
|
210
|
+
} else {
|
|
211
|
+
for (const [match, rule] of Object.entries(rules)) {
|
|
212
|
+
rulesList.push({ match, ...rule });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const matchingRules = rulesList.filter((r) =>
|
|
218
|
+
matchesRulePattern(r.match, url.pathname, procedurePath)
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
// Enforce required headers, query parameters, and custom guards from rules
|
|
222
|
+
for (const rule of matchingRules) {
|
|
223
|
+
if (rule.requireHeaders) {
|
|
224
|
+
for (const headerName of rule.requireHeaders) {
|
|
225
|
+
if (!request.headers.get(headerName)) {
|
|
226
|
+
const error = new RPCError({
|
|
227
|
+
code: "UNAUTHORIZED",
|
|
228
|
+
message: `Missing required header '${headerName}' for rule '${rule.match}'`
|
|
229
|
+
});
|
|
230
|
+
onError?.({ error, path: procedurePath, req: request });
|
|
231
|
+
baseHeaders.set("Content-Type", "application/json");
|
|
232
|
+
return new Response(
|
|
233
|
+
JSON.stringify({
|
|
234
|
+
error: {
|
|
235
|
+
code: error.code,
|
|
236
|
+
message: error.message
|
|
237
|
+
}
|
|
238
|
+
}),
|
|
239
|
+
{
|
|
240
|
+
status: 401,
|
|
241
|
+
headers: baseHeaders
|
|
242
|
+
}
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (rule.requireParams) {
|
|
249
|
+
for (const paramName of rule.requireParams) {
|
|
250
|
+
if (!url.searchParams.has(paramName)) {
|
|
251
|
+
const error = new RPCError({
|
|
252
|
+
code: "BAD_REQUEST",
|
|
253
|
+
message: `Missing required query parameter '${paramName}' for rule '${rule.match}'`
|
|
254
|
+
});
|
|
255
|
+
onError?.({ error, path: procedurePath, req: request });
|
|
256
|
+
baseHeaders.set("Content-Type", "application/json");
|
|
257
|
+
return new Response(
|
|
258
|
+
JSON.stringify({
|
|
259
|
+
error: {
|
|
260
|
+
code: error.code,
|
|
261
|
+
message: error.message
|
|
262
|
+
}
|
|
263
|
+
}),
|
|
264
|
+
{
|
|
265
|
+
status: 400,
|
|
266
|
+
headers: baseHeaders
|
|
267
|
+
}
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (rule.guard) {
|
|
274
|
+
try {
|
|
275
|
+
const guardResult = await rule.guard({ req: request, ctx, path: procedurePath });
|
|
276
|
+
if (guardResult === false) {
|
|
277
|
+
throw new RPCError({
|
|
278
|
+
code: "UNAUTHORIZED",
|
|
279
|
+
message: `Access denied by route rule guard for '${rule.match}'`
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
} catch (err: any) {
|
|
283
|
+
const error =
|
|
284
|
+
err instanceof RPCError
|
|
285
|
+
? err
|
|
286
|
+
: new RPCError({
|
|
287
|
+
code: "UNAUTHORIZED",
|
|
288
|
+
message: err.message ?? "Unauthorized",
|
|
289
|
+
cause: err
|
|
290
|
+
});
|
|
291
|
+
onError?.({ error, path: procedurePath, req: request });
|
|
292
|
+
baseHeaders.set("Content-Type", "application/json");
|
|
293
|
+
return new Response(
|
|
294
|
+
JSON.stringify({
|
|
295
|
+
error: {
|
|
296
|
+
code: error.code,
|
|
297
|
+
message: error.message
|
|
298
|
+
}
|
|
299
|
+
}),
|
|
300
|
+
{
|
|
301
|
+
status: error.status,
|
|
302
|
+
headers: baseHeaders
|
|
303
|
+
}
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Inject custom response headers defined by matching rule
|
|
309
|
+
if (rule.headers) {
|
|
310
|
+
const resHeaders =
|
|
311
|
+
typeof rule.headers === "function" ? await rule.headers(request, ctx) : rule.headers;
|
|
312
|
+
for (const [k, v] of Object.entries(resHeaders)) {
|
|
313
|
+
baseHeaders.set(k, v);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Check for batch execution in POST body
|
|
319
|
+
if (request.method === "POST" && (!pathname || pathname === "batch")) {
|
|
320
|
+
try {
|
|
321
|
+
const body = await request.json();
|
|
322
|
+
if (Array.isArray(body)) {
|
|
323
|
+
const batchResults = await Promise.all(
|
|
324
|
+
body.map(async (item: { id?: string | number; path: string; input?: any }) => {
|
|
325
|
+
try {
|
|
326
|
+
const proc = resolveProcedureFromPath(router, item.path);
|
|
327
|
+
const data = await executeProcedure(proc, {
|
|
328
|
+
rawInput: item.input,
|
|
329
|
+
ctx,
|
|
330
|
+
req: request,
|
|
331
|
+
signal: request.signal
|
|
332
|
+
});
|
|
333
|
+
return {
|
|
334
|
+
id: item.id,
|
|
335
|
+
result: { data }
|
|
336
|
+
};
|
|
337
|
+
} catch (err: any) {
|
|
338
|
+
const error =
|
|
339
|
+
err instanceof RPCError
|
|
340
|
+
? err
|
|
341
|
+
: new RPCError({
|
|
342
|
+
code: "INTERNAL_SERVER_ERROR",
|
|
343
|
+
message: err.message ?? "Internal Server Error",
|
|
344
|
+
cause: err
|
|
345
|
+
});
|
|
346
|
+
onError?.({ error, path: item.path, req: request });
|
|
347
|
+
return {
|
|
348
|
+
id: item.id,
|
|
349
|
+
error: {
|
|
350
|
+
code: error.code,
|
|
351
|
+
message: error.message,
|
|
352
|
+
issues: error.issues
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
})
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
baseHeaders.set("Content-Type", "application/json");
|
|
360
|
+
return new Response(JSON.stringify(batchResults), {
|
|
361
|
+
status: 200,
|
|
362
|
+
headers: baseHeaders
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
} catch (err: any) {
|
|
366
|
+
const error = new RPCError({
|
|
367
|
+
code: "BAD_REQUEST",
|
|
368
|
+
message: "Malformed batch request body",
|
|
369
|
+
cause: err
|
|
370
|
+
});
|
|
371
|
+
baseHeaders.set("Content-Type", "application/json");
|
|
372
|
+
return new Response(
|
|
373
|
+
JSON.stringify({
|
|
374
|
+
error: {
|
|
375
|
+
code: error.code,
|
|
376
|
+
message: error.message
|
|
377
|
+
}
|
|
378
|
+
}),
|
|
379
|
+
{
|
|
380
|
+
status: 400,
|
|
381
|
+
headers: baseHeaders
|
|
382
|
+
}
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
try {
|
|
388
|
+
const procedure = resolveProcedureFromPath(router, procedurePath);
|
|
389
|
+
|
|
390
|
+
// Verify HTTP method constraints
|
|
391
|
+
if (procedure.type === "mutation" && request.method !== "POST") {
|
|
392
|
+
throw new RPCError({
|
|
393
|
+
code: "METHOD_NOT_ALLOWED",
|
|
394
|
+
message: "Mutations must use POST method"
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Parse input
|
|
399
|
+
let rawInput: unknown;
|
|
400
|
+
if (request.method === "GET") {
|
|
401
|
+
const inputParam = url.searchParams.get("input");
|
|
402
|
+
if (inputParam !== null) {
|
|
403
|
+
try {
|
|
404
|
+
rawInput = JSON.parse(inputParam);
|
|
405
|
+
} catch {
|
|
406
|
+
rawInput = inputParam;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
} else if (request.method === "POST") {
|
|
410
|
+
try {
|
|
411
|
+
const body = await request.json();
|
|
412
|
+
rawInput = body && typeof body === "object" && "input" in body ? body.input : body;
|
|
413
|
+
} catch {
|
|
414
|
+
rawInput = undefined;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Handle Subscription (Server-Sent Events)
|
|
419
|
+
if (procedure.type === "subscription") {
|
|
420
|
+
const iterable = (await executeProcedure(procedure, {
|
|
421
|
+
rawInput,
|
|
422
|
+
ctx,
|
|
423
|
+
req: request,
|
|
424
|
+
signal: request.signal
|
|
425
|
+
})) as AsyncIterable<any>;
|
|
426
|
+
|
|
427
|
+
if (!iterable || typeof (iterable as any)[Symbol.asyncIterator] !== "function") {
|
|
428
|
+
throw new RPCError({
|
|
429
|
+
code: "INTERNAL_SERVER_ERROR",
|
|
430
|
+
message: "Subscription procedure must return an AsyncIterable"
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const encoder = new TextEncoder();
|
|
435
|
+
const stream = new ReadableStream({
|
|
436
|
+
async start(controller) {
|
|
437
|
+
try {
|
|
438
|
+
for await (const chunk of iterable) {
|
|
439
|
+
if (request.signal.aborted) break;
|
|
440
|
+
const message = `data: ${JSON.stringify(chunk)}\n\n`;
|
|
441
|
+
controller.enqueue(encoder.encode(message));
|
|
442
|
+
}
|
|
443
|
+
controller.close();
|
|
444
|
+
} catch (err: any) {
|
|
445
|
+
const errorMessage = `event: error\ndata: ${JSON.stringify({ message: err.message ?? "Stream error" })}\n\n`;
|
|
446
|
+
controller.enqueue(encoder.encode(errorMessage));
|
|
447
|
+
controller.close();
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
cancel() {
|
|
451
|
+
if (typeof (iterable as any).return === "function") {
|
|
452
|
+
(iterable as any).return();
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
baseHeaders.set("Content-Type", "text/event-stream; charset=utf-8");
|
|
458
|
+
baseHeaders.set("Cache-Control", "no-cache, no-transform");
|
|
459
|
+
baseHeaders.set("Connection", "keep-alive");
|
|
460
|
+
|
|
461
|
+
return new Response(stream, {
|
|
462
|
+
status: 200,
|
|
463
|
+
headers: baseHeaders
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Standard Query or Mutation execution
|
|
468
|
+
const data = await executeProcedure(procedure, {
|
|
469
|
+
rawInput,
|
|
470
|
+
ctx,
|
|
471
|
+
req: request,
|
|
472
|
+
signal: request.signal
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
const responsePayload: RPCResponse = {
|
|
476
|
+
result: { data }
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
baseHeaders.set("Content-Type", "application/json");
|
|
480
|
+
return new Response(JSON.stringify(responsePayload), {
|
|
481
|
+
status: 200,
|
|
482
|
+
headers: baseHeaders
|
|
483
|
+
});
|
|
484
|
+
} catch (err: any) {
|
|
485
|
+
const error =
|
|
486
|
+
err instanceof RPCError
|
|
487
|
+
? err
|
|
488
|
+
: new RPCError({
|
|
489
|
+
code: "INTERNAL_SERVER_ERROR",
|
|
490
|
+
message: err.message ?? "Internal Server Error",
|
|
491
|
+
cause: err
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
onError?.({ error, path: procedurePath, req: request });
|
|
495
|
+
|
|
496
|
+
const responsePayload: RPCResponse = {
|
|
497
|
+
error: {
|
|
498
|
+
code: error.code,
|
|
499
|
+
message: error.message,
|
|
500
|
+
issues: error.issues
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
baseHeaders.set("Content-Type", "application/json");
|
|
505
|
+
return new Response(JSON.stringify(responsePayload), {
|
|
506
|
+
status: ERROR_CODE_TO_STATUS[error.code] ?? 500,
|
|
507
|
+
headers: baseHeaders
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
}
|