@canmi/seam-server 0.5.27 → 0.5.36

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/dist/index.js CHANGED
@@ -1,25 +1,19 @@
1
- import { validate } from "jtd";
2
1
  import { existsSync, readFileSync, watch } from "node:fs";
3
2
  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
-
7
6
  //#region \0rolldown/runtime.js
8
7
  var __defProp = Object.defineProperty;
9
8
  var __exportAll = (all, no_symbols) => {
10
9
  let target = {};
11
- for (var name in all) {
12
- __defProp(target, name, {
13
- get: all[name],
14
- enumerable: true
15
- });
16
- }
17
- if (!no_symbols) {
18
- __defProp(target, Symbol.toStringTag, { value: "Module" });
19
- }
10
+ for (var name in all) __defProp(target, name, {
11
+ get: all[name],
12
+ enumerable: true
13
+ });
14
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
20
15
  return target;
21
16
  };
22
-
23
17
  //#endregion
24
18
  //#region src/types/schema.ts
25
19
  function createSchemaNode(schema) {
@@ -31,7 +25,6 @@ function createOptionalSchemaNode(schema) {
31
25
  _optional: true
32
26
  };
33
27
  }
34
-
35
28
  //#endregion
36
29
  //#region src/types/primitives.ts
37
30
  var primitives_exports = /* @__PURE__ */ __exportAll({
@@ -87,7 +80,6 @@ function html() {
87
80
  metadata: { format: "html" }
88
81
  });
89
82
  }
90
-
91
83
  //#endregion
92
84
  //#region src/types/composites.ts
93
85
  function object(fields) {
@@ -126,7 +118,6 @@ function discriminator(tag, mapping) {
126
118
  mapping: jtdMapping
127
119
  });
128
120
  }
129
-
130
121
  //#endregion
131
122
  //#region src/types/index.ts
132
123
  const t = {
@@ -139,7 +130,6 @@ const t = {
139
130
  values,
140
131
  discriminator
141
132
  };
142
-
143
133
  //#endregion
144
134
  //#region src/validation/index.ts
145
135
  function validateInput(schema, data) {
@@ -178,7 +168,6 @@ function formatValidationDetails(errors, schema, data) {
178
168
  return detail;
179
169
  });
180
170
  }
181
-
182
171
  //#endregion
183
172
  //#region src/errors.ts
184
173
  const DEFAULT_STATUS = {
@@ -213,7 +202,6 @@ var SeamError = class extends Error {
213
202
  };
214
203
  }
215
204
  };
216
-
217
205
  //#endregion
218
206
  //#region src/context.ts
219
207
  /** Parse extract rule into source type and key, e.g. "header:authorization" -> { source: "header", key: "authorization" } */
@@ -302,7 +290,6 @@ const extract = {
302
290
  cookie: (name) => `cookie:${name}`,
303
291
  query: (name) => `query:${name}`
304
292
  };
305
-
306
293
  //#endregion
307
294
  //#region src/manifest/index.ts
308
295
  function normalizeInvalidates(targets) {
@@ -347,7 +334,6 @@ function buildManifest(definitions, channels, contextConfig, transportDefaults)
347
334
  if (channels && Object.keys(channels).length > 0) manifest.channels = channels;
348
335
  return manifest;
349
336
  }
350
-
351
337
  //#endregion
352
338
  //#region src/page/route-matcher.ts
353
339
  function compileRoute(pattern) {
@@ -397,6 +383,12 @@ var RouteMatcher = class {
397
383
  value
398
384
  });
399
385
  }
