@canmi/seam-server 0.5.10 → 0.5.27
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 +2 -2
- package/dist/index.d.ts +215 -62
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +672 -290
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { validate } from "jtd";
|
|
1
2
|
import { existsSync, readFileSync, watch } from "node:fs";
|
|
2
3
|
import { extname, join } from "node:path";
|
|
3
|
-
import { validate } from "jtd";
|
|
4
4
|
import { escapeHtml, renderPage } from "@canmi/seam-engine";
|
|
5
5
|
import { readFile } from "node:fs/promises";
|
|
6
6
|
|
|
@@ -157,6 +157,27 @@ function formatValidationErrors(errors) {
|
|
|
157
157
|
return `${e.instancePath.length > 0 ? e.instancePath.join("/") : "(root)"} (schema: ${e.schemaPath.join("/")})`;
|
|
158
158
|
}).join("; ");
|
|
159
159
|
}
|
|
160
|
+
/** Walk a nested object along a key path, returning undefined if unreachable */
|
|
161
|
+
function walkPath(obj, keys) {
|
|
162
|
+
let cur = obj;
|
|
163
|
+
for (const k of keys) {
|
|
164
|
+
if (cur === null || cur === void 0 || typeof cur !== "object") return void 0;
|
|
165
|
+
cur = cur[k];
|
|
166
|
+
}
|
|
167
|
+
return cur;
|
|
168
|
+
}
|
|
169
|
+
/** Extract structured details from JTD validation errors (best-effort) */
|
|
170
|
+
function formatValidationDetails(errors, schema, data) {
|
|
171
|
+
return errors.map((e) => {
|
|
172
|
+
const detail = { path: "/" + e.instancePath.join("/") };
|
|
173
|
+
const schemaValue = walkPath(schema, e.schemaPath);
|
|
174
|
+
if (typeof schemaValue === "string") detail.expected = schemaValue;
|
|
175
|
+
const actualValue = walkPath(data, e.instancePath);
|
|
176
|
+
if (actualValue !== void 0) detail.actual = typeof actualValue;
|
|
177
|
+
else if (e.instancePath.length === 0) detail.actual = typeof data;
|
|
178
|
+
return detail;
|
|
179
|
+
});
|
|
180
|
+
}
|
|
160
181
|
|
|
161
182
|
//#endregion
|
|
162
183
|
//#region src/errors.ts
|
|
@@ -171,20 +192,24 @@ const DEFAULT_STATUS = {
|
|
|
171
192
|
var SeamError = class extends Error {
|
|
172
193
|
code;
|
|
173
194
|
status;
|
|
174
|
-
|
|
195
|
+
details;
|
|
196
|
+
constructor(code, message, status, details) {
|
|
175
197
|
super(message);
|
|
176
198
|
this.code = code;
|
|
177
199
|
this.status = status ?? DEFAULT_STATUS[code] ?? 500;
|
|
200
|
+
this.details = details;
|
|
178
201
|
this.name = "SeamError";
|
|
179
202
|
}
|
|
180
203
|
toJSON() {
|
|
204
|
+
const error = {
|
|
205
|
+
code: this.code,
|
|
206
|
+
message: this.message,
|
|
207
|
+
transient: false
|
|
208
|
+
};
|
|
209
|
+
if (this.details) error.details = this.details;
|
|
181
210
|
return {
|
|
182
211
|
ok: false,
|
|
183
|
-
error
|
|
184
|
-
code: this.code,
|
|
185
|
-
message: this.message,
|
|
186
|
-
transient: false
|
|
187
|
-
}
|
|
212
|
+
error
|
|
188
213
|
};
|
|
189
214
|
}
|
|
190
215
|
};
|
|
@@ -203,14 +228,40 @@ function parseExtractRule(rule) {
|
|
|
203
228
|
key
|
|
204
229
|
};
|
|
205
230
|
}
|
|
206
|
-
/**
|
|
207
|
-
function
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
231
|
+
/** Check whether any context fields are defined */
|
|
232
|
+
function contextHasExtracts(config) {
|
|
233
|
+
return Object.keys(config).length > 0;
|
|
234
|
+
}
|
|
235
|
+
/** Parse a Cookie header into key-value pairs */
|
|
236
|
+
function parseCookieHeader(header) {
|
|
237
|
+
const result = {};
|
|
238
|
+
for (const pair of header.split(";")) {
|
|
239
|
+
const idx = pair.indexOf("=");
|
|
240
|
+
if (idx > 0) result[pair.slice(0, idx).trim()] = pair.slice(idx + 1).trim();
|
|
212
241
|
}
|
|
213
|
-
return
|
|
242
|
+
return result;
|
|
243
|
+
}
|
|
244
|
+
/** Build a RawContextMap keyed by config key from request headers, cookies, and query params */
|
|
245
|
+
function buildRawContext(config, headerFn, url) {
|
|
246
|
+
const raw = {};
|
|
247
|
+
let cookieCache;
|
|
248
|
+
for (const [key, field] of Object.entries(config)) {
|
|
249
|
+
const { source, key: extractKey } = parseExtractRule(field.extract);
|
|
250
|
+
switch (source) {
|
|
251
|
+
case "header":
|
|
252
|
+
raw[key] = headerFn?.(extractKey) ?? null;
|
|
253
|
+
break;
|
|
254
|
+
case "cookie":
|
|
255
|
+
if (!cookieCache) cookieCache = parseCookieHeader(headerFn?.("cookie") ?? "");
|
|
256
|
+
raw[key] = cookieCache[extractKey] ?? null;
|
|
257
|
+
break;
|
|
258
|
+
case "query":
|
|
259
|
+
raw[key] = url.searchParams.get(extractKey) ?? null;
|
|
260
|
+
break;
|
|
261
|
+
default: raw[key] = null;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return raw;
|
|
214
265
|
}
|
|
215
266
|
/**
|
|
216
267
|
* Resolve raw strings into validated context object.
|
|
@@ -225,8 +276,7 @@ function resolveContext(config, raw, requestedKeys) {
|
|
|
225
276
|
for (const key of requestedKeys) {
|
|
226
277
|
const field = config[key];
|
|
227
278
|
if (!field) throw new SeamError("CONTEXT_ERROR", `Context field "${key}" is not defined in router context config`, 400);
|
|
228
|
-
const
|
|
229
|
-
const rawValue = raw[source === "header" ? extractKey : extractKey] ?? null;
|
|
279
|
+
const rawValue = raw[key] ?? null;
|
|
230
280
|
let value;
|
|
231
281
|
if (rawValue === null) value = null;
|
|
232
282
|
else {
|
|
@@ -246,6 +296,12 @@ function resolveContext(config, raw, requestedKeys) {
|
|
|
246
296
|
}
|
|
247
297
|
return result;
|
|
248
298
|
}
|
|
299
|
+
/** Syntax sugar for building extract rules; the underlying "source:key" format is unchanged. */
|
|
300
|
+
const extract = {
|
|
301
|
+
header: (name) => `header:${name}`,
|
|
302
|
+
cookie: (name) => `cookie:${name}`,
|
|
303
|
+
query: (name) => `query:${name}`
|
|
304
|
+
};
|
|
249
305
|
|
|
250
306
|
//#endregion
|
|
251
307
|
//#region src/manifest/index.ts
|
|
@@ -292,19 +348,87 @@ function buildManifest(definitions, channels, contextConfig, transportDefaults)
|
|
|
292
348
|
return manifest;
|
|
293
349
|
}
|
|
294
350
|
|
|
351
|
+
//#endregion
|
|
352
|
+
//#region src/page/route-matcher.ts
|
|
353
|
+
function compileRoute(pattern) {
|
|
354
|
+
return { segments: pattern.split("/").filter(Boolean).map((seg) => {
|
|
355
|
+
if (seg.startsWith("*")) {
|
|
356
|
+
const optional = seg.endsWith("?");
|
|
357
|
+
return {
|
|
358
|
+
kind: "catch-all",
|
|
359
|
+
name: optional ? seg.slice(1, -1) : seg.slice(1),
|
|
360
|
+
optional
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
if (seg.startsWith(":")) return {
|
|
364
|
+
kind: "param",
|
|
365
|
+
name: seg.slice(1)
|
|
366
|
+
};
|
|
367
|
+
return {
|
|
368
|
+
kind: "static",
|
|
369
|
+
value: seg
|
|
370
|
+
};
|
|
371
|
+
}) };
|
|
372
|
+
}
|
|
373
|
+
function matchRoute(segments, pathParts) {
|
|
374
|
+
const params = {};
|
|
375
|
+
for (let i = 0; i < segments.length; i++) {
|
|
376
|
+
const seg = segments[i];
|
|
377
|
+
if (seg.kind === "catch-all") {
|
|
378
|
+
const rest = pathParts.slice(i);
|
|
379
|
+
if (rest.length === 0 && !seg.optional) return null;
|
|
380
|
+
params[seg.name] = rest.join("/");
|
|
381
|
+
return params;
|
|
382
|
+
}
|
|
383
|
+
if (i >= pathParts.length) return null;
|
|
384
|
+
if (seg.kind === "static") {
|
|
385
|
+
if (seg.value !== pathParts[i]) return null;
|
|
386
|
+
} else params[seg.name] = pathParts[i];
|
|
387
|
+
}
|
|
388
|
+
if (segments.length !== pathParts.length) return null;
|
|
389
|
+
return params;
|
|
390
|
+
}
|
|
391
|
+
var RouteMatcher = class {
|
|
392
|
+
routes = [];
|
|
393
|
+
add(pattern, value) {
|
|
394
|
+
this.routes.push({
|
|
395
|
+
pattern,
|
|
396
|
+
compiled: compileRoute(pattern),
|
|
397
|
+
value
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
match(path) {
|
|
401
|
+
const parts = path.split("/").filter(Boolean);
|
|
402
|
+
for (const route of this.routes) {
|
|
403
|
+
const params = matchRoute(route.compiled.segments, parts);
|
|
404
|
+
if (params) return {
|
|
405
|
+
value: route.value,
|
|
406
|
+
params,
|
|
407
|
+
pattern: route.pattern
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
|
|
295
414
|
//#endregion
|
|
296
415
|
//#region src/router/handler.ts
|
|
297
|
-
async function handleRequest(procedures, procedureName, rawBody, validateOutput, ctx) {
|
|
416
|
+
async function handleRequest(procedures, procedureName, rawBody, shouldValidateInput = true, validateOutput, ctx) {
|
|
298
417
|
const procedure = procedures.get(procedureName);
|
|
299
418
|
if (!procedure) return {
|
|
300
419
|
status: 404,
|
|
301
420
|
body: new SeamError("NOT_FOUND", `Procedure '${procedureName}' not found`).toJSON()
|
|
302
421
|
};
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
422
|
+
if (shouldValidateInput) {
|
|
423
|
+
const validation = validateInput(procedure.inputSchema, rawBody);
|
|
424
|
+
if (!validation.valid) {
|
|
425
|
+
const details = formatValidationDetails(validation.errors, procedure.inputSchema, rawBody);
|
|
426
|
+
return {
|
|
427
|
+
status: 400,
|
|
428
|
+
body: new SeamError("VALIDATION_ERROR", `Input validation failed for procedure '${procedureName}': ${formatValidationErrors(validation.errors)}`, void 0, details).toJSON()
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
}
|
|
308
432
|
try {
|
|
309
433
|
const result = await procedure.handler({
|
|
310
434
|
input: rawBody,
|
|
@@ -335,10 +459,10 @@ async function handleRequest(procedures, procedureName, rawBody, validateOutput,
|
|
|
335
459
|
};
|
|
336
460
|
}
|
|
337
461
|
}
|
|
338
|
-
async function handleBatchRequest(procedures, calls, validateOutput, ctxResolver) {
|
|
462
|
+
async function handleBatchRequest(procedures, calls, shouldValidateInput = true, validateOutput, ctxResolver) {
|
|
339
463
|
return { results: await Promise.all(calls.map(async (call) => {
|
|
340
464
|
const ctx = ctxResolver ? ctxResolver(call.procedure) : void 0;
|
|
341
|
-
const result = await handleRequest(procedures, call.procedure, call.input, validateOutput, ctx);
|
|
465
|
+
const result = await handleRequest(procedures, call.procedure, call.input, shouldValidateInput, validateOutput, ctx);
|
|
342
466
|
if (result.status === 200) return {
|
|
343
467
|
ok: true,
|
|
344
468
|
data: result.body.data
|
|
@@ -349,11 +473,16 @@ async function handleBatchRequest(procedures, calls, validateOutput, ctxResolver
|
|
|
349
473
|
};
|
|
350
474
|
})) };
|
|
351
475
|
}
|
|
352
|
-
async function* handleSubscription(subscriptions, name, rawInput, validateOutput, ctx) {
|
|
476
|
+
async function* handleSubscription(subscriptions, name, rawInput, shouldValidateInput = true, validateOutput, ctx) {
|
|
353
477
|
const sub = subscriptions.get(name);
|
|
354
478
|
if (!sub) throw new SeamError("NOT_FOUND", `Subscription '${name}' not found`);
|
|
355
|
-
|
|
356
|
-
|
|
479
|
+
if (shouldValidateInput) {
|
|
480
|
+
const validation = validateInput(sub.inputSchema, rawInput);
|
|
481
|
+
if (!validation.valid) {
|
|
482
|
+
const details = formatValidationDetails(validation.errors, sub.inputSchema, rawInput);
|
|
483
|
+
throw new SeamError("VALIDATION_ERROR", `Input validation failed: ${formatValidationErrors(validation.errors)}`, void 0, details);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
357
486
|
for await (const value of sub.handler({
|
|
358
487
|
input: rawInput,
|
|
359
488
|
ctx: ctx ?? {}
|
|
@@ -365,17 +494,22 @@ async function* handleSubscription(subscriptions, name, rawInput, validateOutput
|
|
|
365
494
|
yield value;
|
|
366
495
|
}
|
|
367
496
|
}
|
|
368
|
-
async function handleUploadRequest(uploads, procedureName, rawBody, file, validateOutput, ctx) {
|
|
497
|
+
async function handleUploadRequest(uploads, procedureName, rawBody, file, shouldValidateInput = true, validateOutput, ctx) {
|
|
369
498
|
const upload = uploads.get(procedureName);
|
|
370
499
|
if (!upload) return {
|
|
371
500
|
status: 404,
|
|
372
501
|
body: new SeamError("NOT_FOUND", `Procedure '${procedureName}' not found`).toJSON()
|
|
373
502
|
};
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
503
|
+
if (shouldValidateInput) {
|
|
504
|
+
const validation = validateInput(upload.inputSchema, rawBody);
|
|
505
|
+
if (!validation.valid) {
|
|
506
|
+
const details = formatValidationDetails(validation.errors, upload.inputSchema, rawBody);
|
|
507
|
+
return {
|
|
508
|
+
status: 400,
|
|
509
|
+
body: new SeamError("VALIDATION_ERROR", `Input validation failed for procedure '${procedureName}': ${formatValidationErrors(validation.errors)}`, void 0, details).toJSON()
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
}
|
|
379
513
|
try {
|
|
380
514
|
const result = await upload.handler({
|
|
381
515
|
input: rawBody,
|
|
@@ -407,11 +541,16 @@ async function handleUploadRequest(uploads, procedureName, rawBody, file, valida
|
|
|
407
541
|
};
|
|
408
542
|
}
|
|
409
543
|
}
|
|
410
|
-
async function* handleStream(streams, name, rawInput, validateOutput, ctx) {
|
|
544
|
+
async function* handleStream(streams, name, rawInput, shouldValidateInput = true, validateOutput, ctx) {
|
|
411
545
|
const stream = streams.get(name);
|
|
412
546
|
if (!stream) throw new SeamError("NOT_FOUND", `Stream '${name}' not found`);
|
|
413
|
-
|
|
414
|
-
|
|
547
|
+
if (shouldValidateInput) {
|
|
548
|
+
const validation = validateInput(stream.inputSchema, rawInput);
|
|
549
|
+
if (!validation.valid) {
|
|
550
|
+
const details = formatValidationDetails(validation.errors, stream.inputSchema, rawInput);
|
|
551
|
+
throw new SeamError("VALIDATION_ERROR", `Input validation failed: ${formatValidationErrors(validation.errors)}`, void 0, details);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
415
554
|
for await (const value of stream.handler({
|
|
416
555
|
input: rawInput,
|
|
417
556
|
ctx: ctx ?? {}
|
|
@@ -424,6 +563,70 @@ async function* handleStream(streams, name, rawInput, validateOutput, ctx) {
|
|
|
424
563
|
}
|
|
425
564
|
}
|
|
426
565
|
|
|
566
|
+
//#endregion
|
|
567
|
+
//#region src/router/categorize.ts
|
|
568
|
+
function resolveKind(name, def) {
|
|
569
|
+
if ("kind" in def && def.kind) return def.kind;
|
|
570
|
+
if ("type" in def && def.type) {
|
|
571
|
+
console.warn(`[seam] "${name}": "type" field in procedure definition is deprecated, use "kind" instead`);
|
|
572
|
+
return def.type;
|
|
573
|
+
}
|
|
574
|
+
return "query";
|
|
575
|
+
}
|
|
576
|
+
/** Split a flat definition map into typed procedure/subscription/stream maps */
|
|
577
|
+
function categorizeProcedures(definitions, contextConfig) {
|
|
578
|
+
const procedureMap = /* @__PURE__ */ new Map();
|
|
579
|
+
const subscriptionMap = /* @__PURE__ */ new Map();
|
|
580
|
+
const streamMap = /* @__PURE__ */ new Map();
|
|
581
|
+
const uploadMap = /* @__PURE__ */ new Map();
|
|
582
|
+
const kindMap = /* @__PURE__ */ new Map();
|
|
583
|
+
for (const [name, def] of Object.entries(definitions)) {
|
|
584
|
+
const kind = resolveKind(name, def);
|
|
585
|
+
kindMap.set(name, kind);
|
|
586
|
+
const contextKeys = def.context ?? [];
|
|
587
|
+
if (contextConfig && contextKeys.length > 0) {
|
|
588
|
+
for (const key of contextKeys) if (!(key in contextConfig)) throw new Error(`Procedure "${name}" references undefined context field "${key}"`);
|
|
589
|
+
}
|
|
590
|
+
if (kind === "upload") uploadMap.set(name, {
|
|
591
|
+
inputSchema: def.input._schema,
|
|
592
|
+
outputSchema: def.output._schema,
|
|
593
|
+
contextKeys,
|
|
594
|
+
handler: def.handler
|
|
595
|
+
});
|
|
596
|
+
else if (kind === "stream") streamMap.set(name, {
|
|
597
|
+
inputSchema: def.input._schema,
|
|
598
|
+
chunkOutputSchema: def.output._schema,
|
|
599
|
+
contextKeys,
|
|
600
|
+
handler: def.handler
|
|
601
|
+
});
|
|
602
|
+
else if (kind === "subscription") subscriptionMap.set(name, {
|
|
603
|
+
inputSchema: def.input._schema,
|
|
604
|
+
outputSchema: def.output._schema,
|
|
605
|
+
contextKeys,
|
|
606
|
+
handler: def.handler
|
|
607
|
+
});
|
|
608
|
+
else procedureMap.set(name, {
|
|
609
|
+
inputSchema: def.input._schema,
|
|
610
|
+
outputSchema: def.output._schema,
|
|
611
|
+
contextKeys,
|
|
612
|
+
handler: def.handler
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
return {
|
|
616
|
+
procedureMap,
|
|
617
|
+
subscriptionMap,
|
|
618
|
+
streamMap,
|
|
619
|
+
uploadMap,
|
|
620
|
+
kindMap
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
//#endregion
|
|
625
|
+
//#region src/page/loader-error.ts
|
|
626
|
+
function isLoaderError(value) {
|
|
627
|
+
return typeof value === "object" && value !== null && value.__error === true && typeof value.code === "string" && typeof value.message === "string";
|
|
628
|
+
}
|
|
629
|
+
|
|
427
630
|
//#endregion
|
|
428
631
|
//#region src/page/projection.ts
|
|
429
632
|
/** Set a nested field by dot-separated path, creating intermediate objects as needed. */
|
|
@@ -479,6 +682,10 @@ function applyProjection(data, projections) {
|
|
|
479
682
|
if (!projections) return data;
|
|
480
683
|
const result = {};
|
|
481
684
|
for (const [key, value] of Object.entries(data)) {
|
|
685
|
+
if (isLoaderError(value)) {
|
|
686
|
+
result[key] = value;
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
482
689
|
const fields = projections[key];
|
|
483
690
|
if (!fields) result[key] = value;
|
|
484
691
|
else result[key] = pruneValue(value, fields);
|
|
@@ -488,19 +695,58 @@ function applyProjection(data, projections) {
|
|
|
488
695
|
|
|
489
696
|
//#endregion
|
|
490
697
|
//#region src/page/handler.ts
|
|
491
|
-
/** Execute loaders, returning keyed results
|
|
492
|
-
|
|
698
|
+
/** Execute loaders, returning keyed results and metadata.
|
|
699
|
+
* Each loader is wrapped in its own try-catch so a single failure
|
|
700
|
+
* does not abort sibling loaders — the page renders at 200 with partial data. */
|
|
701
|
+
async function executeLoaders(loaders, params, procedures, searchParams, ctxResolver, shouldValidateInput) {
|
|
493
702
|
const entries = Object.entries(loaders);
|
|
494
703
|
const results = await Promise.all(entries.map(async ([key, loader]) => {
|
|
495
704
|
const { procedure, input } = loader(params, searchParams);
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
705
|
+
try {
|
|
706
|
+
const proc = procedures.get(procedure);
|
|
707
|
+
if (!proc) throw new SeamError("INTERNAL_ERROR", `Procedure '${procedure}' not found`);
|
|
708
|
+
if (shouldValidateInput) {
|
|
709
|
+
const v = validateInput(proc.inputSchema, input);
|
|
710
|
+
if (!v.valid) throw new SeamError("VALIDATION_ERROR", `Input validation failed: ${formatValidationErrors(v.errors)}`);
|
|
711
|
+
}
|
|
712
|
+
const ctx = ctxResolver ? ctxResolver(proc) : {};
|
|
713
|
+
return {
|
|
714
|
+
key,
|
|
715
|
+
result: await proc.handler({
|
|
716
|
+
input,
|
|
717
|
+
ctx
|
|
718
|
+
}),
|
|
719
|
+
procedure,
|
|
720
|
+
input
|
|
721
|
+
};
|
|
722
|
+
} catch (err) {
|
|
723
|
+
const code = err instanceof SeamError ? err.code : "INTERNAL_ERROR";
|
|
724
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
725
|
+
console.error(`[seam] Loader "${key}" failed:`, err);
|
|
726
|
+
return {
|
|
727
|
+
key,
|
|
728
|
+
result: {
|
|
729
|
+
__error: true,
|
|
730
|
+
code,
|
|
731
|
+
message
|
|
732
|
+
},
|
|
733
|
+
procedure,
|
|
734
|
+
input,
|
|
735
|
+
error: true
|
|
736
|
+
};
|
|
737
|
+
}
|
|
502
738
|
}));
|
|
503
|
-
return
|
|
739
|
+
return {
|
|
740
|
+
data: Object.fromEntries(results.map((r) => [r.key, r.result])),
|
|
741
|
+
meta: Object.fromEntries(results.map((r) => {
|
|
742
|
+
const entry = {
|
|
743
|
+
procedure: r.procedure,
|
|
744
|
+
input: r.input
|
|
745
|
+
};
|
|
746
|
+
if (r.error) entry.error = true;
|
|
747
|
+
return [r.key, entry];
|
|
748
|
+
}))
|
|
749
|
+
};
|
|
504
750
|
}
|
|
505
751
|
/** Select the template for a given locale, falling back to the default template */
|
|
506
752
|
function selectTemplate(defaultTemplate, localeTemplates, locale) {
|
|
@@ -518,15 +764,19 @@ function lookupMessages(config, routePattern, locale) {
|
|
|
518
764
|
}
|
|
519
765
|
return config.messages[locale]?.[routeHash] ?? {};
|
|
520
766
|
}
|
|
521
|
-
async function handlePageRequest(page, params, procedures, i18nOpts, searchParams) {
|
|
767
|
+
async function handlePageRequest(page, params, procedures, i18nOpts, searchParams, ctxResolver, shouldValidateInput) {
|
|
522
768
|
try {
|
|
523
769
|
const t0 = performance.now();
|
|
524
770
|
const layoutChain = page.layoutChain ?? [];
|
|
525
771
|
const locale = i18nOpts?.locale;
|
|
526
|
-
const loaderResults = await Promise.all([...layoutChain.map((layout) => executeLoaders(layout.loaders, params, procedures, searchParams)), executeLoaders(page.loaders, params, procedures, searchParams)]);
|
|
772
|
+
const loaderResults = await Promise.all([...layoutChain.map((layout) => executeLoaders(layout.loaders, params, procedures, searchParams, ctxResolver, shouldValidateInput)), executeLoaders(page.loaders, params, procedures, searchParams, ctxResolver, shouldValidateInput)]);
|
|
527
773
|
const t1 = performance.now();
|
|
528
774
|
const allData = {};
|
|
529
|
-
|
|
775
|
+
const allMeta = {};
|
|
776
|
+
for (const { data, meta } of loaderResults) {
|
|
777
|
+
Object.assign(allData, data);
|
|
778
|
+
Object.assign(allMeta, meta);
|
|
779
|
+
}
|
|
530
780
|
const prunedData = applyProjection(allData, page.projections);
|
|
531
781
|
let composedTemplate = selectTemplate(page.template, page.localeTemplates, locale);
|
|
532
782
|
for (let i = layoutChain.length - 1; i >= 0; i--) {
|
|
@@ -539,7 +789,8 @@ async function handlePageRequest(page, params, procedures, i18nOpts, searchParam
|
|
|
539
789
|
loader_keys: Object.keys(l.loaders)
|
|
540
790
|
})),
|
|
541
791
|
data_id: page.dataId ?? "__data",
|
|
542
|
-
head_meta: page.headMeta
|
|
792
|
+
head_meta: page.headMeta,
|
|
793
|
+
loader_metadata: allMeta
|
|
543
794
|
};
|
|
544
795
|
if (page.pageAssets) config.page_assets = page.pageAssets;
|
|
545
796
|
let i18nOptsJson;
|
|
@@ -576,69 +827,6 @@ async function handlePageRequest(page, params, procedures, i18nOpts, searchParam
|
|
|
576
827
|
}
|
|
577
828
|
}
|
|
578
829
|
|
|
579
|
-
//#endregion
|
|
580
|
-
//#region src/page/route-matcher.ts
|
|
581
|
-
function compileRoute(pattern) {
|
|
582
|
-
return { segments: pattern.split("/").filter(Boolean).map((seg) => {
|
|
583
|
-
if (seg.startsWith("*")) {
|
|
584
|
-
const optional = seg.endsWith("?");
|
|
585
|
-
return {
|
|
586
|
-
kind: "catch-all",
|
|
587
|
-
name: optional ? seg.slice(1, -1) : seg.slice(1),
|
|
588
|
-
optional
|
|
589
|
-
};
|
|
590
|
-
}
|
|
591
|
-
if (seg.startsWith(":")) return {
|
|
592
|
-
kind: "param",
|
|
593
|
-
name: seg.slice(1)
|
|
594
|
-
};
|
|
595
|
-
return {
|
|
596
|
-
kind: "static",
|
|
597
|
-
value: seg
|
|
598
|
-
};
|
|
599
|
-
}) };
|
|
600
|
-
}
|
|
601
|
-
function matchRoute(segments, pathParts) {
|
|
602
|
-
const params = {};
|
|
603
|
-
for (let i = 0; i < segments.length; i++) {
|
|
604
|
-
const seg = segments[i];
|
|
605
|
-
if (seg.kind === "catch-all") {
|
|
606
|
-
const rest = pathParts.slice(i);
|
|
607
|
-
if (rest.length === 0 && !seg.optional) return null;
|
|
608
|
-
params[seg.name] = rest.join("/");
|
|
609
|
-
return params;
|
|
610
|
-
}
|
|
611
|
-
if (i >= pathParts.length) return null;
|
|
612
|
-
if (seg.kind === "static") {
|
|
613
|
-
if (seg.value !== pathParts[i]) return null;
|
|
614
|
-
} else params[seg.name] = pathParts[i];
|
|
615
|
-
}
|
|
616
|
-
if (segments.length !== pathParts.length) return null;
|
|
617
|
-
return params;
|
|
618
|
-
}
|
|
619
|
-
var RouteMatcher = class {
|
|
620
|
-
routes = [];
|
|
621
|
-
add(pattern, value) {
|
|
622
|
-
this.routes.push({
|
|
623
|
-
pattern,
|
|
624
|
-
compiled: compileRoute(pattern),
|
|
625
|
-
value
|
|
626
|
-
});
|
|
627
|
-
}
|
|
628
|
-
match(path) {
|
|
629
|
-
const parts = path.split("/").filter(Boolean);
|
|
630
|
-
for (const route of this.routes) {
|
|
631
|
-
const params = matchRoute(route.compiled.segments, parts);
|
|
632
|
-
if (params) return {
|
|
633
|
-
value: route.value,
|
|
634
|
-
params,
|
|
635
|
-
pattern: route.pattern
|
|
636
|
-
};
|
|
637
|
-
}
|
|
638
|
-
return null;
|
|
639
|
-
}
|
|
640
|
-
};
|
|
641
|
-
|
|
642
830
|
//#endregion
|
|
643
831
|
//#region src/resolve.ts
|
|
644
832
|
/** URL prefix strategy: trusts pathLocale if it is a known locale */
|
|
@@ -730,65 +918,14 @@ function defaultStrategies() {
|
|
|
730
918
|
}
|
|
731
919
|
|
|
732
920
|
//#endregion
|
|
733
|
-
//#region src/router/
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
return "query";
|
|
921
|
+
//#region src/router/helpers.ts
|
|
922
|
+
/** Resolve a ValidationMode to a boolean flag */
|
|
923
|
+
function resolveValidationMode(mode) {
|
|
924
|
+
const m = mode ?? "dev";
|
|
925
|
+
if (m === "always") return true;
|
|
926
|
+
if (m === "never") return false;
|
|
927
|
+
return typeof process !== "undefined" && process.env.NODE_ENV !== "production";
|
|
741
928
|
}
|
|
742
|
-
/** Split a flat definition map into typed procedure/subscription/stream maps */
|
|
743
|
-
function categorizeProcedures(definitions, contextConfig) {
|
|
744
|
-
const procedureMap = /* @__PURE__ */ new Map();
|
|
745
|
-
const subscriptionMap = /* @__PURE__ */ new Map();
|
|
746
|
-
const streamMap = /* @__PURE__ */ new Map();
|
|
747
|
-
const uploadMap = /* @__PURE__ */ new Map();
|
|
748
|
-
const kindMap = /* @__PURE__ */ new Map();
|
|
749
|
-
for (const [name, def] of Object.entries(definitions)) {
|
|
750
|
-
const kind = resolveKind(name, def);
|
|
751
|
-
kindMap.set(name, kind);
|
|
752
|
-
const contextKeys = def.context ?? [];
|
|
753
|
-
if (contextConfig && contextKeys.length > 0) {
|
|
754
|
-
for (const key of contextKeys) if (!(key in contextConfig)) throw new Error(`Procedure "${name}" references undefined context field "${key}"`);
|
|
755
|
-
}
|
|
756
|
-
if (kind === "upload") uploadMap.set(name, {
|
|
757
|
-
inputSchema: def.input._schema,
|
|
758
|
-
outputSchema: def.output._schema,
|
|
759
|
-
contextKeys,
|
|
760
|
-
handler: def.handler
|
|
761
|
-
});
|
|
762
|
-
else if (kind === "stream") streamMap.set(name, {
|
|
763
|
-
inputSchema: def.input._schema,
|
|
764
|
-
chunkOutputSchema: def.output._schema,
|
|
765
|
-
contextKeys,
|
|
766
|
-
handler: def.handler
|
|
767
|
-
});
|
|
768
|
-
else if (kind === "subscription") subscriptionMap.set(name, {
|
|
769
|
-
inputSchema: def.input._schema,
|
|
770
|
-
outputSchema: def.output._schema,
|
|
771
|
-
contextKeys,
|
|
772
|
-
handler: def.handler
|
|
773
|
-
});
|
|
774
|
-
else procedureMap.set(name, {
|
|
775
|
-
inputSchema: def.input._schema,
|
|
776
|
-
outputSchema: def.output._schema,
|
|
777
|
-
contextKeys,
|
|
778
|
-
handler: def.handler
|
|
779
|
-
});
|
|
780
|
-
}
|
|
781
|
-
return {
|
|
782
|
-
procedureMap,
|
|
783
|
-
subscriptionMap,
|
|
784
|
-
streamMap,
|
|
785
|
-
uploadMap,
|
|
786
|
-
kindMap
|
|
787
|
-
};
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
//#endregion
|
|
791
|
-
//#region src/router/index.ts
|
|
792
929
|
/** Build the resolve strategy list from options */
|
|
793
930
|
function buildStrategies(opts) {
|
|
794
931
|
const strategies = opts?.resolve ?? defaultStrategies();
|
|
@@ -831,14 +968,14 @@ function collectChannelMeta(channels) {
|
|
|
831
968
|
}));
|
|
832
969
|
}
|
|
833
970
|
/** Resolve context for a procedure, returning undefined if no context needed */
|
|
834
|
-
function resolveCtxFor(map, name, rawCtx,
|
|
835
|
-
if (!rawCtx
|
|
971
|
+
function resolveCtxFor(map, name, rawCtx, ctxConfig) {
|
|
972
|
+
if (!rawCtx) return void 0;
|
|
836
973
|
const proc = map.get(name);
|
|
837
974
|
if (!proc || proc.contextKeys.length === 0) return void 0;
|
|
838
975
|
return resolveContext(ctxConfig, rawCtx, proc.contextKeys);
|
|
839
976
|
}
|
|
840
977
|
/** Resolve locale and match page route */
|
|
841
|
-
async function matchAndHandlePage(pageMatcher, procedureMap, i18nConfig, strategies, hasUrlPrefix, path, headers) {
|
|
978
|
+
async function matchAndHandlePage(pageMatcher, procedureMap, i18nConfig, strategies, hasUrlPrefix, path, headers, rawCtx, ctxConfig, shouldValidateInput) {
|
|
842
979
|
let pathLocale = null;
|
|
843
980
|
let routePath = path;
|
|
844
981
|
if (hasUrlPrefix && i18nConfig) {
|
|
@@ -871,12 +1008,16 @@ async function matchAndHandlePage(pageMatcher, procedureMap, i18nConfig, strateg
|
|
|
871
1008
|
config: i18nConfig,
|
|
872
1009
|
routePattern: match.pattern
|
|
873
1010
|
} : void 0;
|
|
874
|
-
|
|
1011
|
+
const ctxResolver = rawCtx ? (proc) => {
|
|
1012
|
+
if (proc.contextKeys.length === 0) return {};
|
|
1013
|
+
return resolveContext(ctxConfig ?? {}, rawCtx, proc.contextKeys);
|
|
1014
|
+
} : void 0;
|
|
1015
|
+
return handlePageRequest(match.value, match.params, procedureMap, i18nOpts, searchParams, ctxResolver, shouldValidateInput);
|
|
875
1016
|
}
|
|
876
1017
|
/** Catch context resolution errors and return them as HandleResult */
|
|
877
|
-
function resolveCtxSafe(map, name, rawCtx,
|
|
1018
|
+
function resolveCtxSafe(map, name, rawCtx, ctxConfig) {
|
|
878
1019
|
try {
|
|
879
|
-
return { ctx: resolveCtxFor(map, name, rawCtx,
|
|
1020
|
+
return { ctx: resolveCtxFor(map, name, rawCtx, ctxConfig) };
|
|
880
1021
|
} catch (err) {
|
|
881
1022
|
if (err instanceof SeamError) return { error: {
|
|
882
1023
|
status: err.status,
|
|
@@ -885,9 +1026,14 @@ function resolveCtxSafe(map, name, rawCtx, extractKeys, ctxConfig) {
|
|
|
885
1026
|
throw err;
|
|
886
1027
|
}
|
|
887
1028
|
}
|
|
888
|
-
|
|
1029
|
+
|
|
1030
|
+
//#endregion
|
|
1031
|
+
//#region src/router/state.ts
|
|
1032
|
+
/** Build all shared state that createRouter methods close over */
|
|
1033
|
+
function initRouterState(procedures, opts) {
|
|
889
1034
|
const ctxConfig = opts?.context ?? {};
|
|
890
1035
|
const { procedureMap, subscriptionMap, streamMap, uploadMap, kindMap } = categorizeProcedures(procedures, Object.keys(ctxConfig).length > 0 ? ctxConfig : void 0);
|
|
1036
|
+
const shouldValidateInput = resolveValidationMode(opts?.validation?.input);
|
|
891
1037
|
const shouldValidateOutput = opts?.validateOutput ?? (typeof process !== "undefined" && process.env.NODE_ENV !== "production");
|
|
892
1038
|
const pageMatcher = new RouteMatcher();
|
|
893
1039
|
const pages = opts?.pages;
|
|
@@ -895,43 +1041,163 @@ function createRouter(procedures, opts) {
|
|
|
895
1041
|
const i18nConfig = opts?.i18n ?? null;
|
|
896
1042
|
const { strategies, hasUrlPrefix } = buildStrategies(opts);
|
|
897
1043
|
if (i18nConfig) registerI18nQuery(procedureMap, i18nConfig);
|
|
898
|
-
const channelsMeta = collectChannelMeta(opts?.channels);
|
|
899
|
-
const extractKeys = contextExtractKeys(ctxConfig);
|
|
900
1044
|
return {
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
1045
|
+
ctxConfig,
|
|
1046
|
+
procedureMap,
|
|
1047
|
+
subscriptionMap,
|
|
1048
|
+
streamMap,
|
|
1049
|
+
uploadMap,
|
|
1050
|
+
kindMap,
|
|
1051
|
+
shouldValidateInput,
|
|
1052
|
+
shouldValidateOutput,
|
|
1053
|
+
pageMatcher,
|
|
1054
|
+
pages,
|
|
1055
|
+
i18nConfig,
|
|
1056
|
+
strategies,
|
|
1057
|
+
hasUrlPrefix,
|
|
1058
|
+
channelsMeta: collectChannelMeta(opts?.channels),
|
|
1059
|
+
hasCtx: contextHasExtracts(ctxConfig)
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
/** Build request-response methods: handle, handleBatch, handleUpload */
|
|
1063
|
+
function buildRpcMethods(state) {
|
|
1064
|
+
return {
|
|
909
1065
|
async handle(procedureName, body, rawCtx) {
|
|
910
|
-
const { ctx, error } = resolveCtxSafe(procedureMap, procedureName, rawCtx,
|
|
1066
|
+
const { ctx, error } = resolveCtxSafe(state.procedureMap, procedureName, rawCtx, state.ctxConfig);
|
|
911
1067
|
if (error) return error;
|
|
912
|
-
return handleRequest(procedureMap, procedureName, body, shouldValidateOutput, ctx);
|
|
1068
|
+
return handleRequest(state.procedureMap, procedureName, body, state.shouldValidateInput, state.shouldValidateOutput, ctx);
|
|
913
1069
|
},
|
|
914
1070
|
handleBatch(calls, rawCtx) {
|
|
915
|
-
|
|
1071
|
+
const ctxResolver = rawCtx ? (name) => resolveCtxFor(state.procedureMap, name, rawCtx, state.ctxConfig) ?? {} : void 0;
|
|
1072
|
+
return handleBatchRequest(state.procedureMap, calls, state.shouldValidateInput, state.shouldValidateOutput, ctxResolver);
|
|
1073
|
+
},
|
|
1074
|
+
async handleUpload(name, body, file, rawCtx) {
|
|
1075
|
+
const { ctx, error } = resolveCtxSafe(state.uploadMap, name, rawCtx, state.ctxConfig);
|
|
1076
|
+
if (error) return error;
|
|
1077
|
+
return handleUploadRequest(state.uploadMap, name, body, file, state.shouldValidateInput, state.shouldValidateOutput, ctx);
|
|
1078
|
+
}
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
/** Build all Router method implementations from shared state */
|
|
1082
|
+
function buildRouterMethods(state, procedures, opts) {
|
|
1083
|
+
return {
|
|
1084
|
+
hasPages: !!state.pages && Object.keys(state.pages).length > 0,
|
|
1085
|
+
ctxConfig: state.ctxConfig,
|
|
1086
|
+
hasContext() {
|
|
1087
|
+
return state.hasCtx;
|
|
916
1088
|
},
|
|
1089
|
+
manifest() {
|
|
1090
|
+
return buildManifest(procedures, state.channelsMeta, state.ctxConfig, opts?.transportDefaults);
|
|
1091
|
+
},
|
|
1092
|
+
...buildRpcMethods(state),
|
|
917
1093
|
handleSubscription(name, input, rawCtx) {
|
|
918
|
-
|
|
1094
|
+
const ctx = resolveCtxFor(state.subscriptionMap, name, rawCtx, state.ctxConfig);
|
|
1095
|
+
return handleSubscription(state.subscriptionMap, name, input, state.shouldValidateInput, state.shouldValidateOutput, ctx);
|
|
919
1096
|
},
|
|
920
1097
|
handleStream(name, input, rawCtx) {
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
async handleUpload(name, body, file, rawCtx) {
|
|
924
|
-
const { ctx, error } = resolveCtxSafe(uploadMap, name, rawCtx, extractKeys, ctxConfig);
|
|
925
|
-
if (error) return error;
|
|
926
|
-
return handleUploadRequest(uploadMap, name, body, file, shouldValidateOutput, ctx);
|
|
1098
|
+
const ctx = resolveCtxFor(state.streamMap, name, rawCtx, state.ctxConfig);
|
|
1099
|
+
return handleStream(state.streamMap, name, input, state.shouldValidateInput, state.shouldValidateOutput, ctx);
|
|
927
1100
|
},
|
|
928
1101
|
getKind(name) {
|
|
929
|
-
return kindMap.get(name) ?? null;
|
|
1102
|
+
return state.kindMap.get(name) ?? null;
|
|
1103
|
+
},
|
|
1104
|
+
handlePage(path, headers, rawCtx) {
|
|
1105
|
+
return matchAndHandlePage(state.pageMatcher, state.procedureMap, state.i18nConfig, state.strategies, state.hasUrlPrefix, path, headers, rawCtx, state.ctxConfig, state.shouldValidateInput);
|
|
1106
|
+
}
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
//#endregion
|
|
1111
|
+
//#region src/router/index.ts
|
|
1112
|
+
function createRouter(procedures, opts) {
|
|
1113
|
+
const state = initRouterState(procedures, opts);
|
|
1114
|
+
return {
|
|
1115
|
+
procedures,
|
|
1116
|
+
rpcHashMap: opts?.rpcHashMap,
|
|
1117
|
+
...buildRouterMethods(state, procedures, opts)
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
//#endregion
|
|
1122
|
+
//#region src/factory.ts
|
|
1123
|
+
function query(def) {
|
|
1124
|
+
return {
|
|
1125
|
+
...def,
|
|
1126
|
+
kind: "query"
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
function command(def) {
|
|
1130
|
+
return {
|
|
1131
|
+
...def,
|
|
1132
|
+
kind: "command"
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
function subscription(def) {
|
|
1136
|
+
return {
|
|
1137
|
+
...def,
|
|
1138
|
+
kind: "subscription"
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
function stream(def) {
|
|
1142
|
+
return {
|
|
1143
|
+
...def,
|
|
1144
|
+
kind: "stream"
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
function upload(def) {
|
|
1148
|
+
return {
|
|
1149
|
+
...def,
|
|
1150
|
+
kind: "upload"
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
//#endregion
|
|
1155
|
+
//#region src/seam-router.ts
|
|
1156
|
+
function createSeamRouter(config) {
|
|
1157
|
+
const { context, ...restConfig } = config;
|
|
1158
|
+
const define = {
|
|
1159
|
+
query(def) {
|
|
1160
|
+
return {
|
|
1161
|
+
...def,
|
|
1162
|
+
kind: "query"
|
|
1163
|
+
};
|
|
930
1164
|
},
|
|
931
|
-
|
|
932
|
-
return
|
|
1165
|
+
command(def) {
|
|
1166
|
+
return {
|
|
1167
|
+
...def,
|
|
1168
|
+
kind: "command"
|
|
1169
|
+
};
|
|
1170
|
+
},
|
|
1171
|
+
subscription(def) {
|
|
1172
|
+
return {
|
|
1173
|
+
...def,
|
|
1174
|
+
kind: "subscription"
|
|
1175
|
+
};
|
|
1176
|
+
},
|
|
1177
|
+
stream(def) {
|
|
1178
|
+
return {
|
|
1179
|
+
...def,
|
|
1180
|
+
kind: "stream"
|
|
1181
|
+
};
|
|
1182
|
+
},
|
|
1183
|
+
upload(def) {
|
|
1184
|
+
return {
|
|
1185
|
+
...def,
|
|
1186
|
+
kind: "upload"
|
|
1187
|
+
};
|
|
933
1188
|
}
|
|
934
1189
|
};
|
|
1190
|
+
function router(procedures, extraOpts) {
|
|
1191
|
+
return createRouter(procedures, {
|
|
1192
|
+
...restConfig,
|
|
1193
|
+
context,
|
|
1194
|
+
...extraOpts
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
return {
|
|
1198
|
+
router,
|
|
1199
|
+
define
|
|
1200
|
+
};
|
|
935
1201
|
}
|
|
936
1202
|
|
|
937
1203
|
//#endregion
|
|
@@ -1102,13 +1368,77 @@ function sseErrorEvent(code, message, transient = false) {
|
|
|
1102
1368
|
function sseCompleteEvent() {
|
|
1103
1369
|
return "event: complete\ndata: {}\n\n";
|
|
1104
1370
|
}
|
|
1371
|
+
function formatSseError(error) {
|
|
1372
|
+
if (error instanceof SeamError) return sseErrorEvent(error.code, error.message);
|
|
1373
|
+
return sseErrorEvent("INTERNAL_ERROR", error instanceof Error ? error.message : "Unknown error");
|
|
1374
|
+
}
|
|
1375
|
+
const DEFAULT_HEARTBEAT_MS$1 = 21e3;
|
|
1376
|
+
const DEFAULT_SSE_IDLE_MS = 3e4;
|
|
1377
|
+
async function* withSseLifecycle(inner, opts) {
|
|
1378
|
+
const heartbeatMs = opts?.heartbeatInterval ?? DEFAULT_HEARTBEAT_MS$1;
|
|
1379
|
+
const idleMs = opts?.sseIdleTimeout ?? DEFAULT_SSE_IDLE_MS;
|
|
1380
|
+
const idleEnabled = idleMs > 0;
|
|
1381
|
+
const queue = [];
|
|
1382
|
+
let resolve = null;
|
|
1383
|
+
const signal = () => {
|
|
1384
|
+
if (resolve) {
|
|
1385
|
+
resolve();
|
|
1386
|
+
resolve = null;
|
|
1387
|
+
}
|
|
1388
|
+
};
|
|
1389
|
+
let idleTimer = null;
|
|
1390
|
+
const resetIdle = () => {
|
|
1391
|
+
if (!idleEnabled) return;
|
|
1392
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
1393
|
+
idleTimer = setTimeout(() => {
|
|
1394
|
+
queue.push({ type: "idle" });
|
|
1395
|
+
signal();
|
|
1396
|
+
}, idleMs);
|
|
1397
|
+
};
|
|
1398
|
+
const heartbeatTimer = setInterval(() => {
|
|
1399
|
+
queue.push({ type: "heartbeat" });
|
|
1400
|
+
signal();
|
|
1401
|
+
}, heartbeatMs);
|
|
1402
|
+
resetIdle();
|
|
1403
|
+
(async () => {
|
|
1404
|
+
try {
|
|
1405
|
+
for await (const chunk of inner) {
|
|
1406
|
+
queue.push({
|
|
1407
|
+
type: "data",
|
|
1408
|
+
value: chunk
|
|
1409
|
+
});
|
|
1410
|
+
resetIdle();
|
|
1411
|
+
signal();
|
|
1412
|
+
}
|
|
1413
|
+
} catch {}
|
|
1414
|
+
queue.push({ type: "done" });
|
|
1415
|
+
signal();
|
|
1416
|
+
})();
|
|
1417
|
+
try {
|
|
1418
|
+
for (;;) {
|
|
1419
|
+
while (queue.length === 0) await new Promise((r) => {
|
|
1420
|
+
resolve = r;
|
|
1421
|
+
});
|
|
1422
|
+
const item = queue.shift();
|
|
1423
|
+
if (!item) continue;
|
|
1424
|
+
if (item.type === "data") yield item.value;
|
|
1425
|
+
else if (item.type === "heartbeat") yield ": heartbeat\n\n";
|
|
1426
|
+
else if (item.type === "idle") {
|
|
1427
|
+
yield sseCompleteEvent();
|
|
1428
|
+
return;
|
|
1429
|
+
} else return;
|
|
1430
|
+
}
|
|
1431
|
+
} finally {
|
|
1432
|
+
clearInterval(heartbeatTimer);
|
|
1433
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1105
1436
|
async function* sseStream(router, name, input, rawCtx) {
|
|
1106
1437
|
try {
|
|
1107
1438
|
for await (const value of router.handleSubscription(name, input, rawCtx)) yield sseDataEvent(value);
|
|
1108
1439
|
yield sseCompleteEvent();
|
|
1109
1440
|
} catch (error) {
|
|
1110
|
-
|
|
1111
|
-
else yield sseErrorEvent("INTERNAL_ERROR", error instanceof Error ? error.message : "Unknown error");
|
|
1441
|
+
yield formatSseError(error);
|
|
1112
1442
|
}
|
|
1113
1443
|
}
|
|
1114
1444
|
async function* sseStreamForStream(router, name, input, signal, rawCtx) {
|
|
@@ -1121,8 +1451,7 @@ async function* sseStreamForStream(router, name, input, signal, rawCtx) {
|
|
|
1121
1451
|
for await (const value of gen) yield sseDataEventWithId(value, seq++);
|
|
1122
1452
|
yield sseCompleteEvent();
|
|
1123
1453
|
} catch (error) {
|
|
1124
|
-
|
|
1125
|
-
else yield sseErrorEvent("INTERNAL_ERROR", error instanceof Error ? error.message : "Unknown error");
|
|
1454
|
+
yield formatSseError(error);
|
|
1126
1455
|
}
|
|
1127
1456
|
}
|
|
1128
1457
|
async function handleBatchHttp(req, router, hashToName, rawCtx) {
|
|
@@ -1142,22 +1471,49 @@ async function handleBatchHttp(req, router, hashToName, rawCtx) {
|
|
|
1142
1471
|
data: await router.handleBatch(calls, rawCtx)
|
|
1143
1472
|
});
|
|
1144
1473
|
}
|
|
1145
|
-
/** Resolve hash -> original name when obfuscation is active.
|
|
1474
|
+
/** Resolve hash -> original name when obfuscation is active. Accepts both hashed and raw names. */
|
|
1146
1475
|
function resolveHashName(hashToName, name) {
|
|
1147
1476
|
if (!hashToName) return name;
|
|
1148
|
-
return hashToName.get(name) ??
|
|
1477
|
+
return hashToName.get(name) ?? name;
|
|
1478
|
+
}
|
|
1479
|
+
async function handleProcedurePost(req, router, name, rawCtx, sseOptions) {
|
|
1480
|
+
let body;
|
|
1481
|
+
try {
|
|
1482
|
+
body = await req.body();
|
|
1483
|
+
} catch {
|
|
1484
|
+
return errorResponse(400, "VALIDATION_ERROR", "Invalid JSON body");
|
|
1485
|
+
}
|
|
1486
|
+
if (router.getKind(name) === "stream") {
|
|
1487
|
+
const controller = new AbortController();
|
|
1488
|
+
return {
|
|
1489
|
+
status: 200,
|
|
1490
|
+
headers: SSE_HEADER,
|
|
1491
|
+
stream: withSseLifecycle(sseStreamForStream(router, name, body, controller.signal, rawCtx), sseOptions),
|
|
1492
|
+
onCancel: () => controller.abort()
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
if (router.getKind(name) === "upload") {
|
|
1496
|
+
if (!req.file) return errorResponse(400, "VALIDATION_ERROR", "Upload requires multipart/form-data");
|
|
1497
|
+
const file = await req.file();
|
|
1498
|
+
if (!file) return errorResponse(400, "VALIDATION_ERROR", "Upload requires file in multipart body");
|
|
1499
|
+
const result = await router.handleUpload(name, body, file, rawCtx);
|
|
1500
|
+
return jsonResponse(result.status, result.body);
|
|
1501
|
+
}
|
|
1502
|
+
const result = await router.handle(name, body, rawCtx);
|
|
1503
|
+
return jsonResponse(result.status, result.body);
|
|
1149
1504
|
}
|
|
1150
1505
|
function createHttpHandler(router, opts) {
|
|
1151
|
-
const
|
|
1506
|
+
const effectiveHashMap = opts?.rpcHashMap ?? router.rpcHashMap;
|
|
1507
|
+
const hashToName = effectiveHashMap ? new Map(Object.entries(effectiveHashMap.procedures).map(([n, h]) => [h, n])) : null;
|
|
1152
1508
|
if (hashToName) hashToName.set("__seam_i18n_query", "__seam_i18n_query");
|
|
1153
|
-
const batchHash =
|
|
1154
|
-
const
|
|
1509
|
+
const batchHash = effectiveHashMap?.batch ?? null;
|
|
1510
|
+
const hasCtx = router.hasContext();
|
|
1155
1511
|
return async (req) => {
|
|
1156
1512
|
const url = new URL(req.url, "http://localhost");
|
|
1157
1513
|
const { pathname } = url;
|
|
1158
|
-
const rawCtx =
|
|
1514
|
+
const rawCtx = hasCtx ? buildRawContext(router.ctxConfig, req.header, url) : void 0;
|
|
1159
1515
|
if (req.method === "GET" && pathname === MANIFEST_PATH) {
|
|
1160
|
-
if (
|
|
1516
|
+
if (effectiveHashMap) return errorResponse(403, "FORBIDDEN", "Manifest disabled");
|
|
1161
1517
|
return jsonResponse(200, router.manifest());
|
|
1162
1518
|
}
|
|
1163
1519
|
if (pathname.startsWith(PROCEDURE_PREFIX)) {
|
|
@@ -1165,36 +1521,10 @@ function createHttpHandler(router, opts) {
|
|
|
1165
1521
|
if (!rawName) return errorResponse(404, "NOT_FOUND", "Empty procedure name");
|
|
1166
1522
|
if (req.method === "POST") {
|
|
1167
1523
|
if (rawName === "_batch" || batchHash && rawName === batchHash) return handleBatchHttp(req, router, hashToName, rawCtx);
|
|
1168
|
-
|
|
1169
|
-
if (!name) return errorResponse(404, "NOT_FOUND", "Not found");
|
|
1170
|
-
let body;
|
|
1171
|
-
try {
|
|
1172
|
-
body = await req.body();
|
|
1173
|
-
} catch {
|
|
1174
|
-
return errorResponse(400, "VALIDATION_ERROR", "Invalid JSON body");
|
|
1175
|
-
}
|
|
1176
|
-
if (router.getKind(name) === "stream") {
|
|
1177
|
-
const controller = new AbortController();
|
|
1178
|
-
return {
|
|
1179
|
-
status: 200,
|
|
1180
|
-
headers: SSE_HEADER,
|
|
1181
|
-
stream: sseStreamForStream(router, name, body, controller.signal, rawCtx),
|
|
1182
|
-
onCancel: () => controller.abort()
|
|
1183
|
-
};
|
|
1184
|
-
}
|
|
1185
|
-
if (router.getKind(name) === "upload") {
|
|
1186
|
-
if (!req.file) return errorResponse(400, "VALIDATION_ERROR", "Upload requires multipart/form-data");
|
|
1187
|
-
const file = await req.file();
|
|
1188
|
-
if (!file) return errorResponse(400, "VALIDATION_ERROR", "Upload requires file in multipart body");
|
|
1189
|
-
const result = await router.handleUpload(name, body, file, rawCtx);
|
|
1190
|
-
return jsonResponse(result.status, result.body);
|
|
1191
|
-
}
|
|
1192
|
-
const result = await router.handle(name, body, rawCtx);
|
|
1193
|
-
return jsonResponse(result.status, result.body);
|
|
1524
|
+
return handleProcedurePost(req, router, resolveHashName(hashToName, rawName), rawCtx, opts?.sseOptions);
|
|
1194
1525
|
}
|
|
1195
1526
|
if (req.method === "GET") {
|
|
1196
1527
|
const name = resolveHashName(hashToName, rawName);
|
|
1197
|
-
if (!name) return errorResponse(404, "NOT_FOUND", "Not found");
|
|
1198
1528
|
const rawInput = url.searchParams.get("input");
|
|
1199
1529
|
let input;
|
|
1200
1530
|
try {
|
|
@@ -1205,7 +1535,7 @@ function createHttpHandler(router, opts) {
|
|
|
1205
1535
|
return {
|
|
1206
1536
|
status: 200,
|
|
1207
1537
|
headers: SSE_HEADER,
|
|
1208
|
-
stream: sseStream(router, name, input, rawCtx)
|
|
1538
|
+
stream: withSseLifecycle(sseStream(router, name, input, rawCtx), opts?.sseOptions)
|
|
1209
1539
|
};
|
|
1210
1540
|
}
|
|
1211
1541
|
}
|
|
@@ -1216,7 +1546,7 @@ function createHttpHandler(router, opts) {
|
|
|
1216
1546
|
cookie: req.header("cookie") ?? void 0,
|
|
1217
1547
|
acceptLanguage: req.header("accept-language") ?? void 0
|
|
1218
1548
|
} : void 0;
|
|
1219
|
-
const result = await router.handlePage(pagePath, headers);
|
|
1549
|
+
const result = await router.handlePage(pagePath, headers, rawCtx);
|
|
1220
1550
|
if (result) return {
|
|
1221
1551
|
status: result.status,
|
|
1222
1552
|
headers: HTML_HEADER,
|
|
@@ -1267,10 +1597,14 @@ function toWebResponse(result) {
|
|
|
1267
1597
|
|
|
1268
1598
|
//#endregion
|
|
1269
1599
|
//#region src/page/build-loader.ts
|
|
1600
|
+
function normalizeParamConfig(value) {
|
|
1601
|
+
return typeof value === "string" ? { from: value } : value;
|
|
1602
|
+
}
|
|
1270
1603
|
function buildLoaderFn(config) {
|
|
1271
1604
|
return (params, searchParams) => {
|
|
1272
1605
|
const input = {};
|
|
1273
|
-
if (config.params) for (const [key,
|
|
1606
|
+
if (config.params) for (const [key, raw_mapping] of Object.entries(config.params)) {
|
|
1607
|
+
const mapping = normalizeParamConfig(raw_mapping);
|
|
1274
1608
|
const raw = mapping.from === "query" ? searchParams?.get(key) ?? void 0 : params[key];
|
|
1275
1609
|
if (raw !== void 0) input[key] = mapping.type === "int" ? Number(raw) : raw;
|
|
1276
1610
|
}
|
|
@@ -1348,6 +1682,22 @@ function mergeI18nKeys(route, layoutEntries) {
|
|
|
1348
1682
|
if (route.i18n_keys) keys.push(...route.i18n_keys);
|
|
1349
1683
|
return keys.length > 0 ? keys : void 0;
|
|
1350
1684
|
}
|
|
1685
|
+
/** Load all build artifacts (pages, rpcHashMap, i18n) in one call */
|
|
1686
|
+
function loadBuild(distDir) {
|
|
1687
|
+
return {
|
|
1688
|
+
pages: loadBuildOutput(distDir),
|
|
1689
|
+
rpcHashMap: loadRpcHashMap(distDir),
|
|
1690
|
+
i18n: loadI18nMessages(distDir)
|
|
1691
|
+
};
|
|
1692
|
+
}
|
|
1693
|
+
/** Load all build artifacts with lazy template getters (for dev mode) */
|
|
1694
|
+
function loadBuildDev(distDir) {
|
|
1695
|
+
return {
|
|
1696
|
+
pages: loadBuildOutputDev(distDir),
|
|
1697
|
+
rpcHashMap: loadRpcHashMap(distDir),
|
|
1698
|
+
i18n: loadI18nMessages(distDir)
|
|
1699
|
+
};
|
|
1700
|
+
}
|
|
1351
1701
|
/** Load the RPC hash map from build output (returns undefined when obfuscation is off) */
|
|
1352
1702
|
function loadRpcHashMap(distDir) {
|
|
1353
1703
|
const hashMapPath = join(distDir, "rpc-hash-map.json");
|
|
@@ -1529,7 +1879,8 @@ function fromCallback(setup) {
|
|
|
1529
1879
|
|
|
1530
1880
|
//#endregion
|
|
1531
1881
|
//#region src/ws.ts
|
|
1532
|
-
const DEFAULT_HEARTBEAT_MS =
|
|
1882
|
+
const DEFAULT_HEARTBEAT_MS = 21e3;
|
|
1883
|
+
const DEFAULT_PONG_TIMEOUT_MS = 5e3;
|
|
1533
1884
|
function sendError(ws, id, code, message) {
|
|
1534
1885
|
ws.send(JSON.stringify({
|
|
1535
1886
|
id,
|
|
@@ -1541,6 +1892,60 @@ function sendError(ws, id, code, message) {
|
|
|
1541
1892
|
}
|
|
1542
1893
|
}));
|
|
1543
1894
|
}
|
|
1895
|
+
/** Validate uplink message fields and channel scope */
|
|
1896
|
+
function parseUplink(ws, data, channelName) {
|
|
1897
|
+
let msg;
|
|
1898
|
+
try {
|
|
1899
|
+
msg = JSON.parse(data);
|
|
1900
|
+
} catch {
|
|
1901
|
+
sendError(ws, null, "VALIDATION_ERROR", "Invalid JSON");
|
|
1902
|
+
return null;
|
|
1903
|
+
}
|
|
1904
|
+
if (!msg.id || typeof msg.id !== "string") {
|
|
1905
|
+
sendError(ws, null, "VALIDATION_ERROR", "Missing 'id' field");
|
|
1906
|
+
return null;
|
|
1907
|
+
}
|
|
1908
|
+
if (!msg.procedure || typeof msg.procedure !== "string") {
|
|
1909
|
+
sendError(ws, msg.id, "VALIDATION_ERROR", "Missing 'procedure' field");
|
|
1910
|
+
return null;
|
|
1911
|
+
}
|
|
1912
|
+
const prefix = channelName + ".";
|
|
1913
|
+
if (!msg.procedure.startsWith(prefix) || msg.procedure === `${channelName}.events`) {
|
|
1914
|
+
sendError(ws, msg.id, "VALIDATION_ERROR", `Procedure '${msg.procedure}' is not a command of channel '${channelName}'`);
|
|
1915
|
+
return null;
|
|
1916
|
+
}
|
|
1917
|
+
return msg;
|
|
1918
|
+
}
|
|
1919
|
+
/** Dispatch validated uplink command through the router */
|
|
1920
|
+
function dispatchUplink(router, ws, msg, channelInput) {
|
|
1921
|
+
const mergedInput = {
|
|
1922
|
+
...channelInput,
|
|
1923
|
+
...msg.input ?? {}
|
|
1924
|
+
};
|
|
1925
|
+
(async () => {
|
|
1926
|
+
try {
|
|
1927
|
+
const result = await router.handle(msg.procedure, mergedInput);
|
|
1928
|
+
if (result.status === 200) {
|
|
1929
|
+
const envelope = result.body;
|
|
1930
|
+
ws.send(JSON.stringify({
|
|
1931
|
+
id: msg.id,
|
|
1932
|
+
ok: true,
|
|
1933
|
+
data: envelope.data
|
|
1934
|
+
}));
|
|
1935
|
+
} else {
|
|
1936
|
+
const envelope = result.body;
|
|
1937
|
+
ws.send(JSON.stringify({
|
|
1938
|
+
id: msg.id,
|
|
1939
|
+
ok: false,
|
|
1940
|
+
error: envelope.error
|
|
1941
|
+
}));
|
|
1942
|
+
}
|
|
1943
|
+
} catch (err) {
|
|
1944
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
1945
|
+
sendError(ws, msg.id, "INTERNAL_ERROR", message);
|
|
1946
|
+
}
|
|
1947
|
+
})();
|
|
1948
|
+
}
|
|
1544
1949
|
/**
|
|
1545
1950
|
* Start a WebSocket session for a channel.
|
|
1546
1951
|
*
|
|
@@ -1549,9 +1954,24 @@ function sendError(ws, id, code, message) {
|
|
|
1549
1954
|
*/
|
|
1550
1955
|
function startChannelWs(router, channelName, channelInput, ws, opts) {
|
|
1551
1956
|
const heartbeatMs = opts?.heartbeatInterval ?? DEFAULT_HEARTBEAT_MS;
|
|
1957
|
+
const pongTimeoutMs = opts?.pongTimeout ?? DEFAULT_PONG_TIMEOUT_MS;
|
|
1552
1958
|
let closed = false;
|
|
1959
|
+
let pongTimer = null;
|
|
1553
1960
|
const heartbeatTimer = setInterval(() => {
|
|
1554
|
-
if (
|
|
1961
|
+
if (closed) return;
|
|
1962
|
+
ws.send(JSON.stringify({ heartbeat: true }));
|
|
1963
|
+
if (ws.ping) {
|
|
1964
|
+
ws.ping();
|
|
1965
|
+
if (pongTimer) clearTimeout(pongTimer);
|
|
1966
|
+
pongTimer = setTimeout(() => {
|
|
1967
|
+
if (!closed) {
|
|
1968
|
+
closed = true;
|
|
1969
|
+
clearInterval(heartbeatTimer);
|
|
1970
|
+
iter.return?.(void 0);
|
|
1971
|
+
ws.close?.();
|
|
1972
|
+
}
|
|
1973
|
+
}, pongTimeoutMs);
|
|
1974
|
+
}
|
|
1555
1975
|
}, heartbeatMs);
|
|
1556
1976
|
const iter = router.handleSubscription(`${channelName}.events`, channelInput)[Symbol.asyncIterator]();
|
|
1557
1977
|
(async () => {
|
|
@@ -1582,58 +2002,20 @@ function startChannelWs(router, channelName, channelInput, ws, opts) {
|
|
|
1582
2002
|
return {
|
|
1583
2003
|
onMessage(data) {
|
|
1584
2004
|
if (closed) return;
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
if (!msg.id || typeof msg.id !== "string") {
|
|
1593
|
-
sendError(ws, null, "VALIDATION_ERROR", "Missing 'id' field");
|
|
1594
|
-
return;
|
|
1595
|
-
}
|
|
1596
|
-
if (!msg.procedure || typeof msg.procedure !== "string") {
|
|
1597
|
-
sendError(ws, msg.id, "VALIDATION_ERROR", "Missing 'procedure' field");
|
|
1598
|
-
return;
|
|
1599
|
-
}
|
|
1600
|
-
const prefix = channelName + ".";
|
|
1601
|
-
if (!msg.procedure.startsWith(prefix) || msg.procedure === `${channelName}.events`) {
|
|
1602
|
-
sendError(ws, msg.id, "VALIDATION_ERROR", `Procedure '${msg.procedure}' is not a command of channel '${channelName}'`);
|
|
1603
|
-
return;
|
|
2005
|
+
const msg = parseUplink(ws, data, channelName);
|
|
2006
|
+
if (msg) dispatchUplink(router, ws, msg, channelInput);
|
|
2007
|
+
},
|
|
2008
|
+
onPong() {
|
|
2009
|
+
if (pongTimer) {
|
|
2010
|
+
clearTimeout(pongTimer);
|
|
2011
|
+
pongTimer = null;
|
|
1604
2012
|
}
|
|
1605
|
-
const mergedInput = {
|
|
1606
|
-
...channelInput,
|
|
1607
|
-
...msg.input ?? {}
|
|
1608
|
-
};
|
|
1609
|
-
(async () => {
|
|
1610
|
-
try {
|
|
1611
|
-
const result = await router.handle(msg.procedure, mergedInput);
|
|
1612
|
-
if (result.status === 200) {
|
|
1613
|
-
const envelope = result.body;
|
|
1614
|
-
ws.send(JSON.stringify({
|
|
1615
|
-
id: msg.id,
|
|
1616
|
-
ok: true,
|
|
1617
|
-
data: envelope.data
|
|
1618
|
-
}));
|
|
1619
|
-
} else {
|
|
1620
|
-
const envelope = result.body;
|
|
1621
|
-
ws.send(JSON.stringify({
|
|
1622
|
-
id: msg.id,
|
|
1623
|
-
ok: false,
|
|
1624
|
-
error: envelope.error
|
|
1625
|
-
}));
|
|
1626
|
-
}
|
|
1627
|
-
} catch (err) {
|
|
1628
|
-
const message = err instanceof Error ? err.message : "Unknown error";
|
|
1629
|
-
sendError(ws, msg.id, "INTERNAL_ERROR", message);
|
|
1630
|
-
}
|
|
1631
|
-
})();
|
|
1632
2013
|
},
|
|
1633
2014
|
close() {
|
|
1634
2015
|
if (closed) return;
|
|
1635
2016
|
closed = true;
|
|
1636
2017
|
clearInterval(heartbeatTimer);
|
|
2018
|
+
if (pongTimer) clearTimeout(pongTimer);
|
|
1637
2019
|
iter.return?.(void 0);
|
|
1638
2020
|
}
|
|
1639
2021
|
};
|
|
@@ -1759,5 +2141,5 @@ function watchReloadTrigger(distDir, onReload) {
|
|
|
1759
2141
|
}
|
|
1760
2142
|
|
|
1761
2143
|
//#endregion
|
|
1762
|
-
export { SeamError, createChannel, createDevProxy, createHttpHandler, createRouter, createStaticHandler, defaultStrategies, definePage, drainStream, fromAcceptLanguage, fromCallback, fromCookie, fromUrlPrefix, fromUrlQuery, loadBuildOutput, loadBuildOutputDev, loadI18nMessages, loadRpcHashMap, resolveChain, serialize, sseCompleteEvent, sseDataEvent, sseDataEventWithId, sseErrorEvent, startChannelWs, t, toWebResponse, watchReloadTrigger };
|
|
2144
|
+
export { SeamError, buildRawContext, command, contextHasExtracts, createChannel, createDevProxy, createHttpHandler, createRouter, createSeamRouter, createStaticHandler, defaultStrategies, definePage, drainStream, extract, fromAcceptLanguage, fromCallback, fromCookie, fromUrlPrefix, fromUrlQuery, isLoaderError, loadBuild, loadBuildDev, loadBuildOutput, loadBuildOutputDev, loadI18nMessages, loadRpcHashMap, parseCookieHeader, query, resolveChain, serialize, sseCompleteEvent, sseDataEvent, sseDataEventWithId, sseErrorEvent, startChannelWs, stream, subscription, t, toWebResponse, upload, watchReloadTrigger };
|
|
1763
2145
|
//# sourceMappingURL=index.js.map
|