386
+ clear() {
387
+ this.routes = [];
388
+ }
389
+ get size() {
390
+ return this.routes.length;
391
+ }
400
392
  match(path) {
401
393
  const parts = path.split("/").filter(Boolean);
402
394
  for (const route of this.routes) {
@@ -410,10 +402,9 @@ var RouteMatcher = class {
410
402
  return null;
411
403
  }
412
404
  };
413
-
414
405
  //#endregion
415
406
  //#region src/router/handler.ts
416
- async function handleRequest(procedures, procedureName, rawBody, shouldValidateInput = true, validateOutput, ctx) {
407
+ async function handleRequest(procedures, procedureName, rawBody, shouldValidateInput = true, validateOutput, ctx, state) {
417
408
  const procedure = procedures.get(procedureName);
418
409
  if (!procedure) return {
419
410
  status: 404,
@@ -432,7 +423,8 @@ async function handleRequest(procedures, procedureName, rawBody, shouldValidateI
432
423
  try {
433
424
  const result = await procedure.handler({
434
425
  input: rawBody,
435
- ctx: ctx ?? {}
426
+ ctx: ctx ?? {},
427
+ state
436
428
  });
437
429
  if (validateOutput) {
438
430
  const outValidation = validateInput(procedure.outputSchema, result);
@@ -459,10 +451,10 @@ async function handleRequest(procedures, procedureName, rawBody, shouldValidateI
459
451
  };
460
452
  }
461
453
  }
462
- async function handleBatchRequest(procedures, calls, shouldValidateInput = true, validateOutput, ctxResolver) {
454
+ async function handleBatchRequest(procedures, calls, shouldValidateInput = true, validateOutput, ctxResolver, state) {
463
455
  return { results: await Promise.all(calls.map(async (call) => {
464
456
  const ctx = ctxResolver ? ctxResolver(call.procedure) : void 0;
465
- const result = await handleRequest(procedures, call.procedure, call.input, shouldValidateInput, validateOutput, ctx);
457
+ const result = await handleRequest(procedures, call.procedure, call.input, shouldValidateInput, validateOutput, ctx, state);
466
458
  if (result.status === 200) return {
467
459
  ok: true,
468
460
  data: result.body.data
@@ -473,7 +465,7 @@ async function handleBatchRequest(procedures, calls, shouldValidateInput = true,
473
465
  };
474
466
  })) };
475
467
  }
476
- async function* handleSubscription(subscriptions, name, rawInput, shouldValidateInput = true, validateOutput, ctx) {
468
+ async function* handleSubscription(subscriptions, name, rawInput, shouldValidateInput = true, validateOutput, ctx, state, lastEventId) {
477
469
  const sub = subscriptions.get(name);
478
470
  if (!sub) throw new SeamError("NOT_FOUND", `Subscription '${name}' not found`);
479
471
  if (shouldValidateInput) {
@@ -485,7 +477,9 @@ async function* handleSubscription(subscriptions, name, rawInput, shouldValidate
485
477
  }
486
478
  for await (const value of sub.handler({
487
479
  input: rawInput,
488
- ctx: ctx ?? {}
480
+ ctx: ctx ?? {},
481
+ state,
482
+ lastEventId
489
483
  })) {
490
484
  if (validateOutput) {
491
485
  const outValidation = validateInput(sub.outputSchema, value);
@@ -494,7 +488,7 @@ async function* handleSubscription(subscriptions, name, rawInput, shouldValidate
494
488
  yield value;
495
489
  }
496
490
  }
497
- async function handleUploadRequest(uploads, procedureName, rawBody, file, shouldValidateInput = true, validateOutput, ctx) {
491
+ async function handleUploadRequest(uploads, procedureName, rawBody, file, shouldValidateInput = true, validateOutput, ctx, state) {
498
492
  const upload = uploads.get(procedureName);
499
493
  if (!upload) return {
500
494
  status: 404,
@@ -514,7 +508,8 @@ async function handleUploadRequest(uploads, procedureName, rawBody, file, should
514
508
  const result = await upload.handler({
515
509
  input: rawBody,
516
510
  file,
517
- ctx: ctx ?? {}
511
+ ctx: ctx ?? {},
512
+ state
518
513
  });
519
514
  if (validateOutput) {
520
515
  const outValidation = validateInput(upload.outputSchema, result);
@@ -541,7 +536,7 @@ async function handleUploadRequest(uploads, procedureName, rawBody, file, should
541
536
  };
542
537
  }
543
538
  }
544
- async function* handleStream(streams, name, rawInput, shouldValidateInput = true, validateOutput, ctx) {
539
+ async function* handleStream(streams, name, rawInput, shouldValidateInput = true, validateOutput, ctx, state) {
545
540
  const stream = streams.get(name);
546
541
  if (!stream) throw new SeamError("NOT_FOUND", `Stream '${name}' not found`);
547
542
  if (shouldValidateInput) {
@@ -553,7 +548,8 @@ async function* handleStream(streams, name, rawInput, shouldValidateInput = true
553
548
  }
554
549
  for await (const value of stream.handler({
555
550
  input: rawInput,
556
- ctx: ctx ?? {}
551
+ ctx: ctx ?? {},
552
+ state
557
553
  })) {
558
554
  if (validateOutput) {
559
555
  const outValidation = validateInput(stream.chunkOutputSchema, value);
@@ -562,7 +558,6 @@ async function* handleStream(streams, name, rawInput, shouldValidateInput = true
562
558
  yield value;
563
559
  }
564
560
  }
565
-
566
561
  //#endregion
567
562
  //#region src/router/categorize.ts
568
563
  function resolveKind(name, def) {
@@ -581,6 +576,7 @@ function categorizeProcedures(definitions, contextConfig) {
581
576
  const uploadMap = /* @__PURE__ */ new Map();
582
577
  const kindMap = /* @__PURE__ */ new Map();
583
578
  for (const [name, def] of Object.entries(definitions)) {
579
+ if (name.startsWith("seam.")) throw new Error(`Procedure name "${name}" uses reserved "seam." namespace`);
584
580
  const kind = resolveKind(name, def);
585
581
  kindMap.set(name, kind);
586
582
  const contextKeys = def.context ?? [];
@@ -620,13 +616,32 @@ function categorizeProcedures(definitions, contextConfig) {
620
616
  kindMap
621
617
  };
622
618
  }
623
-
619
+ //#endregion
620
+ //#region src/page/head.ts
621
+ /**
622
+ * Convert a HeadConfig with real values into escaped HTML.
623
+ * Used at request-time by TS server backends.
624
+ */
625
+ function headConfigToHtml(config) {
626
+ let html = "";
627
+ if (config.title !== void 0) html += `<title>${escapeHtml(config.title)}</title>`;
628
+ for (const meta of config.meta ?? []) {
629
+ html += "<meta";
630
+ for (const [k, v] of Object.entries(meta)) if (v !== void 0) html += ` ${k}="${escapeHtml(v)}"`;
631
+ html += ">";
632
+ }
633
+ for (const link of config.link ?? []) {
634
+ html += "<link";
635
+ for (const [k, v] of Object.entries(link)) if (v !== void 0) html += ` ${k}="${escapeHtml(v)}"`;
636
+ html += ">";
637
+ }
638
+ return html;
639
+ }
624
640
  //#endregion
625
641
  //#region src/page/loader-error.ts
626
642
  function isLoaderError(value) {
627
643
  return typeof value === "object" && value !== null && value.__error === true && typeof value.code === "string" && typeof value.message === "string";
628
644
  }
629
-
630
645
  //#endregion
631
646
  //#region src/page/projection.ts
632
647
  /** Set a nested field by dot-separated path, creating intermediate objects as needed. */
@@ -635,10 +650,13 @@ function setNestedField(target, path, value) {
635
650
  let current = target;
636
651
  for (let i = 0; i < parts.length - 1; i++) {
637
652
  const key = parts[i];
653
+ if (key === "__proto__" || key === "prototype" || key === "constructor") return;
638
654
  if (!(key in current) || typeof current[key] !== "object" || current[key] === null) current[key] = {};
639
655
  current = current[key];
640
656
  }
641
- current[parts[parts.length - 1]] = value;
657
+ const lastPart = parts[parts.length - 1];
658
+ if (lastPart === "__proto__" || lastPart === "prototype" || lastPart === "constructor") return;
659
+ current[lastPart] = value;
642
660
  }
643
661
  /** Get a nested field by dot-separated path. */
644
662
  function getNestedField(source, path) {
@@ -692,13 +710,12 @@ function applyProjection(data, projections) {
692
710
  }
693
711
  return result;
694
712
  }
695
-
696
713
  //#endregion
697
714
  //#region src/page/handler.ts
698
715
  /** Execute loaders, returning keyed results and metadata.
699
716
  * Each loader is wrapped in its own try-catch so a single failure
700
717
  * does not abort sibling loaders — the page renders at 200 with partial data. */
701
- async function executeLoaders(loaders, params, procedures, searchParams, ctxResolver, shouldValidateInput) {
718
+ async function executeLoaders(loaders, params, procedures, searchParams, ctxResolver, appState, shouldValidateInput) {
702
719
  const entries = Object.entries(loaders);
703
720
  const results = await Promise.all(entries.map(async ([key, loader]) => {
704
721
  const { procedure, input } = loader(params, searchParams);
@@ -714,7 +731,8 @@ async function executeLoaders(loaders, params, procedures, searchParams, ctxReso
714
731
  key,
715
732
  result: await proc.handler({
716
733
  input,
717
- ctx
734
+ ctx,
735
+ state: appState
718
736
  }),
719
737
  procedure,
720
738
  input
@@ -764,12 +782,28 @@ function lookupMessages(config, routePattern, locale) {
764
782
  }
765
783
  return config.messages[locale]?.[routeHash] ?? {};
766
784
  }
767
- async function handlePageRequest(page, params, procedures, i18nOpts, searchParams, ctxResolver, shouldValidateInput) {
785
+ /** Build JSON payload for engine i18n injection */
786
+ function buildI18nPayload(opts) {
787
+ const { config: i18nConfig, routePattern, locale } = opts;
788
+ const messages = lookupMessages(i18nConfig, routePattern, locale);
789
+ const routeHash = i18nConfig.routeHashes[routePattern];
790
+ const i18nData = {
791
+ locale,
792
+ default_locale: i18nConfig.default,
793
+ messages
794
+ };
795
+ if (i18nConfig.cache && routeHash) {
796
+ i18nData.hash = i18nConfig.contentHashes[routeHash]?.[locale];
797
+ i18nData.router = i18nConfig.contentHashes;
798
+ }
799
+ return JSON.stringify(i18nData);
800
+ }
801
+ async function handlePageRequest(page, params, procedures, i18nOpts, searchParams, ctxResolver, appState, shouldValidateInput) {
768
802
  try {
769
803
  const t0 = performance.now();
770
804
  const layoutChain = page.layoutChain ?? [];
771
805
  const locale = i18nOpts?.locale;
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)]);
806
+ const loaderResults = await Promise.all([...layoutChain.map((layout) => executeLoaders(layout.loaders, params, procedures, searchParams, ctxResolver, appState, shouldValidateInput)), executeLoaders(page.loaders, params, procedures, searchParams, ctxResolver, appState, shouldValidateInput)]);
773
807
  const t1 = performance.now();
774
808
  const allData = {};
775
809
  const allMeta = {};
@@ -783,32 +817,23 @@ async function handlePageRequest(page, params, procedures, i18nOpts, searchParam
783
817
  const layout = layoutChain[i];
784
818
  composedTemplate = selectTemplate(layout.template, layout.localeTemplates, locale).replace("<!--seam:outlet-->", composedTemplate);
785
819
  }
820
+ let resolvedHeadMeta = page.headMeta;
821
+ if (page.headFn) try {
822
+ resolvedHeadMeta = headConfigToHtml(page.headFn(allData));
823
+ } catch (err) {
824
+ console.error("[seam] head function failed:", err);
825
+ }
786
826
  const config = {
787
827
  layout_chain: layoutChain.map((l) => ({
788
828
  id: l.id,
789
829
  loader_keys: Object.keys(l.loaders)
790
830
  })),
791
831
  data_id: page.dataId ?? "__data",
792
- head_meta: page.headMeta,
832
+ head_meta: resolvedHeadMeta,
793
833
  loader_metadata: allMeta
794
834
  };
795
835
  if (page.pageAssets) config.page_assets = page.pageAssets;
796
- let i18nOptsJson;
797
- if (i18nOpts) {
798
- const { config: i18nConfig, routePattern } = i18nOpts;
799
- const messages = lookupMessages(i18nConfig, routePattern, i18nOpts.locale);
800
- const routeHash = i18nConfig.routeHashes[routePattern];
801
- const i18nData = {
802
- locale: i18nOpts.locale,
803
- default_locale: i18nConfig.default,
804
- messages
805
- };
806
- if (i18nConfig.cache && routeHash) {
807
- i18nData.hash = i18nConfig.contentHashes[routeHash]?.[i18nOpts.locale];
808
- i18nData.router = i18nConfig.contentHashes;
809
- }
810
- i18nOptsJson = JSON.stringify(i18nData);
811
- }
836
+ const i18nOptsJson = i18nOpts ? buildI18nPayload(i18nOpts) : void 0;
812
837
  const html = renderPage(composedTemplate, JSON.stringify(prunedData), JSON.stringify(config), i18nOptsJson);
813
838
  const t2 = performance.now();
814
839
  return {
@@ -822,11 +847,14 @@ async function handlePageRequest(page, params, procedures, i18nOpts, searchParam
822
847
  } catch (error) {
823
848
  return {
824
849
  status: 500,
825
- html: `<!DOCTYPE html><html><body><h1>500 Internal Server Error</h1><p>${escapeHtml(error instanceof Error ? error.message : "Unknown error")}</p></body></html>`
850
+ html: `<!DOCTYPE html><html><body><h1>500 Internal Server Error</h1><p>${escapeHtml(error instanceof Error ? error.message : "Unknown error")}</p></body></html>`,
851
+ timing: {
852
+ dataFetch: 0,
853
+ inject: 0
854
+ }
826
855
  };
827
856
  }
828
857
  }
829
-
830
858
  //#endregion
831
859
  //#region src/resolve.ts
832
860
  /** URL prefix strategy: trusts pathLocale if it is a known locale */
@@ -916,7 +944,6 @@ function defaultStrategies() {
916
944
  fromAcceptLanguage()
917
945
  ];
918
946
  }
919
-
920
947
  //#endregion
921
948
  //#region src/router/helpers.ts
922
949
  /** Resolve a ValidationMode to a boolean flag */
@@ -934,14 +961,16 @@ function buildStrategies(opts) {
934
961
  hasUrlPrefix: strategies.some((s) => s.kind === "url_prefix")
935
962
  };
936
963
  }
937
- /** Register built-in __seam_i18n_query procedure (route-hash-based lookup) */
964
+ /** Register built-in seam.i18n.query procedure (route-hash-based lookup) */
938
965
  function registerI18nQuery(procedureMap, config) {
939
- procedureMap.set("__seam_i18n_query", {
966
+ const localeSet = new Set(config.locales);
967
+ procedureMap.set("seam.i18n.query", {
940
968
  inputSchema: {},
941
969
  outputSchema: {},
942
970
  contextKeys: [],
943
971
  handler: ({ input }) => {
944
- const { route, locale } = input;
972
+ const { route, locale: rawLocale } = input;
973
+ const locale = localeSet.has(rawLocale) ? rawLocale : config.default;
945
974
  const messages = lookupI18nMessages(config, route, locale);
946
975
  return {
947
976
  hash: config.contentHashes[route]?.[locale] ?? "",
@@ -975,7 +1004,7 @@ function resolveCtxFor(map, name, rawCtx, ctxConfig) {
975
1004
  return resolveContext(ctxConfig, rawCtx, proc.contextKeys);
976
1005
  }
977
1006
  /** Resolve locale and match page route */
978
- async function matchAndHandlePage(pageMatcher, procedureMap, i18nConfig, strategies, hasUrlPrefix, path, headers, rawCtx, ctxConfig, shouldValidateInput) {
1007
+ async function matchAndHandlePage(pageMatcher, procedureMap, i18nConfig, strategies, hasUrlPrefix, path, headers, rawCtx, ctxConfig, appState, shouldValidateInput) {
979
1008
  let pathLocale = null;
980
1009
  let routePath = path;
981
1010
  if (hasUrlPrefix && i18nConfig) {
@@ -998,6 +1027,15 @@ async function matchAndHandlePage(pageMatcher, procedureMap, i18nConfig, strateg
998
1027
  });
999
1028
  const match = pageMatcher.match(routePath);
1000
1029
  if (!match) return null;
1030
+ if (match.value.prerender && match.value.staticDir) {
1031
+ const htmlPath = join(match.value.staticDir, routePath === "/" ? "" : routePath, "index.html");
1032
+ try {
1033
+ return {
1034
+ status: 200,
1035
+ html: readFileSync(htmlPath, "utf-8")
1036
+ };
1037
+ } catch {}
1038
+ }
1001
1039
  let searchParams;
1002
1040
  if (headers?.url) try {
1003
1041
  const url = new URL(headers.url, "http://localhost");
@@ -1012,7 +1050,7 @@ async function matchAndHandlePage(pageMatcher, procedureMap, i18nConfig, strateg
1012
1050
  if (proc.contextKeys.length === 0) return {};
1013
1051
  return resolveContext(ctxConfig ?? {}, rawCtx, proc.contextKeys);
1014
1052
  } : void 0;
1015
- return handlePageRequest(match.value, match.params, procedureMap, i18nOpts, searchParams, ctxResolver, shouldValidateInput);
1053
+ return handlePageRequest(match.value, match.params, procedureMap, i18nOpts, searchParams, ctxResolver, appState, shouldValidateInput);
1016
1054
  }
1017
1055
  /** Catch context resolution errors and return them as HandleResult */
1018
1056
  function resolveCtxSafe(map, name, rawCtx, ctxConfig) {
@@ -1026,7 +1064,6 @@ function resolveCtxSafe(map, name, rawCtx, ctxConfig) {
1026
1064
  throw err;
1027
1065
  }
1028
1066
  }
1029
-
1030
1067
  //#endregion
1031
1068
  //#region src/router/state.ts
1032
1069
  /** Build all shared state that createRouter methods close over */
@@ -1056,7 +1093,10 @@ function initRouterState(procedures, opts) {
1056
1093
  strategies,
1057
1094
  hasUrlPrefix,
1058
1095
  channelsMeta: collectChannelMeta(opts?.channels),
1059
- hasCtx: contextHasExtracts(ctxConfig)
1096
+ hasCtx: contextHasExtracts(ctxConfig),
1097
+ appState: opts?.state,
1098
+ rpcHashMap: opts?.rpcHashMap,
1099
+ publicDir: opts?.publicDir
1060
1100
  };
1061
1101
  }
1062
1102
  /** Build request-response methods: handle, handleBatch, handleUpload */
@@ -1065,23 +1105,22 @@ function buildRpcMethods(state) {
1065
1105
  async handle(procedureName, body, rawCtx) {
1066
1106
  const { ctx, error } = resolveCtxSafe(state.procedureMap, procedureName, rawCtx, state.ctxConfig);
1067
1107
  if (error) return error;
1068
- return handleRequest(state.procedureMap, procedureName, body, state.shouldValidateInput, state.shouldValidateOutput, ctx);
1108
+ return handleRequest(state.procedureMap, procedureName, body, state.shouldValidateInput, state.shouldValidateOutput, ctx, state.appState);
1069
1109
  },
1070
1110
  handleBatch(calls, rawCtx) {
1071
1111
  const ctxResolver = rawCtx ? (name) => resolveCtxFor(state.procedureMap, name, rawCtx, state.ctxConfig) ?? {} : void 0;
1072
- return handleBatchRequest(state.procedureMap, calls, state.shouldValidateInput, state.shouldValidateOutput, ctxResolver);
1112
+ return handleBatchRequest(state.procedureMap, calls, state.shouldValidateInput, state.shouldValidateOutput, ctxResolver, state.appState);
1073
1113
  },
1074
1114
  async handleUpload(name, body, file, rawCtx) {
1075
1115
  const { ctx, error } = resolveCtxSafe(state.uploadMap, name, rawCtx, state.ctxConfig);
1076
1116
  if (error) return error;
1077
- return handleUploadRequest(state.uploadMap, name, body, file, state.shouldValidateInput, state.shouldValidateOutput, ctx);
1117
+ return handleUploadRequest(state.uploadMap, name, body, file, state.shouldValidateInput, state.shouldValidateOutput, ctx, state.appState);
1078
1118
  }
1079
1119
  };
1080
1120
  }
1081
1121
  /** Build all Router method implementations from shared state */
1082
1122
  function buildRouterMethods(state, procedures, opts) {
1083
1123
  return {
1084
- hasPages: !!state.pages && Object.keys(state.pages).length > 0,
1085
1124
  ctxConfig: state.ctxConfig,
1086
1125
  hasContext() {
1087
1126
  return state.hasCtx;
@@ -1090,34 +1129,82 @@ function buildRouterMethods(state, procedures, opts) {
1090
1129
  return buildManifest(procedures, state.channelsMeta, state.ctxConfig, opts?.transportDefaults);
1091
1130
  },
1092
1131
  ...buildRpcMethods(state),
1093
- handleSubscription(name, input, rawCtx) {
1132
+ handleSubscription(name, input, rawCtx, lastEventId) {
1094
1133
  const ctx = resolveCtxFor(state.subscriptionMap, name, rawCtx, state.ctxConfig);
1095
- return handleSubscription(state.subscriptionMap, name, input, state.shouldValidateInput, state.shouldValidateOutput, ctx);
1134
+ return handleSubscription(state.subscriptionMap, name, input, state.shouldValidateInput, state.shouldValidateOutput, ctx, state.appState, lastEventId);
1096
1135
  },
1097
1136
  handleStream(name, input, rawCtx) {
1098
1137
  const ctx = resolveCtxFor(state.streamMap, name, rawCtx, state.ctxConfig);
1099
- return handleStream(state.streamMap, name, input, state.shouldValidateInput, state.shouldValidateOutput, ctx);
1138
+ return handleStream(state.streamMap, name, input, state.shouldValidateInput, state.shouldValidateOutput, ctx, state.appState);
1100
1139
  },
1101
1140
  getKind(name) {
1102
1141
  return state.kindMap.get(name) ?? null;
1103
1142
  },
1104
1143
  handlePage(path, headers, rawCtx) {
1105
- return matchAndHandlePage(state.pageMatcher, state.procedureMap, state.i18nConfig, state.strategies, state.hasUrlPrefix, path, headers, rawCtx, state.ctxConfig, state.shouldValidateInput);
1144
+ return matchAndHandlePage(state.pageMatcher, state.procedureMap, state.i18nConfig, state.strategies, state.hasUrlPrefix, path, headers, rawCtx, state.ctxConfig, state.appState, state.shouldValidateInput);
1145
+ },
1146
+ reload(build) {
1147
+ state.pageMatcher.clear();
1148
+ for (const [pattern, page] of Object.entries(build.pages)) state.pageMatcher.add(pattern, page);
1149
+ state.pages = build.pages;
1150
+ state.i18nConfig = build.i18n ?? null;
1151
+ if (state.i18nConfig) registerI18nQuery(state.procedureMap, state.i18nConfig);
1152
+ state.rpcHashMap = build.rpcHashMap;
1153
+ state.publicDir = build.publicDir;
1154
+ },
1155
+ handlePageData(path) {
1156
+ const match = state.pageMatcher?.match(path);
1157
+ if (!match) return Promise.resolve(null);
1158
+ const page = match.value;
1159
+ if (!page.prerender || !page.staticDir) return Promise.resolve(null);
1160
+ const dataPath = join(page.staticDir, path === "/" ? "" : path, "__data.json");
1161
+ try {
1162
+ if (!existsSync(dataPath)) return Promise.resolve(null);
1163
+ return Promise.resolve(JSON.parse(readFileSync(dataPath, "utf-8")));
1164
+ } catch {
1165
+ return Promise.resolve(null);
1166
+ }
1106
1167
  }
1107
1168
  };
1108
1169
  }
1109
-
1110
1170
  //#endregion
1111
1171
  //#region src/router/index.ts
1172
+ function isProcedureDef(value) {
1173
+ return typeof value === "object" && value !== null && "input" in value && "handler" in value;
1174
+ }
1175
+ function flattenDefinitions(nested, prefix = "") {
1176
+ const flat = {};
1177
+ for (const [key, value] of Object.entries(nested)) {
1178
+ const fullKey = prefix ? `${prefix}.${key}` : key;
1179
+ if (isProcedureDef(value)) flat[fullKey] = value;
1180
+ else Object.assign(flat, flattenDefinitions(value, fullKey));
1181
+ }
1182
+ return flat;
1183
+ }
1112
1184
  function createRouter(procedures, opts) {
1113
- const state = initRouterState(procedures, opts);
1114
- return {
1115
- procedures,
1116
- rpcHashMap: opts?.rpcHashMap,
1117
- ...buildRouterMethods(state, procedures, opts)
1185
+ const flat = flattenDefinitions(procedures);
1186
+ const state = initRouterState(flat, opts);
1187
+ const router = {
1188
+ procedures: flat,
1189
+ ...buildRouterMethods(state, flat, opts)
1118
1190
  };
1191
+ Object.defineProperty(router, "hasPages", {
1192
+ get: () => state.pageMatcher.size > 0,
1193
+ enumerable: true,
1194
+ configurable: true
1195
+ });
1196
+ Object.defineProperty(router, "rpcHashMap", {
1197
+ get: () => state.rpcHashMap,
1198
+ enumerable: true,
1199
+ configurable: true
1200
+ });
1201
+ Object.defineProperty(router, "publicDir", {
1202
+ get: () => state.publicDir,
1203
+ enumerable: true,
1204
+ configurable: true
1205
+ });
1206
+ return router;
1119
1207
  }
1120
-
1121
1208
  //#endregion
1122
1209
  //#region src/factory.ts
1123
1210
  function query(def) {
@@ -1150,11 +1237,10 @@ function upload(def) {
1150
1237
  kind: "upload"
1151
1238
  };
1152
1239
  }
1153
-
1154
1240
  //#endregion
1155
1241
  //#region src/seam-router.ts
1156
1242
  function createSeamRouter(config) {
1157
- const { context, ...restConfig } = config;
1243
+ const { context, state, ...restConfig } = config;
1158
1244
  const define = {
1159
1245
  query(def) {
1160
1246
  return {
@@ -1191,6 +1277,7 @@ function createSeamRouter(config) {
1191
1277
  return createRouter(procedures, {
1192
1278
  ...restConfig,
1193
1279
  context,
1280
+ state,
1194
1281
  ...extraOpts
1195
1282
  });
1196
1283
  }
@@ -1199,7 +1286,6 @@ function createSeamRouter(config) {
1199
1286
  define
1200
1287
  };
1201
1288
  }
1202
-
1203
1289
  //#endregion
1204
1290
  //#region src/channel.ts
1205
1291
  /** Merge channel-level and message-level JTD properties schemas */
@@ -1274,7 +1360,6 @@ function createChannel(name, def) {
1274
1360
  channelMeta
1275
1361
  };
1276
1362
  }
1277
-
1278
1363
  //#endregion
1279
1364
  //#region src/page/index.ts
1280
1365
  function definePage(config) {
@@ -1283,103 +1368,383 @@ function definePage(config) {
1283
1368
  layoutChain: config.layoutChain ?? []
1284
1369
  };
1285
1370
  }
1286
-
1287
- //#endregion
1288
- //#region src/mime.ts
1289
- const MIME_TYPES = {
1290
- ".js": "application/javascript",
1291
- ".mjs": "application/javascript",
1292
- ".css": "text/css",
1293
- ".html": "text/html",
1294
- ".json": "application/json",
1295
- ".svg": "image/svg+xml",
1296
- ".png": "image/png",
1297
- ".jpg": "image/jpeg",
1298
- ".jpeg": "image/jpeg",
1299
- ".gif": "image/gif",
1300
- ".woff": "font/woff",
1301
- ".woff2": "font/woff2",
1302
- ".ttf": "font/ttf",
1303
- ".ico": "image/x-icon",
1304
- ".map": "application/json",
1305
- ".ts": "application/javascript",
1306
- ".tsx": "application/javascript"
1307
- };
1308
-
1309
1371
  //#endregion
1310
- //#region src/http.ts
1311
- const PROCEDURE_PREFIX = "/_seam/procedure/";
1312
- const PAGE_PREFIX = "/_seam/page/";
1313
- const STATIC_PREFIX = "/_seam/static/";
1314
- const MANIFEST_PATH = "/_seam/manifest.json";
1315
- const JSON_HEADER = { "Content-Type": "application/json" };
1316
- const HTML_HEADER = { "Content-Type": "text/html; charset=utf-8" };
1317
- const SSE_HEADER = {
1318
- "Content-Type": "text/event-stream",
1319
- "Cache-Control": "no-cache",
1320
- Connection: "keep-alive"
1372
+ //#region src/dev/reload-watcher.ts
1373
+ const nodeReloadWatcherBackend = {
1374
+ watchFile(path, onChange, onError) {
1375
+ const watcher = watch(path, () => onChange());
1376
+ watcher.on("error", onError);
1377
+ return watcher;
1378
+ },
1379
+ fileExists(path) {
1380
+ return existsSync(path);
1381
+ },
1382
+ setPoll(callback, intervalMs) {
1383
+ const timer = setInterval(callback, intervalMs);
1384
+ return { close() {
1385
+ clearInterval(timer);
1386
+ } };
1387
+ }
1321
1388
  };
1322
- const IMMUTABLE_CACHE = "public, max-age=31536000, immutable";
1323
- function jsonResponse(status, body) {
1389
+ function isMissingFileError(error) {
1390
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
1391
+ }
1392
+ function createReloadWatcher(distDir, onReload, backend) {
1393
+ const triggerPath = join(distDir, ".reload-trigger");
1394
+ let watcher = null;
1395
+ let poller = null;
1396
+ let closed = false;
1397
+ let pending = [];
1398
+ const notify = () => {
1399
+ onReload();
1400
+ const batch = pending;
1401
+ pending = [];
1402
+ for (const p of batch) p.resolve();
1403
+ };
1404
+ const nextReload = () => {
1405
+ if (closed) return Promise.reject(/* @__PURE__ */ new Error("watcher closed"));
1406
+ return new Promise((resolve, reject) => {
1407
+ pending.push({
1408
+ resolve,
1409
+ reject
1410
+ });
1411
+ });
1412
+ };
1413
+ const closeAll = () => {
1414
+ closed = true;
1415
+ const batch = pending;
1416
+ pending = [];
1417
+ const err = /* @__PURE__ */ new Error("watcher closed");
1418
+ for (const p of batch) p.reject(err);
1419
+ };
1420
+ const stopWatcher = () => {
1421
+ watcher?.close();
1422
+ watcher = null;
1423
+ };
1424
+ const stopPoller = () => {
1425
+ poller?.close();
1426
+ poller = null;
1427
+ };
1428
+ const startPolling = () => {
1429
+ if (closed || poller) return;
1430
+ poller = backend.setPoll(() => {
1431
+ if (!backend.fileExists(triggerPath)) return;
1432
+ stopPoller();
1433
+ notify();
1434
+ startWatchingFile();
1435
+ }, 50);
1436
+ };
1437
+ const startWatchingFile = () => {
1438
+ if (closed) return;
1439
+ try {
1440
+ watcher = backend.watchFile(triggerPath, () => notify(), (error) => {
1441
+ stopWatcher();
1442
+ if (!closed && isMissingFileError(error)) startPolling();
1443
+ });
1444
+ } catch (error) {
1445
+ stopWatcher();
1446
+ if (isMissingFileError(error)) {
1447
+ startPolling();
1448
+ return;
1449
+ }
1450
+ throw error;
1451
+ }
1452
+ };
1453
+ startWatchingFile();
1324
1454
  return {
1325
- status,
1326
- headers: JSON_HEADER,
1327
- body
1455
+ close() {
1456
+ stopWatcher();
1457
+ stopPoller();
1458
+ closeAll();
1459
+ },
1460
+ nextReload
1328
1461
  };
1329
1462
  }
1330
- function errorResponse(status, code, message) {
1331
- return jsonResponse(status, new SeamError(code, message).toJSON());
1463
+ function watchReloadTrigger(distDir, onReload) {
1464
+ return createReloadWatcher(distDir, onReload, nodeReloadWatcherBackend);
1332
1465
  }
1333
- async function handleStaticAsset(assetPath, staticDir) {
1334
- if (assetPath.includes("..")) return errorResponse(403, "VALIDATION_ERROR", "Forbidden");
1335
- const filePath = join(staticDir, assetPath);
1336
- try {
1337
- const content = await readFile(filePath, "utf-8");
1338
- const contentType = MIME_TYPES[extname(filePath)] || "application/octet-stream";
1466
+ //#endregion
1467
+ //#region src/page/build-loader.ts
1468
+ function normalizeParamConfig(value) {
1469
+ return typeof value === "string" ? { from: value } : value;
1470
+ }
1471
+ function buildLoaderFn(config) {
1472
+ return (params, searchParams) => {
1473
+ const input = {};
1474
+ if (config.params) for (const [key, raw_mapping] of Object.entries(config.params)) {
1475
+ const mapping = normalizeParamConfig(raw_mapping);
1476
+ const raw = mapping.from === "query" ? searchParams?.get(key) ?? void 0 : params[key];
1477
+ if (raw !== void 0) input[key] = mapping.type === "int" ? Number(raw) : raw;
1478
+ }
1339
1479
  return {
1340
- status: 200,
1341
- headers: {
1342
- "Content-Type": contentType,
1343
- "Cache-Control": IMMUTABLE_CACHE
1344
- },
1345
- body: content
1480
+ procedure: config.procedure,
1481
+ input
1346
1482
  };
1347
- } catch {
1348
- return errorResponse(404, "NOT_FOUND", "Asset not found");
1349
- }
1483
+ };
1350
1484
  }
1351
- /** Format a single SSE data event */
1352
- function sseDataEvent(data) {
1353
- return `event: data\ndata: ${JSON.stringify(data)}\n\n`;
1485
+ function buildLoaderFns(configs) {
1486
+ const fns = {};
1487
+ for (const [key, config] of Object.entries(configs)) fns[key] = buildLoaderFn(config);
1488
+ return fns;
1354
1489
  }
1355
- /** Format an SSE data event with a sequence id (for streams) */
1356
- function sseDataEventWithId(data, id) {
1357
- return `event: data\nid: ${id}\ndata: ${JSON.stringify(data)}\n\n`;
1490
+ function resolveTemplatePath(entry, defaultLocale) {
1491
+ if (entry.template) return entry.template;
1492
+ if (entry.templates) {
1493
+ const locale = defaultLocale ?? Object.keys(entry.templates)[0];
1494
+ const path = entry.templates[locale];
1495
+ if (!path) throw new Error(`No template for locale "${locale}"`);
1496
+ return path;
1497
+ }
1498
+ throw new Error("Manifest entry has neither 'template' nor 'templates'");
1358
1499
  }
1359
- /** Format an SSE error event */
1360
- function sseErrorEvent(code, message, transient = false) {
1361
- return `event: error\ndata: ${JSON.stringify({
1362
- code,
1363
- message,
1364
- transient
1365
- })}\n\n`;
1500
+ /** Load all locale templates for a manifest entry, keyed by locale */
1501
+ function loadLocaleTemplates(entry, distDir) {
1502
+ if (!entry.templates) return void 0;
1503
+ const result = {};
1504
+ for (const [locale, relPath] of Object.entries(entry.templates)) result[locale] = readFileSync(join(distDir, relPath), "utf-8");
1505
+ return result;
1366
1506
  }
1367
- /** Format an SSE complete event */
1368
- function sseCompleteEvent() {
1369
- return "event: complete\ndata: {}\n\n";
1507
+ /** Resolve parent chain for a layout, returning outer-to-inner order */
1508
+ function resolveLayoutChain(layoutId, layoutEntries, getTemplates) {
1509
+ const chain = [];
1510
+ let currentId = layoutId;
1511
+ while (currentId) {
1512
+ const entry = layoutEntries[currentId];
1513
+ if (!entry) break;
1514
+ const { template, localeTemplates } = getTemplates(currentId, entry);
1515
+ chain.push({
1516
+ id: currentId,
1517
+ template,
1518
+ localeTemplates,
1519
+ loaders: buildLoaderFns(entry.loaders ?? {})
1520
+ });
1521
+ currentId = entry.parent;
1522
+ }
1523
+ chain.reverse();
1524
+ return chain;
1370
1525
  }
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");
1526
+ /** Create a proxy object that lazily reads locale templates from disk */
1527
+ function makeLocaleTemplateGetters(templates, distDir) {
1528
+ const obj = {};
1529
+ for (const [locale, relPath] of Object.entries(templates)) {
1530
+ const fullPath = join(distDir, relPath);
1531
+ Object.defineProperty(obj, locale, {
1532
+ get: () => readFileSync(fullPath, "utf-8"),
1533
+ enumerable: true
1534
+ });
1535
+ }
1536
+ return obj;
1374
1537
  }
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;
1538
+ /** Merge i18n_keys from route + layout chain into a single list */
1539
+ function mergeI18nKeys(route, layoutEntries) {
1540
+ const keys = [];
1541
+ if (route.layout) {
1542
+ let currentId = route.layout;
1543
+ while (currentId) {
1544
+ const entry = layoutEntries[currentId];
1545
+ if (!entry) break;
1546
+ if (entry.i18n_keys) keys.push(...entry.i18n_keys);
1547
+ currentId = entry.parent;
1548
+ }
1549
+ }
1550
+ if (route.i18n_keys) keys.push(...route.i18n_keys);
1551
+ return keys.length > 0 ? keys : void 0;
1552
+ }
1553
+ /** Detect public-root directory from production build output */
1554
+ function detectBuiltPublicDir(distDir) {
1555
+ const publicRootDir = join(distDir, "public-root");
1556
+ return existsSync(publicRootDir) ? publicRootDir : void 0;
1557
+ }
1558
+ /** Detect source public/ directory for dev mode. */
1559
+ function detectDevPublicDir(distDir) {
1560
+ const explicitDir = process.env.SEAM_PUBLIC_DIR;
1561
+ if (explicitDir && existsSync(explicitDir)) return explicitDir;
1562
+ const sourcePublicDir = join(distDir, "..", "..", "public");
1563
+ return existsSync(sourcePublicDir) ? sourcePublicDir : void 0;
1564
+ }
1565
+ /** Load all build artifacts (pages, rpcHashMap, i18n) in one call */
1566
+ function loadBuild(distDir) {
1567
+ return {
1568
+ pages: loadBuildOutput(distDir),
1569
+ rpcHashMap: loadRpcHashMap(distDir),
1570
+ i18n: loadI18nMessages(distDir),
1571
+ publicDir: detectBuiltPublicDir(distDir)
1572
+ };
1573
+ }
1574
+ /** Load all build artifacts with lazy template getters (for dev mode) */
1575
+ function loadBuildDev(distDir) {
1576
+ return {
1577
+ pages: loadBuildOutputDev(distDir),
1578
+ rpcHashMap: loadRpcHashMap(distDir),
1579
+ i18n: loadI18nMessages(distDir),
1580
+ publicDir: detectDevPublicDir(distDir) ?? detectBuiltPublicDir(distDir)
1581
+ };
1582
+ }
1583
+ /** Load the RPC hash map from build output (returns undefined when obfuscation is off) */
1584
+ function loadRpcHashMap(distDir) {
1585
+ const hashMapPath = join(distDir, "rpc-hash-map.json");
1586
+ try {
1587
+ return JSON.parse(readFileSync(hashMapPath, "utf-8"));
1588
+ } catch {
1589
+ return;
1590
+ }
1591
+ }
1592
+ /** Load i18n config and messages from build output */
1593
+ function loadI18nMessages(distDir) {
1594
+ const manifestPath = join(distDir, "route-manifest.json");
1595
+ try {
1596
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
1597
+ if (!manifest.i18n) return null;
1598
+ const mode = manifest.i18n.mode ?? "memory";
1599
+ const cache = manifest.i18n.cache ?? false;
1600
+ const routeHashes = manifest.i18n.route_hashes ?? {};
1601
+ const contentHashes = manifest.i18n.content_hashes ?? {};
1602
+ const messages = {};
1603
+ if (mode === "memory") {
1604
+ const i18nDir = join(distDir, "i18n");
1605
+ for (const locale of manifest.i18n.locales) {
1606
+ const localePath = join(i18nDir, `${locale}.json`);
1607
+ if (existsSync(localePath)) messages[locale] = JSON.parse(readFileSync(localePath, "utf-8"));
1608
+ else messages[locale] = {};
1609
+ }
1610
+ }
1611
+ return {
1612
+ locales: manifest.i18n.locales,
1613
+ default: manifest.i18n.default,
1614
+ mode,
1615
+ cache,
1616
+ routeHashes,
1617
+ contentHashes,
1618
+ messages,
1619
+ distDir: mode === "paged" ? distDir : void 0
1620
+ };
1621
+ } catch {
1622
+ return null;
1623
+ }
1624
+ }
1625
+ function loadBuildOutput(distDir) {
1626
+ const raw = readFileSync(join(distDir, "route-manifest.json"), "utf-8");
1627
+ const manifest = JSON.parse(raw);
1628
+ const defaultLocale = manifest.i18n?.default;
1629
+ const layoutTemplates = {};
1630
+ const layoutLocaleTemplates = {};
1631
+ const layoutEntries = manifest.layouts ?? {};
1632
+ for (const [id, entry] of Object.entries(layoutEntries)) {
1633
+ layoutTemplates[id] = readFileSync(join(distDir, resolveTemplatePath(entry, defaultLocale)), "utf-8");
1634
+ const lt = loadLocaleTemplates(entry, distDir);
1635
+ if (lt) layoutLocaleTemplates[id] = lt;
1636
+ }
1637
+ const staticDir = join(distDir, "..", "static");
1638
+ const hasStaticDir = existsSync(staticDir);
1639
+ const pages = {};
1640
+ for (const [path, entry] of Object.entries(manifest.routes)) {
1641
+ const template = readFileSync(join(distDir, resolveTemplatePath(entry, defaultLocale)), "utf-8");
1642
+ const loaders = buildLoaderFns(entry.loaders);
1643
+ const layoutChain = entry.layout ? resolveLayoutChain(entry.layout, layoutEntries, (id) => ({
1644
+ template: layoutTemplates[id] ?? "",
1645
+ localeTemplates: layoutLocaleTemplates[id]
1646
+ })) : [];
1647
+ const i18nKeys = mergeI18nKeys(entry, layoutEntries);
1648
+ const page = {
1649
+ template,
1650
+ localeTemplates: loadLocaleTemplates(entry, distDir),
1651
+ loaders,
1652
+ layoutChain,
1653
+ headMeta: entry.head_meta,
1654
+ dataId: manifest.data_id,
1655
+ i18nKeys,
1656
+ pageAssets: entry.assets,
1657
+ projections: entry.projections
1658
+ };
1659
+ if (entry.prerender && hasStaticDir) {
1660
+ page.prerender = true;
1661
+ page.staticDir = staticDir;
1662
+ }
1663
+ pages[path] = page;
1664
+ }
1665
+ return pages;
1666
+ }
1667
+ /** Load build output with lazy template getters -- templates re-read from disk on each access */
1668
+ function loadBuildOutputDev(distDir) {
1669
+ const raw = readFileSync(join(distDir, "route-manifest.json"), "utf-8");
1670
+ const manifest = JSON.parse(raw);
1671
+ const defaultLocale = manifest.i18n?.default;
1672
+ const layoutEntries = manifest.layouts ?? {};
1673
+ const pages = {};
1674
+ for (const [path, entry] of Object.entries(manifest.routes)) {
1675
+ const templatePath = join(distDir, resolveTemplatePath(entry, defaultLocale));
1676
+ const loaders = buildLoaderFns(entry.loaders);
1677
+ const layoutChain = entry.layout ? resolveLayoutChain(entry.layout, layoutEntries, (id, layoutEntry) => {
1678
+ const tmplPath = join(distDir, resolveTemplatePath(layoutEntry, defaultLocale));
1679
+ const def = {
1680
+ template: "",
1681
+ localeTemplates: layoutEntry.templates ? makeLocaleTemplateGetters(layoutEntry.templates, distDir) : void 0
1682
+ };
1683
+ Object.defineProperty(def, "template", {
1684
+ get: () => readFileSync(tmplPath, "utf-8"),
1685
+ enumerable: true
1686
+ });
1687
+ return def;
1688
+ }) : [];
1689
+ const localeTemplates = entry.templates ? makeLocaleTemplateGetters(entry.templates, distDir) : void 0;
1690
+ const i18nKeys = mergeI18nKeys(entry, layoutEntries);
1691
+ const page = {
1692
+ template: "",
1693
+ localeTemplates,
1694
+ loaders,
1695
+ layoutChain,
1696
+ headMeta: entry.head_meta,
1697
+ dataId: manifest.data_id,
1698
+ i18nKeys,
1699
+ pageAssets: entry.assets,
1700
+ projections: entry.projections
1701
+ };
1702
+ Object.defineProperty(page, "template", {
1703
+ get: () => readFileSync(templatePath, "utf-8"),
1704
+ enumerable: true
1705
+ });
1706
+ pages[path] = page;
1707
+ }
1708
+ return pages;
1709
+ }
1710
+ //#endregion
1711
+ //#region src/http-sse.ts
1712
+ const SSE_HEADER = {
1713
+ "Content-Type": "text/event-stream",
1714
+ "Cache-Control": "no-cache",
1715
+ Connection: "keep-alive"
1716
+ };
1717
+ const DEFAULT_HEARTBEAT_MS$1 = 8e3;
1718
+ const DEFAULT_SSE_IDLE_MS = 12e3;
1719
+ function getSseHeaders() {
1720
+ return SSE_HEADER;
1721
+ }
1722
+ function sseDataEvent(data) {
1723
+ return `event: data\ndata: ${JSON.stringify(data)}\n\n`;
1724
+ }
1725
+ function sseDataEventWithId(data, id) {
1726
+ return `event: data\nid: ${id}\ndata: ${JSON.stringify(data)}\n\n`;
1727
+ }
1728
+ function sseErrorEvent(code, message, transient = false) {
1729
+ return `event: error\ndata: ${JSON.stringify({
1730
+ code,
1731
+ message,
1732
+ transient
1733
+ })}\n\n`;
1734
+ }
1735
+ function sseCompleteEvent() {
1736
+ return "event: complete\ndata: {}\n\n";
1737
+ }
1738
+ function formatSseError(error) {
1739
+ if (error instanceof SeamError) return sseErrorEvent(error.code, error.message);
1740
+ return sseErrorEvent("INTERNAL_ERROR", error instanceof Error ? error.message : "Unknown error");
1741
+ }
1742
+ async function* withSseLifecycle(inner, opts) {
1743
+ const heartbeatMs = opts?.heartbeatInterval ?? DEFAULT_HEARTBEAT_MS$1;
1744
+ const idleMs = opts?.sseIdleTimeout ?? DEFAULT_SSE_IDLE_MS;
1745
+ const idleEnabled = idleMs > 0;
1746
+ const queue = [];
1747
+ let resolve = null;
1383
1748
  const signal = () => {
1384
1749
  if (resolve) {
1385
1750
  resolve();
@@ -1399,6 +1764,7 @@ async function* withSseLifecycle(inner, opts) {
1399
1764
  queue.push({ type: "heartbeat" });
1400
1765
  signal();
1401
1766
  }, heartbeatMs);
1767
+ queue.push({ type: "heartbeat" });
1402
1768
  resetIdle();
1403
1769
  (async () => {
1404
1770
  try {
@@ -1433,9 +1799,10 @@ async function* withSseLifecycle(inner, opts) {
1433
1799
  if (idleTimer) clearTimeout(idleTimer);
1434
1800
  }
1435
1801
  }
1436
- async function* sseStream(router, name, input, rawCtx) {
1802
+ async function* sseStream(router, name, input, rawCtx, lastEventId) {
1437
1803
  try {
1438
- for await (const value of router.handleSubscription(name, input, rawCtx)) yield sseDataEvent(value);
1804
+ let seq = 0;
1805
+ for await (const value of router.handleSubscription(name, input, rawCtx, lastEventId)) yield sseDataEventWithId(value, seq++);
1439
1806
  yield sseCompleteEvent();
1440
1807
  } catch (error) {
1441
1808
  yield formatSseError(error);
@@ -1454,120 +1821,147 @@ async function* sseStreamForStream(router, name, input, signal, rawCtx) {
1454
1821
  yield formatSseError(error);
1455
1822
  }
1456
1823
  }
1457
- async function handleBatchHttp(req, router, hashToName, rawCtx) {
1458
- let body;
1459
- try {
1460
- body = await req.body();
1461
- } catch {
1462
- return errorResponse(400, "VALIDATION_ERROR", "Invalid JSON body");
1824
+ function buildHashLookup(hashMap) {
1825
+ if (!hashMap) return null;
1826
+ const map = new Map(Object.entries(hashMap.procedures).map(([n, h]) => [h, n]));
1827
+ map.set("seam.i18n.query", "seam.i18n.query");
1828
+ return map;
1829
+ }
1830
+ function createDevReloadResponse(devState, sseOptions) {
1831
+ const controller = new AbortController();
1832
+ async function* devStream() {
1833
+ yield ": connected\n\n";
1834
+ const aborted = new Promise((_, reject) => {
1835
+ controller.signal.addEventListener("abort", () => reject(/* @__PURE__ */ new Error("aborted")), { once: true });
1836
+ });
1837
+ try {
1838
+ while (!controller.signal.aborted) {
1839
+ await Promise.race([new Promise((r) => {
1840
+ devState.resolvers.add(r);
1841
+ }), aborted]);
1842
+ yield "data: reload\n\n";
1843
+ }
1844
+ } catch {}
1463
1845
  }
1464
- if (!body || typeof body !== "object" || !Array.isArray(body.calls)) return errorResponse(400, "VALIDATION_ERROR", "Batch request must have a 'calls' array");
1465
- const calls = body.calls.map((c) => ({
1466
- procedure: typeof c.procedure === "string" ? hashToName?.get(c.procedure) ?? c.procedure : "",
1467
- input: c.input ?? {}
1468
- }));
1469
- return jsonResponse(200, {
1470
- ok: true,
1471
- data: await router.handleBatch(calls, rawCtx)
1472
- });
1846
+ return {
1847
+ status: 200,
1848
+ headers: {
1849
+ ...SSE_HEADER,
1850
+ "X-Accel-Buffering": "no"
1851
+ },
1852
+ stream: withSseLifecycle(devStream(), {
1853
+ ...sseOptions,
1854
+ sseIdleTimeout: 0
1855
+ }),
1856
+ onCancel: () => controller.abort()
1857
+ };
1473
1858
  }
1474
- /** Resolve hash -> original name when obfuscation is active. Accepts both hashed and raw names. */
1475
- function resolveHashName(hashToName, name) {
1476
- if (!hashToName) return name;
1477
- return hashToName.get(name) ?? name;
1859
+ //#endregion
1860
+ //#region src/mime.ts
1861
+ const MIME_TYPES = {
1862
+ ".js": "application/javascript",
1863
+ ".mjs": "application/javascript",
1864
+ ".css": "text/css",
1865
+ ".html": "text/html",
1866
+ ".json": "application/json",
1867
+ ".svg": "image/svg+xml",
1868
+ ".png": "image/png",
1869
+ ".jpg": "image/jpeg",
1870
+ ".jpeg": "image/jpeg",
1871
+ ".gif": "image/gif",
1872
+ ".woff": "font/woff",
1873
+ ".woff2": "font/woff2",
1874
+ ".ttf": "font/ttf",
1875
+ ".ico": "image/x-icon",
1876
+ ".webp": "image/webp",
1877
+ ".txt": "text/plain",
1878
+ ".xml": "application/xml",
1879
+ ".webmanifest": "application/manifest+json",
1880
+ ".map": "application/json",
1881
+ ".ts": "application/javascript",
1882
+ ".tsx": "application/javascript"
1883
+ };
1884
+ //#endregion
1885
+ //#region src/http-response.ts
1886
+ const JSON_HEADER = { "Content-Type": "application/json" };
1887
+ const PUBLIC_CACHE = "public, max-age=3600";
1888
+ const IMMUTABLE_CACHE = "public, max-age=31536000, immutable";
1889
+ const TEXT_ENCODINGS = new Set([
1890
+ "application/javascript",
1891
+ "application/json",
1892
+ "application/manifest+json",
1893
+ "application/xml",
1894
+ "image/svg+xml"
1895
+ ]);
1896
+ function normalizeBinaryBody(body) {
1897
+ if (body instanceof Uint8Array) return body;
1898
+ if (body instanceof ArrayBuffer) return new Uint8Array(body);
1899
+ return new Uint8Array(body.buffer, body.byteOffset, body.byteLength);
1900
+ }
1901
+ function isTextContentType(contentType) {
1902
+ const mime = contentType.split(";", 1)[0]?.trim().toLowerCase() ?? "";
1903
+ return mime.startsWith("text/") || TEXT_ENCODINGS.has(mime);
1904
+ }
1905
+ async function readResponseBody(filePath, contentType) {
1906
+ const content = await readFile(filePath);
1907
+ return isTextContentType(contentType) ? content.toString("utf-8") : content;
1478
1908
  }
1479
- async function handleProcedurePost(req, router, name, rawCtx, sseOptions) {
1480
- let body;
1909
+ function jsonResponse(status, body) {
1910
+ return {
1911
+ status,
1912
+ headers: JSON_HEADER,
1913
+ body
1914
+ };
1915
+ }
1916
+ function errorResponse(status, code, message) {
1917
+ return jsonResponse(status, new SeamError(code, message).toJSON());
1918
+ }
1919
+ async function handleStaticAsset(assetPath, staticDir) {
1920
+ if (assetPath.includes("..")) return errorResponse(403, "VALIDATION_ERROR", "Forbidden");
1921
+ const filePath = join(staticDir, assetPath);
1481
1922
  try {
1482
- body = await req.body();
1923
+ const contentType = MIME_TYPES[extname(filePath)] || "application/octet-stream";
1924
+ const content = await readResponseBody(filePath, contentType);
1925
+ return {
1926
+ status: 200,
1927
+ headers: {
1928
+ "Content-Type": contentType,
1929
+ "Cache-Control": IMMUTABLE_CACHE
1930
+ },
1931
+ body: content
1932
+ };
1483
1933
  } catch {
1484
- return errorResponse(400, "VALIDATION_ERROR", "Invalid JSON body");
1934
+ return errorResponse(404, "NOT_FOUND", "Asset not found");
1485
1935
  }
1486
- if (router.getKind(name) === "stream") {
1487
- const controller = new AbortController();
1936
+ }
1937
+ async function handlePublicFile(pathname, publicDir) {
1938
+ if (pathname.includes("..")) return null;
1939
+ const filePath = join(publicDir, pathname);
1940
+ try {
1941
+ const contentType = MIME_TYPES[extname(filePath)] || "application/octet-stream";
1942
+ const content = await readResponseBody(filePath, contentType);
1488
1943
  return {
1489
1944
  status: 200,
1490
- headers: SSE_HEADER,
1491
- stream: withSseLifecycle(sseStreamForStream(router, name, body, controller.signal, rawCtx), sseOptions),
1492
- onCancel: () => controller.abort()
1945
+ headers: {
1946
+ "Content-Type": contentType,
1947
+ "Cache-Control": PUBLIC_CACHE
1948
+ },
1949
+ body: content
1493
1950
  };
1951
+ } catch {
1952
+ return null;
1494
1953
  }
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);
1504
- }
1505
- function createHttpHandler(router, opts) {
1506
- const effectiveHashMap = opts?.rpcHashMap ?? router.rpcHashMap;
1507
- const hashToName = effectiveHashMap ? new Map(Object.entries(effectiveHashMap.procedures).map(([n, h]) => [h, n])) : null;
1508
- if (hashToName) hashToName.set("__seam_i18n_query", "__seam_i18n_query");
1509
- const batchHash = effectiveHashMap?.batch ?? null;
1510
- const hasCtx = router.hasContext();
1511
- return async (req) => {
1512
- const url = new URL(req.url, "http://localhost");
1513
- const { pathname } = url;
1514
- const rawCtx = hasCtx ? buildRawContext(router.ctxConfig, req.header, url) : void 0;
1515
- if (req.method === "GET" && pathname === MANIFEST_PATH) {
1516
- if (effectiveHashMap) return errorResponse(403, "FORBIDDEN", "Manifest disabled");
1517
- return jsonResponse(200, router.manifest());
1518
- }
1519
- if (pathname.startsWith(PROCEDURE_PREFIX)) {
1520
- const rawName = pathname.slice(17);
1521
- if (!rawName) return errorResponse(404, "NOT_FOUND", "Empty procedure name");
1522
- if (req.method === "POST") {
1523
- if (rawName === "_batch" || batchHash && rawName === batchHash) return handleBatchHttp(req, router, hashToName, rawCtx);
1524
- return handleProcedurePost(req, router, resolveHashName(hashToName, rawName), rawCtx, opts?.sseOptions);
1525
- }
1526
- if (req.method === "GET") {
1527
- const name = resolveHashName(hashToName, rawName);
1528
- const rawInput = url.searchParams.get("input");
1529
- let input;
1530
- try {
1531
- input = rawInput ? JSON.parse(rawInput) : {};
1532
- } catch {
1533
- return errorResponse(400, "VALIDATION_ERROR", "Invalid input query parameter");
1534
- }
1535
- return {
1536
- status: 200,
1537
- headers: SSE_HEADER,
1538
- stream: withSseLifecycle(sseStream(router, name, input, rawCtx), opts?.sseOptions)
1539
- };
1540
- }
1541
- }
1542
- if (req.method === "GET" && pathname.startsWith(PAGE_PREFIX) && router.hasPages) {
1543
- const pagePath = "/" + pathname.slice(12);
1544
- const headers = req.header ? {
1545
- url: req.url,
1546
- cookie: req.header("cookie") ?? void 0,
1547
- acceptLanguage: req.header("accept-language") ?? void 0
1548
- } : void 0;
1549
- const result = await router.handlePage(pagePath, headers, rawCtx);
1550
- if (result) return {
1551
- status: result.status,
1552
- headers: HTML_HEADER,
1553
- body: result.html
1554
- };
1555
- }
1556
- if (req.method === "GET" && pathname.startsWith(STATIC_PREFIX) && opts?.staticDir) return handleStaticAsset(pathname.slice(14), opts.staticDir);
1557
- if (opts?.fallback) return opts.fallback(req);
1558
- return errorResponse(404, "NOT_FOUND", "Not found");
1559
- };
1560
1954
  }
1561
1955
  function serialize(body) {
1562
- return typeof body === "string" ? body : JSON.stringify(body);
1956
+ if (typeof body === "string") return body;
1957
+ if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) return normalizeBinaryBody(body);
1958
+ return JSON.stringify(body);
1563
1959
  }
1564
- /** Consume an async stream chunk-by-chunk; return false from write to stop early. */
1565
1960
  async function drainStream(stream, write) {
1566
1961
  try {
1567
1962
  for await (const chunk of stream) if (write(chunk) === false) break;
1568
1963
  } catch {}
1569
1964
  }
1570
- /** Convert an HttpResponse to a Web API Response (for adapters using fetch-compatible runtimes) */
1571
1965
  function toWebResponse(result) {
1572
1966
  if ("stream" in result) {
1573
1967
  const stream = result.stream;
@@ -1590,234 +1984,147 @@ function toWebResponse(result) {
1590
1984
  });
1591
1985
  }
1592
1986
  return new Response(serialize(result.body), {
1593
- status: result.status,
1594
- headers: result.headers
1595
- });
1596
- }
1597
-
1598
- //#endregion
1599
- //#region src/page/build-loader.ts
1600
- function normalizeParamConfig(value) {
1601
- return typeof value === "string" ? { from: value } : value;
1602
- }
1603
- function buildLoaderFn(config) {
1604
- return (params, searchParams) => {
1605
- const input = {};
1606
- if (config.params) for (const [key, raw_mapping] of Object.entries(config.params)) {
1607
- const mapping = normalizeParamConfig(raw_mapping);
1608
- const raw = mapping.from === "query" ? searchParams?.get(key) ?? void 0 : params[key];
1609
- if (raw !== void 0) input[key] = mapping.type === "int" ? Number(raw) : raw;
1610
- }
1611
- return {
1612
- procedure: config.procedure,
1613
- input
1614
- };
1615
- };
1616
- }
1617
- function buildLoaderFns(configs) {
1618
- const fns = {};
1619
- for (const [key, config] of Object.entries(configs)) fns[key] = buildLoaderFn(config);
1620
- return fns;
1621
- }
1622
- function resolveTemplatePath(entry, defaultLocale) {
1623
- if (entry.template) return entry.template;
1624
- if (entry.templates) {
1625
- const locale = defaultLocale ?? Object.keys(entry.templates)[0];
1626
- const path = entry.templates[locale];
1627
- if (!path) throw new Error(`No template for locale "${locale}"`);
1628
- return path;
1629
- }
1630
- throw new Error("Manifest entry has neither 'template' nor 'templates'");
1631
- }
1632
- /** Load all locale templates for a manifest entry, keyed by locale */
1633
- function loadLocaleTemplates(entry, distDir) {
1634
- if (!entry.templates) return void 0;
1635
- const result = {};
1636
- for (const [locale, relPath] of Object.entries(entry.templates)) result[locale] = readFileSync(join(distDir, relPath), "utf-8");
1637
- return result;
1638
- }
1639
- /** Resolve parent chain for a layout, returning outer-to-inner order */
1640
- function resolveLayoutChain(layoutId, layoutEntries, getTemplates) {
1641
- const chain = [];
1642
- let currentId = layoutId;
1643
- while (currentId) {
1644
- const entry = layoutEntries[currentId];
1645
- if (!entry) break;
1646
- const { template, localeTemplates } = getTemplates(currentId, entry);
1647
- chain.push({
1648
- id: currentId,
1649
- template,
1650
- localeTemplates,
1651
- loaders: buildLoaderFns(entry.loaders ?? {})
1652
- });
1653
- currentId = entry.parent;
1654
- }
1655
- chain.reverse();
1656
- return chain;
1657
- }
1658
- /** Create a proxy object that lazily reads locale templates from disk */
1659
- function makeLocaleTemplateGetters(templates, distDir) {
1660
- const obj = {};
1661
- for (const [locale, relPath] of Object.entries(templates)) {
1662
- const fullPath = join(distDir, relPath);
1663
- Object.defineProperty(obj, locale, {
1664
- get: () => readFileSync(fullPath, "utf-8"),
1665
- enumerable: true
1666
- });
1667
- }
1668
- return obj;
1669
- }
1670
- /** Merge i18n_keys from route + layout chain into a single list */
1671
- function mergeI18nKeys(route, layoutEntries) {
1672
- const keys = [];
1673
- if (route.layout) {
1674
- let currentId = route.layout;
1675
- while (currentId) {
1676
- const entry = layoutEntries[currentId];
1677
- if (!entry) break;
1678
- if (entry.i18n_keys) keys.push(...entry.i18n_keys);
1679
- currentId = entry.parent;
1680
- }
1681
- }
1682
- if (route.i18n_keys) keys.push(...route.i18n_keys);
1683
- return keys.length > 0 ? keys : void 0;
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
- };
1987
+ status: result.status,
1988
+ headers: result.headers
1989
+ });
1692
1990
  }
1693
- /** Load all build artifacts with lazy template getters (for dev mode) */
1694
- function loadBuildDev(distDir) {
1991
+ //#endregion
1992
+ //#region src/http.ts
1993
+ const PROCEDURE_PREFIX = "/_seam/procedure/";
1994
+ const PAGE_PREFIX = "/_seam/page/";
1995
+ const DATA_PREFIX = "/_seam/data/";
1996
+ const STATIC_PREFIX = "/_seam/static/";
1997
+ const MANIFEST_PATH = "/_seam/manifest.json";
1998
+ const DEV_RELOAD_PATH = "/_seam/dev/reload";
1999
+ const HTML_HEADER = { "Content-Type": "text/html; charset=utf-8" };
2000
+ function getPageRequestHeaders(req) {
2001
+ if (!req.header) return void 0;
1695
2002
  return {
1696
- pages: loadBuildOutputDev(distDir),
1697
- rpcHashMap: loadRpcHashMap(distDir),
1698
- i18n: loadI18nMessages(distDir)
2003
+ url: req.url,
2004
+ cookie: req.header("cookie") ?? void 0,
2005
+ acceptLanguage: req.header("accept-language") ?? void 0
1699
2006
  };
1700
2007
  }
1701
- /** Load the RPC hash map from build output (returns undefined when obfuscation is off) */
1702
- function loadRpcHashMap(distDir) {
1703
- const hashMapPath = join(distDir, "rpc-hash-map.json");
2008
+ async function handleBatchHttp(req, router, hashToName, rawCtx) {
2009
+ let body;
1704
2010
  try {
1705
- return JSON.parse(readFileSync(hashMapPath, "utf-8"));
2011
+ body = await req.body();
1706
2012
  } catch {
1707
- return;
2013
+ return errorResponse(400, "VALIDATION_ERROR", "Invalid JSON body");
1708
2014
  }
2015
+ if (!body || typeof body !== "object" || !Array.isArray(body.calls)) return errorResponse(400, "VALIDATION_ERROR", "Batch request must have a 'calls' array");
2016
+ const calls = body.calls.map((c) => ({
2017
+ procedure: typeof c.procedure === "string" ? hashToName?.get(c.procedure) ?? c.procedure : "",
2018
+ input: c.input ?? {}
2019
+ }));
2020
+ return jsonResponse(200, {
2021
+ ok: true,
2022
+ data: await router.handleBatch(calls, rawCtx)
2023
+ });
1709
2024
  }
1710
- /** Load i18n config and messages from build output */
1711
- function loadI18nMessages(distDir) {
1712
- const manifestPath = join(distDir, "route-manifest.json");
2025
+ function resolveHashName(hashToName, name) {
2026
+ if (!hashToName) return name;
2027
+ return hashToName.get(name) ?? name;
2028
+ }
2029
+ async function handleProcedurePost(req, router, name, rawCtx, sseOptions) {
2030
+ let body;
1713
2031
  try {
1714
- const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
1715
- if (!manifest.i18n) return null;
1716
- const mode = manifest.i18n.mode ?? "memory";
1717
- const cache = manifest.i18n.cache ?? false;
1718
- const routeHashes = manifest.i18n.route_hashes ?? {};
1719
- const contentHashes = manifest.i18n.content_hashes ?? {};
1720
- const messages = {};
1721
- if (mode === "memory") {
1722
- const i18nDir = join(distDir, "i18n");
1723
- for (const locale of manifest.i18n.locales) {
1724
- const localePath = join(i18nDir, `${locale}.json`);
1725
- if (existsSync(localePath)) messages[locale] = JSON.parse(readFileSync(localePath, "utf-8"));
1726
- else messages[locale] = {};
1727
- }
1728
- }
1729
- return {
1730
- locales: manifest.i18n.locales,
1731
- default: manifest.i18n.default,
1732
- mode,
1733
- cache,
1734
- routeHashes,
1735
- contentHashes,
1736
- messages,
1737
- distDir: mode === "paged" ? distDir : void 0
1738
- };
2032
+ body = await req.body();
1739
2033
  } catch {
1740
- return null;
1741
- }
1742
- }
1743
- function loadBuildOutput(distDir) {
1744
- const raw = readFileSync(join(distDir, "route-manifest.json"), "utf-8");
1745
- const manifest = JSON.parse(raw);
1746
- const defaultLocale = manifest.i18n?.default;
1747
- const layoutTemplates = {};
1748
- const layoutLocaleTemplates = {};
1749
- const layoutEntries = manifest.layouts ?? {};
1750
- for (const [id, entry] of Object.entries(layoutEntries)) {
1751
- layoutTemplates[id] = readFileSync(join(distDir, resolveTemplatePath(entry, defaultLocale)), "utf-8");
1752
- const lt = loadLocaleTemplates(entry, distDir);
1753
- if (lt) layoutLocaleTemplates[id] = lt;
2034
+ return errorResponse(400, "VALIDATION_ERROR", "Invalid JSON body");
1754
2035
  }
1755
- const pages = {};
1756
- for (const [path, entry] of Object.entries(manifest.routes)) {
1757
- const template = readFileSync(join(distDir, resolveTemplatePath(entry, defaultLocale)), "utf-8");
1758
- const loaders = buildLoaderFns(entry.loaders);
1759
- const layoutChain = entry.layout ? resolveLayoutChain(entry.layout, layoutEntries, (id) => ({
1760
- template: layoutTemplates[id] ?? "",
1761
- localeTemplates: layoutLocaleTemplates[id]
1762
- })) : [];
1763
- const i18nKeys = mergeI18nKeys(entry, layoutEntries);
1764
- pages[path] = {
1765
- template,
1766
- localeTemplates: loadLocaleTemplates(entry, distDir),
1767
- loaders,
1768
- layoutChain,
1769
- headMeta: entry.head_meta,
1770
- dataId: manifest.data_id,
1771
- i18nKeys,
1772
- pageAssets: entry.assets,
1773
- projections: entry.projections
2036
+ if (router.getKind(name) === "stream") {
2037
+ const controller = new AbortController();
2038
+ return {
2039
+ status: 200,
2040
+ headers: getSseHeaders(),
2041
+ stream: withSseLifecycle(sseStreamForStream(router, name, body, controller.signal, rawCtx), sseOptions),
2042
+ onCancel: () => controller.abort()
1774
2043
  };
1775
2044
  }
1776
- return pages;
2045
+ if (router.getKind(name) === "upload") {
2046
+ if (!req.file) return errorResponse(400, "VALIDATION_ERROR", "Upload requires multipart/form-data");
2047
+ const file = await req.file();
2048
+ if (!file) return errorResponse(400, "VALIDATION_ERROR", "Upload requires file in multipart body");
2049
+ const result = await router.handleUpload(name, body, file, rawCtx);
2050
+ return jsonResponse(result.status, result.body);
2051
+ }
2052
+ const result = await router.handle(name, body, rawCtx);
2053
+ return jsonResponse(result.status, result.body);
1777
2054
  }
1778
- /** Load build output with lazy template getters -- templates re-read from disk on each access */
1779
- function loadBuildOutputDev(distDir) {
1780
- const raw = readFileSync(join(distDir, "route-manifest.json"), "utf-8");
1781
- const manifest = JSON.parse(raw);
1782
- const defaultLocale = manifest.i18n?.default;
1783
- const layoutEntries = manifest.layouts ?? {};
1784
- const pages = {};
1785
- for (const [path, entry] of Object.entries(manifest.routes)) {
1786
- const templatePath = join(distDir, resolveTemplatePath(entry, defaultLocale));
1787
- const loaders = buildLoaderFns(entry.loaders);
1788
- const layoutChain = entry.layout ? resolveLayoutChain(entry.layout, layoutEntries, (id, layoutEntry) => {
1789
- const tmplPath = join(distDir, resolveTemplatePath(layoutEntry, defaultLocale));
1790
- const def = {
1791
- template: "",
1792
- localeTemplates: layoutEntry.templates ? makeLocaleTemplateGetters(layoutEntry.templates, distDir) : void 0
2055
+ function createHttpHandler(router, opts) {
2056
+ const effectiveHashMap = opts?.rpcHashMap ?? router.rpcHashMap;
2057
+ const hashToName = buildHashLookup(effectiveHashMap);
2058
+ const batchHash = effectiveHashMap?.batch ?? null;
2059
+ const hasCtx = router.hasContext();
2060
+ const devDir = opts?.devBuildDir ?? (process.env.SEAM_DEV === "1" && process.env.SEAM_VITE !== "1" ? process.env.SEAM_OUTPUT_DIR : void 0);
2061
+ const devState = devDir ? { resolvers: /* @__PURE__ */ new Set() } : null;
2062
+ if (devState && devDir) watchReloadTrigger(devDir, () => {
2063
+ try {
2064
+ router.reload(loadBuildDev(devDir));
2065
+ } catch {}
2066
+ const batch = devState.resolvers;
2067
+ devState.resolvers = /* @__PURE__ */ new Set();
2068
+ for (const r of batch) r();
2069
+ });
2070
+ return async (req) => {
2071
+ const url = new URL(req.url, "http://localhost");
2072
+ const { pathname } = url;
2073
+ const rawCtx = hasCtx ? buildRawContext(router.ctxConfig, req.header, url) : void 0;
2074
+ if (req.method === "GET" && pathname === MANIFEST_PATH) {
2075
+ if (effectiveHashMap) return errorResponse(403, "FORBIDDEN", "Manifest disabled");
2076
+ return jsonResponse(200, router.manifest());
2077
+ }
2078
+ if (pathname.startsWith(PROCEDURE_PREFIX)) {
2079
+ const rawName = pathname.slice(17);
2080
+ if (!rawName) return errorResponse(404, "NOT_FOUND", "Empty procedure name");
2081
+ if (req.method === "POST") {
2082
+ if (rawName === "_batch" || batchHash && rawName === batchHash) return handleBatchHttp(req, router, hashToName, rawCtx);
2083
+ return handleProcedurePost(req, router, resolveHashName(hashToName, rawName), rawCtx, opts?.sseOptions);
2084
+ }
2085
+ if (req.method === "GET") {
2086
+ const name = resolveHashName(hashToName, rawName);
2087
+ const rawInput = url.searchParams.get("input");
2088
+ let input;
2089
+ try {
2090
+ input = rawInput ? JSON.parse(rawInput) : {};
2091
+ } catch {
2092
+ return errorResponse(400, "VALIDATION_ERROR", "Invalid input query parameter");
2093
+ }
2094
+ const lastEventId = req.header?.("last-event-id") ?? void 0;
2095
+ return {
2096
+ status: 200,
2097
+ headers: getSseHeaders(),
2098
+ stream: withSseLifecycle(sseStream(router, name, input, rawCtx, lastEventId), opts?.sseOptions)
2099
+ };
2100
+ }
2101
+ }
2102
+ if (req.method === "GET" && pathname.startsWith(PAGE_PREFIX) && router.hasPages) {
2103
+ const pagePath = "/" + pathname.slice(12);
2104
+ const headers = getPageRequestHeaders(req);
2105
+ const result = await router.handlePage(pagePath, headers, rawCtx);
2106
+ if (result) return {
2107
+ status: result.status,
2108
+ headers: HTML_HEADER,
2109
+ body: result.html
1793
2110
  };
1794
- Object.defineProperty(def, "template", {
1795
- get: () => readFileSync(tmplPath, "utf-8"),
1796
- enumerable: true
1797
- });
1798
- return def;
1799
- }) : [];
1800
- const localeTemplates = entry.templates ? makeLocaleTemplateGetters(entry.templates, distDir) : void 0;
1801
- const i18nKeys = mergeI18nKeys(entry, layoutEntries);
1802
- const page = {
1803
- template: "",
1804
- localeTemplates,
1805
- loaders,
1806
- layoutChain,
1807
- dataId: manifest.data_id,
1808
- i18nKeys,
1809
- pageAssets: entry.assets,
1810
- projections: entry.projections
1811
- };
1812
- Object.defineProperty(page, "template", {
1813
- get: () => readFileSync(templatePath, "utf-8"),
1814
- enumerable: true
1815
- });
1816
- pages[path] = page;
1817
- }
1818
- return pages;
2111
+ }
2112
+ if (req.method === "GET" && pathname.startsWith(DATA_PREFIX) && router.hasPages) {
2113
+ const pagePath = "/" + pathname.slice(12).replace(/\/$/, "");
2114
+ const dataResult = await router.handlePageData(pagePath);
2115
+ if (dataResult !== null) return jsonResponse(200, dataResult);
2116
+ }
2117
+ if (req.method === "GET" && pathname.startsWith(STATIC_PREFIX) && opts?.staticDir) return handleStaticAsset(pathname.slice(14), opts.staticDir);
2118
+ if (req.method === "GET" && pathname === DEV_RELOAD_PATH && devState) return createDevReloadResponse(devState, opts?.sseOptions);
2119
+ const publicDir = opts?.publicDir ?? router.publicDir;
2120
+ if (req.method === "GET" && publicDir) {
2121
+ const publicResult = await handlePublicFile(pathname, publicDir);
2122
+ if (publicResult) return publicResult;
2123
+ }
2124
+ if (opts?.fallback) return opts.fallback(req);
2125
+ return errorResponse(404, "NOT_FOUND", "Not found");
2126
+ };
1819
2127
  }
1820
-
1821
2128
  //#endregion
1822
2129
  //#region src/subscription.ts
1823
2130
  function fromCallback(setup) {
@@ -1876,10 +2183,9 @@ function fromCallback(setup) {
1876
2183
  }
1877
2184
  return generate();
1878
2185
  }
1879
-
1880
2186
  //#endregion
1881
2187
  //#region src/ws.ts
1882
- const DEFAULT_HEARTBEAT_MS = 21e3;
2188
+ const DEFAULT_HEARTBEAT_MS = 15e3;
1883
2189
  const DEFAULT_PONG_TIMEOUT_MS = 5e3;
1884
2190
  function sendError(ws, id, code, message) {
1885
2191
  ws.send(JSON.stringify({
@@ -2020,7 +2326,6 @@ function startChannelWs(router, channelName, channelInput, ws, opts) {
2020
2326
  }
2021
2327
  };
2022
2328
  }
2023
-
2024
2329
  //#endregion
2025
2330
  //#region src/proxy.ts
2026
2331
  /** Forward non-seam requests to a dev server (e.g. Vite) */
@@ -2082,64 +2387,7 @@ function createStaticHandler(opts) {
2082
2387
  }
2083
2388
  };
2084
2389
  }
2085
-
2086
- //#endregion
2087
- //#region src/dev/reload-watcher.ts
2088
- function watchReloadTrigger(distDir, onReload) {
2089
- const triggerPath = join(distDir, ".reload-trigger");
2090
- let watcher = null;
2091
- let closed = false;
2092
- let pending = [];
2093
- const notify = () => {
2094
- onReload();
2095
- const batch = pending;
2096
- pending = [];
2097
- for (const p of batch) p.resolve();
2098
- };
2099
- const nextReload = () => {
2100
- if (closed) return Promise.reject(/* @__PURE__ */ new Error("watcher closed"));
2101
- return new Promise((resolve, reject) => {
2102
- pending.push({
2103
- resolve,
2104
- reject
2105
- });
2106
- });
2107
- };
2108
- const closeAll = () => {
2109
- closed = true;
2110
- const batch = pending;
2111
- pending = [];
2112
- const err = /* @__PURE__ */ new Error("watcher closed");
2113
- for (const p of batch) p.reject(err);
2114
- };
2115
- try {
2116
- watcher = watch(triggerPath, () => notify());
2117
- } catch {
2118
- const dirWatcher = watch(distDir, (_event, filename) => {
2119
- if (filename === ".reload-trigger") {
2120
- dirWatcher.close();
2121
- watcher = watch(triggerPath, () => notify());
2122
- notify();
2123
- }
2124
- });
2125
- return {
2126
- close() {
2127
- dirWatcher.close();
2128
- watcher?.close();
2129
- closeAll();
2130
- },
2131
- nextReload
2132
- };
2133
- }
2134
- return {
2135
- close() {
2136
- watcher?.close();
2137
- closeAll();
2138
- },
2139
- nextReload
2140
- };
2141
- }
2142
-
2143
2390
  //#endregion
2144
2391
  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 };
2392
+
2145
2393
  //# sourceMappingURL=index.js.map