@canmi/seam-server 0.4.16 → 0.5.10

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
@@ -141,24 +141,21 @@ const t = {
141
141
  };
142
142
 
143
143
  //#endregion
144
- //#region src/manifest/index.ts
145
- function buildManifest(definitions, channels) {
146
- const mapped = {};
147
- for (const [name, def] of Object.entries(definitions)) {
148
- const entry = {
149
- type: def.type === "subscription" ? "subscription" : def.type === "command" ? "command" : "query",
150
- input: def.input._schema,
151
- output: def.output._schema
152
- };
153
- if (def.error) entry.error = def.error._schema;
154
- mapped[name] = entry;
155
- }
156
- const manifest = {
157
- version: 1,
158
- procedures: mapped
144
+ //#region src/validation/index.ts
145
+ function validateInput(schema, data) {
146
+ const errors = validate(schema, data, {
147
+ maxDepth: 32,
148
+ maxErrors: 10
149
+ });
150
+ return {
151
+ valid: errors.length === 0,
152
+ errors
159
153
  };
160
- if (channels && Object.keys(channels).length > 0) manifest.channels = channels;
161
- return manifest;
154
+ }
155
+ function formatValidationErrors(errors) {
156
+ return errors.map((e) => {
157
+ return `${e.instancePath.length > 0 ? e.instancePath.join("/") : "(root)"} (schema: ${e.schemaPath.join("/")})`;
158
+ }).join("; ");
162
159
  }
163
160
 
164
161
  //#endregion
@@ -193,26 +190,111 @@ var SeamError = class extends Error {
193
190
  };
194
191
 
195
192
  //#endregion
196
- //#region src/validation/index.ts
197
- function validateInput(schema, data) {
198
- const errors = validate(schema, data, {
199
- maxDepth: 32,
200
- maxErrors: 10
201
- });
193
+ //#region src/context.ts
194
+ /** Parse extract rule into source type and key, e.g. "header:authorization" -> { source: "header", key: "authorization" } */
195
+ function parseExtractRule(rule) {
196
+ const idx = rule.indexOf(":");
197
+ if (idx === -1) throw new Error(`Invalid extract rule "${rule}": expected "source:key" format`);
198
+ const source = rule.slice(0, idx);
199
+ const key = rule.slice(idx + 1);
200
+ if (!source || !key) throw new Error(`Invalid extract rule "${rule}": source and key must be non-empty`);
202
201
  return {
203
- valid: errors.length === 0,
204
- errors
202
+ source,
203
+ key
205
204
  };
206
205
  }
207
- function formatValidationErrors(errors) {
208
- return errors.map((e) => {
209
- return `${e.instancePath.length > 0 ? e.instancePath.join("/") : "(root)"} (schema: ${e.schemaPath.join("/")})`;
210
- }).join("; ");
206
+ /** Collect all header names needed by the context config */
207
+ function contextExtractKeys(config) {
208
+ const keys = [];
209
+ for (const field of Object.values(config)) {
210
+ const { source, key } = parseExtractRule(field.extract);
211
+ if (source === "header") keys.push(key);
212
+ }
213
+ return [...new Set(keys)];
214
+ }
215
+ /**
216
+ * Resolve raw strings into validated context object.
217
+ *
218
+ * For each requested key:
219
+ * - If raw value is null/missing -> pass null to JTD; schema decides via nullable()
220
+ * - If schema expects string -> use raw value directly
221
+ * - If schema expects object -> JSON.parse then validate
222
+ */
223
+ function resolveContext(config, raw, requestedKeys) {
224
+ const result = {};
225
+ for (const key of requestedKeys) {
226
+ const field = config[key];
227
+ if (!field) throw new SeamError("CONTEXT_ERROR", `Context field "${key}" is not defined in router context config`, 400);
228
+ const { source, key: extractKey } = parseExtractRule(field.extract);
229
+ const rawValue = raw[source === "header" ? extractKey : extractKey] ?? null;
230
+ let value;
231
+ if (rawValue === null) value = null;
232
+ else {
233
+ const schema = field.schema._schema;
234
+ const isStringSchema = "type" in schema && schema.type === "string" && !("nullable" in schema && schema.nullable);
235
+ const isNullableStringSchema = "type" in schema && schema.type === "string" && "nullable" in schema && schema.nullable;
236
+ if (isStringSchema || isNullableStringSchema) value = rawValue;
237
+ else try {
238
+ value = JSON.parse(rawValue);
239
+ } catch {
240
+ throw new SeamError("CONTEXT_ERROR", `Context field "${key}": failed to parse value as JSON`, 400);
241
+ }
242
+ }
243
+ const validation = validateInput(field.schema._schema, value);
244
+ if (!validation.valid) throw new SeamError("CONTEXT_ERROR", `Context field "${key}" validation failed: ${formatValidationErrors(validation.errors)}`, 400);
245
+ result[key] = value;
246
+ }
247
+ return result;
248
+ }
249
+
250
+ //#endregion
251
+ //#region src/manifest/index.ts
252
+ function normalizeInvalidates(targets) {
253
+ return targets.map((t) => {
254
+ if (typeof t === "string") return { query: t };
255
+ const normalized = { query: t.query };
256
+ if (t.mapping) normalized.mapping = Object.fromEntries(Object.entries(t.mapping).map(([k, v]) => [k, typeof v === "string" ? { from: v } : v]));
257
+ return normalized;
258
+ });
259
+ }
260
+ function buildManifest(definitions, channels, contextConfig, transportDefaults) {
261
+ const mapped = {};
262
+ for (const [name, def] of Object.entries(definitions)) {
263
+ const k = def.kind ?? def.type;
264
+ const kind = k === "upload" ? "upload" : k === "stream" ? "stream" : k === "subscription" ? "subscription" : k === "command" ? "command" : "query";
265
+ const entry = {
266
+ kind,
267
+ input: def.input._schema
268
+ };
269
+ if (kind === "stream") entry.chunkOutput = def.output._schema;
270
+ else entry.output = def.output._schema;
271
+ if (def.error) entry.error = def.error._schema;
272
+ if (kind === "command" && def.invalidates && def.invalidates.length > 0) entry.invalidates = normalizeInvalidates(def.invalidates);
273
+ if (def.context && def.context.length > 0) entry.context = def.context;
274
+ const defAny = def;
275
+ if (defAny.transport) entry.transport = defAny.transport;
276
+ if (defAny.suppress) entry.suppress = defAny.suppress;
277
+ if (defAny.cache !== void 0) entry.cache = defAny.cache;
278
+ mapped[name] = entry;
279
+ }
280
+ const context = {};
281
+ if (contextConfig) for (const [key, field] of Object.entries(contextConfig)) context[key] = {
282
+ extract: field.extract,
283
+ schema: field.schema._schema
284
+ };
285
+ const manifest = {
286
+ version: 2,
287
+ context,
288
+ procedures: mapped,
289
+ transportDefaults: transportDefaults ?? {}
290
+ };
291
+ if (channels && Object.keys(channels).length > 0) manifest.channels = channels;
292
+ return manifest;
211
293
  }
212
294
 
213
295
  //#endregion
214
296
  //#region src/router/handler.ts
215
- async function handleRequest(procedures, procedureName, rawBody, validateOutput) {
297
+ async function handleRequest(procedures, procedureName, rawBody, validateOutput, ctx) {
216
298
  const procedure = procedures.get(procedureName);
217
299
  if (!procedure) return {
218
300
  status: 404,
@@ -224,7 +306,10 @@ async function handleRequest(procedures, procedureName, rawBody, validateOutput)
224
306
  body: new SeamError("VALIDATION_ERROR", `Input validation failed: ${formatValidationErrors(validation.errors)}`).toJSON()
225
307
  };
226
308
  try {
227
- const result = await procedure.handler({ input: rawBody });
309
+ const result = await procedure.handler({
310
+ input: rawBody,
311
+ ctx: ctx ?? {}
312
+ });
228
313
  if (validateOutput) {
229
314
  const outValidation = validateInput(procedure.outputSchema, result);
230
315
  if (!outValidation.valid) return {
@@ -250,9 +335,10 @@ async function handleRequest(procedures, procedureName, rawBody, validateOutput)
250
335
  };
251
336
  }
252
337
  }
253
- async function handleBatchRequest(procedures, calls, validateOutput) {
338
+ async function handleBatchRequest(procedures, calls, validateOutput, ctxResolver) {
254
339
  return { results: await Promise.all(calls.map(async (call) => {
255
- const result = await handleRequest(procedures, call.procedure, call.input, validateOutput);
340
+ const ctx = ctxResolver ? ctxResolver(call.procedure) : void 0;
341
+ const result = await handleRequest(procedures, call.procedure, call.input, validateOutput, ctx);
256
342
  if (result.status === 200) return {
257
343
  ok: true,
258
344
  data: result.body.data
@@ -263,12 +349,15 @@ async function handleBatchRequest(procedures, calls, validateOutput) {
263
349
  };
264
350
  })) };
265
351
  }
266
- async function* handleSubscription(subscriptions, name, rawInput, validateOutput) {
352
+ async function* handleSubscription(subscriptions, name, rawInput, validateOutput, ctx) {
267
353
  const sub = subscriptions.get(name);
268
354
  if (!sub) throw new SeamError("NOT_FOUND", `Subscription '${name}' not found`);
269
355
  const validation = validateInput(sub.inputSchema, rawInput);
270
356
  if (!validation.valid) throw new SeamError("VALIDATION_ERROR", `Input validation failed: ${formatValidationErrors(validation.errors)}`);
271
- for await (const value of sub.handler({ input: rawInput })) {
357
+ for await (const value of sub.handler({
358
+ input: rawInput,
359
+ ctx: ctx ?? {}
360
+ })) {
272
361
  if (validateOutput) {
273
362
  const outValidation = validateInput(sub.outputSchema, value);
274
363
  if (!outValidation.valid) throw new SeamError("INTERNAL_ERROR", `Output validation failed: ${formatValidationErrors(outValidation.errors)}`);
@@ -276,17 +365,140 @@ async function* handleSubscription(subscriptions, name, rawInput, validateOutput
276
365
  yield value;
277
366
  }
278
367
  }
368
+ async function handleUploadRequest(uploads, procedureName, rawBody, file, validateOutput, ctx) {
369
+ const upload = uploads.get(procedureName);
370
+ if (!upload) return {
371
+ status: 404,
372
+ body: new SeamError("NOT_FOUND", `Procedure '${procedureName}' not found`).toJSON()
373
+ };
374
+ const validation = validateInput(upload.inputSchema, rawBody);
375
+ if (!validation.valid) return {
376
+ status: 400,
377
+ body: new SeamError("VALIDATION_ERROR", `Input validation failed: ${formatValidationErrors(validation.errors)}`).toJSON()
378
+ };
379
+ try {
380
+ const result = await upload.handler({
381
+ input: rawBody,
382
+ file,
383
+ ctx: ctx ?? {}
384
+ });
385
+ if (validateOutput) {
386
+ const outValidation = validateInput(upload.outputSchema, result);
387
+ if (!outValidation.valid) return {
388
+ status: 500,
389
+ body: new SeamError("INTERNAL_ERROR", `Output validation failed: ${formatValidationErrors(outValidation.errors)}`).toJSON()
390
+ };
391
+ }
392
+ return {
393
+ status: 200,
394
+ body: {
395
+ ok: true,
396
+ data: result
397
+ }
398
+ };
399
+ } catch (error) {
400
+ if (error instanceof SeamError) return {
401
+ status: error.status,
402
+ body: error.toJSON()
403
+ };
404
+ return {
405
+ status: 500,
406
+ body: new SeamError("INTERNAL_ERROR", error instanceof Error ? error.message : "Unknown error").toJSON()
407
+ };
408
+ }
409
+ }
410
+ async function* handleStream(streams, name, rawInput, validateOutput, ctx) {
411
+ const stream = streams.get(name);
412
+ if (!stream) throw new SeamError("NOT_FOUND", `Stream '${name}' not found`);
413
+ const validation = validateInput(stream.inputSchema, rawInput);
414
+ if (!validation.valid) throw new SeamError("VALIDATION_ERROR", `Input validation failed: ${formatValidationErrors(validation.errors)}`);
415
+ for await (const value of stream.handler({
416
+ input: rawInput,
417
+ ctx: ctx ?? {}
418
+ })) {
419
+ if (validateOutput) {
420
+ const outValidation = validateInput(stream.chunkOutputSchema, value);
421
+ if (!outValidation.valid) throw new SeamError("INTERNAL_ERROR", `Output validation failed: ${formatValidationErrors(outValidation.errors)}`);
422
+ }
423
+ yield value;
424
+ }
425
+ }
426
+
427
+ //#endregion
428
+ //#region src/page/projection.ts
429
+ /** Set a nested field by dot-separated path, creating intermediate objects as needed. */
430
+ function setNestedField(target, path, value) {
431
+ const parts = path.split(".");
432
+ let current = target;
433
+ for (let i = 0; i < parts.length - 1; i++) {
434
+ const key = parts[i];
435
+ if (!(key in current) || typeof current[key] !== "object" || current[key] === null) current[key] = {};
436
+ current = current[key];
437
+ }
438
+ current[parts[parts.length - 1]] = value;
439
+ }
440
+ /** Get a nested field by dot-separated path. */
441
+ function getNestedField(source, path) {
442
+ const parts = path.split(".");
443
+ let current = source;
444
+ for (const part of parts) {
445
+ if (current === null || current === void 0 || typeof current !== "object") return;
446
+ current = current[part];
447
+ }
448
+ return current;
449
+ }
450
+ /** Prune a single value according to its projected field paths. */
451
+ function pruneValue(value, fields) {
452
+ const arrayFields = [];
453
+ const plainFields = [];
454
+ for (const f of fields) if (f === "$") return value;
455
+ else if (f.startsWith("$.")) arrayFields.push(f.slice(2));
456
+ else plainFields.push(f);
457
+ if (arrayFields.length > 0 && Array.isArray(value)) return value.map((item) => {
458
+ if (typeof item !== "object" || item === null) return item;
459
+ const pruned = {};
460
+ for (const field of arrayFields) {
461
+ const val = getNestedField(item, field);
462
+ if (val !== void 0) setNestedField(pruned, field, val);
463
+ }
464
+ return pruned;
465
+ });
466
+ if (plainFields.length > 0 && typeof value === "object" && value !== null) {
467
+ const source = value;
468
+ const pruned = {};
469
+ for (const field of plainFields) {
470
+ const val = getNestedField(source, field);
471
+ if (val !== void 0) setNestedField(pruned, field, val);
472
+ }
473
+ return pruned;
474
+ }
475
+ return value;
476
+ }
477
+ /** Prune data to only include projected fields. Missing projection = keep all. */
478
+ function applyProjection(data, projections) {
479
+ if (!projections) return data;
480
+ const result = {};
481
+ for (const [key, value] of Object.entries(data)) {
482
+ const fields = projections[key];
483
+ if (!fields) result[key] = value;
484
+ else result[key] = pruneValue(value, fields);
485
+ }
486
+ return result;
487
+ }
279
488
 
280
489
  //#endregion
281
490
  //#region src/page/handler.ts
282
491
  /** Execute loaders, returning keyed results */
283
- async function executeLoaders(loaders, params, procedures) {
492
+ async function executeLoaders(loaders, params, procedures, searchParams) {
284
493
  const entries = Object.entries(loaders);
285
494
  const results = await Promise.all(entries.map(async ([key, loader]) => {
286
- const { procedure, input } = loader(params);
495
+ const { procedure, input } = loader(params, searchParams);
287
496
  const proc = procedures.get(procedure);
288
497
  if (!proc) throw new SeamError("INTERNAL_ERROR", `Procedure '${procedure}' not found`);
289
- return [key, await proc.handler({ input })];
498
+ return [key, await proc.handler({
499
+ input,
500
+ ctx: {}
501
+ })];
290
502
  }));
291
503
  return Object.fromEntries(results);
292
504
  }
@@ -306,15 +518,16 @@ function lookupMessages(config, routePattern, locale) {
306
518
  }
307
519
  return config.messages[locale]?.[routeHash] ?? {};
308
520
  }
309
- async function handlePageRequest(page, params, procedures, i18nOpts) {
521
+ async function handlePageRequest(page, params, procedures, i18nOpts, searchParams) {
310
522
  try {
311
523
  const t0 = performance.now();
312
524
  const layoutChain = page.layoutChain ?? [];
313
525
  const locale = i18nOpts?.locale;
314
- const loaderResults = await Promise.all([...layoutChain.map((layout) => executeLoaders(layout.loaders, params, procedures)), executeLoaders(page.loaders, params, procedures)]);
526
+ const loaderResults = await Promise.all([...layoutChain.map((layout) => executeLoaders(layout.loaders, params, procedures, searchParams)), executeLoaders(page.loaders, params, procedures, searchParams)]);
315
527
  const t1 = performance.now();
316
528
  const allData = {};
317
529
  for (const result of loaderResults) Object.assign(allData, result);
530
+ const prunedData = applyProjection(allData, page.projections);
318
531
  let composedTemplate = selectTemplate(page.template, page.localeTemplates, locale);
319
532
  for (let i = layoutChain.length - 1; i >= 0; i--) {
320
533
  const layout = layoutChain[i];
@@ -345,7 +558,7 @@ async function handlePageRequest(page, params, procedures, i18nOpts) {
345
558
  }
346
559
  i18nOptsJson = JSON.stringify(i18nData);
347
560
  }
348
- const html = renderPage(composedTemplate, JSON.stringify(allData), JSON.stringify(config), i18nOptsJson);
561
+ const html = renderPage(composedTemplate, JSON.stringify(prunedData), JSON.stringify(config), i18nOptsJson);
349
562
  const t2 = performance.now();
350
563
  return {
351
564
  status: 200,
@@ -366,23 +579,41 @@ async function handlePageRequest(page, params, procedures, i18nOpts) {
366
579
  //#endregion
367
580
  //#region src/page/route-matcher.ts
368
581
  function compileRoute(pattern) {
369
- return { segments: pattern.split("/").filter(Boolean).map((seg) => seg.startsWith(":") ? {
370
- kind: "param",
371
- name: seg.slice(1)
372
- } : {
373
- kind: "static",
374
- value: seg
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
+ };
375
599
  }) };
376
600
  }
377
601
  function matchRoute(segments, pathParts) {
378
- if (segments.length !== pathParts.length) return null;
379
602
  const params = {};
380
603
  for (let i = 0; i < segments.length; i++) {
381
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;
382
612
  if (seg.kind === "static") {
383
613
  if (seg.value !== pathParts[i]) return null;
384
614
  } else params[seg.name] = pathParts[i];
385
615
  }
616
+ if (segments.length !== pathParts.length) return null;
386
617
  return params;
387
618
  }
388
619
  var RouteMatcher = class {
@@ -427,7 +658,9 @@ function fromCookie(name = "seam-locale") {
427
658
  resolve(data) {
428
659
  if (!data.cookie) return null;
429
660
  for (const pair of data.cookie.split(";")) {
430
- const [k, v] = pair.trim().split("=");
661
+ const parts = pair.trim().split("=");
662
+ const k = parts[0];
663
+ const v = parts[1];
431
664
  if (k === name && v && data.locales.includes(v)) return v;
432
665
  }
433
666
  return null;
@@ -442,10 +675,11 @@ function fromAcceptLanguage() {
442
675
  if (!data.acceptLanguage) return null;
443
676
  const entries = [];
444
677
  for (const part of data.acceptLanguage.split(",")) {
445
- const [lang, ...rest] = part.trim().split(";");
678
+ const parts = part.trim().split(";");
679
+ const lang = parts[0];
446
680
  let q = 1;
447
- for (const r of rest) {
448
- const match = r.trim().match(/^q=(\d+(?:\.\d+)?)$/);
681
+ for (let j = 1; j < parts.length; j++) {
682
+ const match = parts[j].trim().match(/^q=(\d+(?:\.\d+)?)$/);
449
683
  if (match) q = parseFloat(match[1]);
450
684
  }
451
685
  entries.push({
@@ -496,10 +730,65 @@ function defaultStrategies() {
496
730
  }
497
731
 
498
732
  //#endregion
499
- //#region src/router/index.ts
500
- function isSubscriptionDef(def) {
501
- return "type" in def && def.type === "subscription";
733
+ //#region src/router/categorize.ts
734
+ function resolveKind(name, def) {
735
+ if ("kind" in def && def.kind) return def.kind;
736
+ if ("type" in def && def.type) {
737
+ console.warn(`[seam] "${name}": "type" field in procedure definition is deprecated, use "kind" instead`);
738
+ return def.type;
739
+ }
740
+ return "query";
741
+ }
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
+ };
502
788
  }
789
+
790
+ //#endregion
791
+ //#region src/router/index.ts
503
792
  /** Build the resolve strategy list from options */
504
793
  function buildStrategies(opts) {
505
794
  const strategies = opts?.resolve ?? defaultStrategies();
@@ -513,6 +802,7 @@ function registerI18nQuery(procedureMap, config) {
513
802
  procedureMap.set("__seam_i18n_query", {
514
803
  inputSchema: {},
515
804
  outputSchema: {},
805
+ contextKeys: [],
516
806
  handler: ({ input }) => {
517
807
  const { route, locale } = input;
518
808
  const messages = lookupI18nMessages(config, route, locale);
@@ -532,19 +822,72 @@ function lookupI18nMessages(config, routeHash, locale) {
532
822
  }
533
823
  return config.messages[locale]?.[routeHash] ?? {};
534
824
  }
535
- function createRouter(procedures, opts) {
536
- const procedureMap = /* @__PURE__ */ new Map();
537
- const subscriptionMap = /* @__PURE__ */ new Map();
538
- for (const [name, def] of Object.entries(procedures)) if (isSubscriptionDef(def)) subscriptionMap.set(name, {
539
- inputSchema: def.input._schema,
540
- outputSchema: def.output._schema,
541
- handler: def.handler
542
- });
543
- else procedureMap.set(name, {
544
- inputSchema: def.input._schema,
545
- outputSchema: def.output._schema,
546
- handler: def.handler
825
+ /** Collect channel metadata from channel results for manifest */
826
+ function collectChannelMeta(channels) {
827
+ if (!channels || channels.length === 0) return void 0;
828
+ return Object.fromEntries(channels.map((ch) => {
829
+ const firstKey = Object.keys(ch.procedures)[0] ?? "";
830
+ return [firstKey.includes(".") ? firstKey.slice(0, firstKey.indexOf(".")) : firstKey, ch.channelMeta];
831
+ }));
832
+ }
833
+ /** Resolve context for a procedure, returning undefined if no context needed */
834
+ function resolveCtxFor(map, name, rawCtx, extractKeys, ctxConfig) {
835
+ if (!rawCtx || extractKeys.length === 0) return void 0;
836
+ const proc = map.get(name);
837
+ if (!proc || proc.contextKeys.length === 0) return void 0;
838
+ return resolveContext(ctxConfig, rawCtx, proc.contextKeys);
839
+ }
840
+ /** Resolve locale and match page route */
841
+ async function matchAndHandlePage(pageMatcher, procedureMap, i18nConfig, strategies, hasUrlPrefix, path, headers) {
842
+ let pathLocale = null;
843
+ let routePath = path;
844
+ if (hasUrlPrefix && i18nConfig) {
845
+ const segments = path.split("/").filter(Boolean);
846
+ const localeSet = new Set(i18nConfig.locales);
847
+ const first = segments[0];
848
+ if (first && localeSet.has(first)) {
849
+ pathLocale = first;
850
+ routePath = "/" + segments.slice(1).join("/") || "/";
851
+ }
852
+ }
853
+ let locale;
854
+ if (i18nConfig) locale = resolveChain(strategies, {
855
+ url: headers?.url ?? "",
856
+ pathLocale,
857
+ cookie: headers?.cookie,
858
+ acceptLanguage: headers?.acceptLanguage,
859
+ locales: i18nConfig.locales,
860
+ defaultLocale: i18nConfig.default
547
861
  });
862
+ const match = pageMatcher.match(routePath);
863
+ if (!match) return null;
864
+ let searchParams;
865
+ if (headers?.url) try {
866
+ const url = new URL(headers.url, "http://localhost");
867
+ if (url.search) searchParams = url.searchParams;
868
+ } catch {}
869
+ const i18nOpts = locale && i18nConfig ? {
870
+ locale,
871
+ config: i18nConfig,
872
+ routePattern: match.pattern
873
+ } : void 0;
874
+ return handlePageRequest(match.value, match.params, procedureMap, i18nOpts, searchParams);
875
+ }
876
+ /** Catch context resolution errors and return them as HandleResult */
877
+ function resolveCtxSafe(map, name, rawCtx, extractKeys, ctxConfig) {
878
+ try {
879
+ return { ctx: resolveCtxFor(map, name, rawCtx, extractKeys, ctxConfig) };
880
+ } catch (err) {
881
+ if (err instanceof SeamError) return { error: {
882
+ status: err.status,
883
+ body: err.toJSON()
884
+ } };
885
+ throw err;
886
+ }
887
+ }
888
+ function createRouter(procedures, opts) {
889
+ const ctxConfig = opts?.context ?? {};
890
+ const { procedureMap, subscriptionMap, streamMap, uploadMap, kindMap } = categorizeProcedures(procedures, Object.keys(ctxConfig).length > 0 ? ctxConfig : void 0);
548
891
  const shouldValidateOutput = opts?.validateOutput ?? (typeof process !== "undefined" && process.env.NODE_ENV !== "production");
549
892
  const pageMatcher = new RouteMatcher();
550
893
  const pages = opts?.pages;
@@ -552,53 +895,41 @@ function createRouter(procedures, opts) {
552
895
  const i18nConfig = opts?.i18n ?? null;
553
896
  const { strategies, hasUrlPrefix } = buildStrategies(opts);
554
897
  if (i18nConfig) registerI18nQuery(procedureMap, i18nConfig);
555
- const channelsMeta = opts?.channels && opts.channels.length > 0 ? Object.fromEntries(opts.channels.map((ch) => {
556
- const firstKey = Object.keys(ch.procedures)[0] ?? "";
557
- return [firstKey.includes(".") ? firstKey.slice(0, firstKey.indexOf(".")) : firstKey, ch.channelMeta];
558
- })) : void 0;
898
+ const channelsMeta = collectChannelMeta(opts?.channels);
899
+ const extractKeys = contextExtractKeys(ctxConfig);
559
900
  return {
560
901
  procedures,
561
902
  hasPages: !!pages && Object.keys(pages).length > 0,
903
+ contextExtractKeys() {
904
+ return extractKeys;
905
+ },
562
906
  manifest() {
563
- return buildManifest(procedures, channelsMeta);
907
+ return buildManifest(procedures, channelsMeta, ctxConfig, opts?.transportDefaults);
564
908
  },
565
- handle(procedureName, body) {
566
- return handleRequest(procedureMap, procedureName, body, shouldValidateOutput);
909
+ async handle(procedureName, body, rawCtx) {
910
+ const { ctx, error } = resolveCtxSafe(procedureMap, procedureName, rawCtx, extractKeys, ctxConfig);
911
+ if (error) return error;
912
+ return handleRequest(procedureMap, procedureName, body, shouldValidateOutput, ctx);
567
913
  },
568
- handleBatch(calls) {
569
- return handleBatchRequest(procedureMap, calls, shouldValidateOutput);
914
+ handleBatch(calls, rawCtx) {
915
+ return handleBatchRequest(procedureMap, calls, shouldValidateOutput, rawCtx ? (name) => resolveCtxFor(procedureMap, name, rawCtx, extractKeys, ctxConfig) ?? {} : void 0);
570
916
  },
571
- handleSubscription(name, input) {
572
- return handleSubscription(subscriptionMap, name, input, shouldValidateOutput);
917
+ handleSubscription(name, input, rawCtx) {
918
+ return handleSubscription(subscriptionMap, name, input, shouldValidateOutput, resolveCtxFor(subscriptionMap, name, rawCtx, extractKeys, ctxConfig));
573
919
  },
574
- async handlePage(path, headers) {
575
- let pathLocale = null;
576
- let routePath = path;
577
- if (hasUrlPrefix && i18nConfig) {
578
- const segments = path.split("/").filter(Boolean);
579
- const localeSet = new Set(i18nConfig.locales);
580
- if (segments.length > 0 && localeSet.has(segments[0])) {
581
- pathLocale = segments[0];
582
- routePath = "/" + segments.slice(1).join("/") || "/";
583
- }
584
- }
585
- let locale;
586
- if (i18nConfig) locale = resolveChain(strategies, {
587
- url: headers?.url ?? "",
588
- pathLocale,
589
- cookie: headers?.cookie,
590
- acceptLanguage: headers?.acceptLanguage,
591
- locales: i18nConfig.locales,
592
- defaultLocale: i18nConfig.default
593
- });
594
- const match = pageMatcher.match(routePath);
595
- if (!match) return null;
596
- const i18nOpts = locale && i18nConfig ? {
597
- locale,
598
- config: i18nConfig,
599
- routePattern: match.pattern
600
- } : void 0;
601
- return handlePageRequest(match.value, match.params, procedureMap, i18nOpts);
920
+ handleStream(name, input, rawCtx) {
921
+ return handleStream(streamMap, name, input, shouldValidateOutput, resolveCtxFor(streamMap, name, rawCtx, extractKeys, ctxConfig));
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);
927
+ },
928
+ getKind(name) {
929
+ return kindMap.get(name) ?? null;
930
+ },
931
+ handlePage(path, headers) {
932
+ return matchAndHandlePage(pageMatcher, procedureMap, i18nConfig, strategies, hasUrlPrefix, path, headers);
602
933
  }
603
934
  };
604
935
  }
@@ -639,7 +970,7 @@ function createChannel(name, def) {
639
970
  const channelInputSchema = def.input._schema;
640
971
  for (const [msgName, msgDef] of Object.entries(def.incoming)) {
641
972
  const command = {
642
- type: "command",
973
+ kind: "command",
643
974
  input: { _schema: mergeObjectSchemas(channelInputSchema, msgDef.input._schema) },
644
975
  output: msgDef.output,
645
976
  handler: msgDef.handler
@@ -649,7 +980,7 @@ function createChannel(name, def) {
649
980
  }
650
981
  const unionSchema = buildOutgoingUnionSchema(def.outgoing);
651
982
  const subscription = {
652
- type: "subscription",
983
+ kind: "subscription",
653
984
  input: def.input,
654
985
  output: { _schema: unionSchema },
655
986
  handler: def.subscribe
@@ -666,13 +997,15 @@ function createChannel(name, def) {
666
997
  }
667
998
  const outgoingMeta = {};
668
999
  for (const [eventName, node] of Object.entries(def.outgoing)) outgoingMeta[eventName] = node._schema;
1000
+ const channelMeta = {
1001
+ input: channelInputSchema,
1002
+ incoming: incomingMeta,
1003
+ outgoing: outgoingMeta
1004
+ };
1005
+ if (def.transport) channelMeta.transport = def.transport;
669
1006
  return {
670
1007
  procedures,
671
- channelMeta: {
672
- input: channelInputSchema,
673
- incoming: incomingMeta,
674
- outgoing: outgoingMeta
675
- }
1008
+ channelMeta
676
1009
  };
677
1010
  }
678
1011
 
@@ -753,6 +1086,10 @@ async function handleStaticAsset(assetPath, staticDir) {
753
1086
  function sseDataEvent(data) {
754
1087
  return `event: data\ndata: ${JSON.stringify(data)}\n\n`;
755
1088
  }
1089
+ /** Format an SSE data event with a sequence id (for streams) */
1090
+ function sseDataEventWithId(data, id) {
1091
+ return `event: data\nid: ${id}\ndata: ${JSON.stringify(data)}\n\n`;
1092
+ }
756
1093
  /** Format an SSE error event */
757
1094
  function sseErrorEvent(code, message, transient = false) {
758
1095
  return `event: error\ndata: ${JSON.stringify({
@@ -765,16 +1102,30 @@ function sseErrorEvent(code, message, transient = false) {
765
1102
  function sseCompleteEvent() {
766
1103
  return "event: complete\ndata: {}\n\n";
767
1104
  }
768
- async function* sseStream(router, name, input) {
1105
+ async function* sseStream(router, name, input, rawCtx) {
769
1106
  try {
770
- for await (const value of router.handleSubscription(name, input)) yield sseDataEvent(value);
1107
+ for await (const value of router.handleSubscription(name, input, rawCtx)) yield sseDataEvent(value);
771
1108
  yield sseCompleteEvent();
772
1109
  } catch (error) {
773
1110
  if (error instanceof SeamError) yield sseErrorEvent(error.code, error.message);
774
1111
  else yield sseErrorEvent("INTERNAL_ERROR", error instanceof Error ? error.message : "Unknown error");
775
1112
  }
776
1113
  }
777
- async function handleBatchHttp(req, router, hashToName) {
1114
+ async function* sseStreamForStream(router, name, input, signal, rawCtx) {
1115
+ const gen = router.handleStream(name, input, rawCtx);
1116
+ if (signal) signal.addEventListener("abort", () => {
1117
+ gen.return(void 0);
1118
+ }, { once: true });
1119
+ try {
1120
+ let seq = 0;
1121
+ for await (const value of gen) yield sseDataEventWithId(value, seq++);
1122
+ yield sseCompleteEvent();
1123
+ } catch (error) {
1124
+ if (error instanceof SeamError) yield sseErrorEvent(error.code, error.message);
1125
+ else yield sseErrorEvent("INTERNAL_ERROR", error instanceof Error ? error.message : "Unknown error");
1126
+ }
1127
+ }
1128
+ async function handleBatchHttp(req, router, hashToName, rawCtx) {
778
1129
  let body;
779
1130
  try {
780
1131
  body = await req.body();
@@ -788,45 +1139,62 @@ async function handleBatchHttp(req, router, hashToName) {
788
1139
  }));
789
1140
  return jsonResponse(200, {
790
1141
  ok: true,
791
- data: await router.handleBatch(calls)
1142
+ data: await router.handleBatch(calls, rawCtx)
792
1143
  });
793
1144
  }
1145
+ /** Resolve hash -> original name when obfuscation is active. Returns null on miss. */
1146
+ function resolveHashName(hashToName, name) {
1147
+ if (!hashToName) return name;
1148
+ return hashToName.get(name) ?? null;
1149
+ }
794
1150
  function createHttpHandler(router, opts) {
795
1151
  const hashToName = opts?.rpcHashMap ? new Map(Object.entries(opts.rpcHashMap.procedures).map(([n, h]) => [h, n])) : null;
796
1152
  if (hashToName) hashToName.set("__seam_i18n_query", "__seam_i18n_query");
797
1153
  const batchHash = opts?.rpcHashMap?.batch ?? null;
1154
+ const ctxExtractKeys = router.contextExtractKeys();
798
1155
  return async (req) => {
799
1156
  const url = new URL(req.url, "http://localhost");
800
1157
  const { pathname } = url;
1158
+ const rawCtx = ctxExtractKeys.length > 0 && req.header ? Object.fromEntries(ctxExtractKeys.map((k) => [k, req.header?.(k) ?? null])) : void 0;
801
1159
  if (req.method === "GET" && pathname === MANIFEST_PATH) {
802
1160
  if (opts?.rpcHashMap) return errorResponse(403, "FORBIDDEN", "Manifest disabled");
803
1161
  return jsonResponse(200, router.manifest());
804
1162
  }
805
1163
  if (pathname.startsWith(PROCEDURE_PREFIX)) {
806
- let name = pathname.slice(17);
807
- if (!name) return errorResponse(404, "NOT_FOUND", "Empty procedure name");
1164
+ const rawName = pathname.slice(17);
1165
+ if (!rawName) return errorResponse(404, "NOT_FOUND", "Empty procedure name");
808
1166
  if (req.method === "POST") {
809
- if (name === "_batch" || batchHash && name === batchHash) return handleBatchHttp(req, router, hashToName);
810
- if (hashToName) {
811
- const resolved = hashToName.get(name);
812
- if (!resolved) return errorResponse(404, "NOT_FOUND", "Not found");
813
- name = resolved;
814
- }
1167
+ if (rawName === "_batch" || batchHash && rawName === batchHash) return handleBatchHttp(req, router, hashToName, rawCtx);
1168
+ const name = resolveHashName(hashToName, rawName);
1169
+ if (!name) return errorResponse(404, "NOT_FOUND", "Not found");
815
1170
  let body;
816
1171
  try {
817
1172
  body = await req.body();
818
1173
  } catch {
819
1174
  return errorResponse(400, "VALIDATION_ERROR", "Invalid JSON body");
820
1175
  }
821
- const result = await router.handle(name, body);
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);
822
1193
  return jsonResponse(result.status, result.body);
823
1194
  }
824
1195
  if (req.method === "GET") {
825
- if (hashToName) {
826
- const resolved = hashToName.get(name);
827
- if (!resolved) return errorResponse(404, "NOT_FOUND", "Not found");
828
- name = resolved;
829
- }
1196
+ const name = resolveHashName(hashToName, rawName);
1197
+ if (!name) return errorResponse(404, "NOT_FOUND", "Not found");
830
1198
  const rawInput = url.searchParams.get("input");
831
1199
  let input;
832
1200
  try {
@@ -837,7 +1205,7 @@ function createHttpHandler(router, opts) {
837
1205
  return {
838
1206
  status: 200,
839
1207
  headers: SSE_HEADER,
840
- stream: sseStream(router, name, input)
1208
+ stream: sseStream(router, name, input, rawCtx)
841
1209
  };
842
1210
  }
843
1211
  }
@@ -873,13 +1241,19 @@ async function drainStream(stream, write) {
873
1241
  function toWebResponse(result) {
874
1242
  if ("stream" in result) {
875
1243
  const stream = result.stream;
1244
+ const onCancel = result.onCancel;
876
1245
  const encoder = new TextEncoder();
877
- const readable = new ReadableStream({ async start(controller) {
878
- await drainStream(stream, (chunk) => {
879
- controller.enqueue(encoder.encode(chunk));
880
- });
881
- controller.close();
882
- } });
1246
+ const readable = new ReadableStream({
1247
+ async start(controller) {
1248
+ await drainStream(stream, (chunk) => {
1249
+ controller.enqueue(encoder.encode(chunk));
1250
+ });
1251
+ controller.close();
1252
+ },
1253
+ cancel() {
1254
+ onCancel?.();
1255
+ }
1256
+ });
883
1257
  return new Response(readable, {
884
1258
  status: result.status,
885
1259
  headers: result.headers
@@ -894,11 +1268,11 @@ function toWebResponse(result) {
894
1268
  //#endregion
895
1269
  //#region src/page/build-loader.ts
896
1270
  function buildLoaderFn(config) {
897
- return (params) => {
1271
+ return (params, searchParams) => {
898
1272
  const input = {};
899
1273
  if (config.params) for (const [key, mapping] of Object.entries(config.params)) {
900
- const raw = params[key];
901
- input[key] = mapping.type === "int" ? Number(raw) : raw;
1274
+ const raw = mapping.from === "query" ? searchParams?.get(key) ?? void 0 : params[key];
1275
+ if (raw !== void 0) input[key] = mapping.type === "int" ? Number(raw) : raw;
902
1276
  }
903
1277
  return {
904
1278
  procedure: config.procedure,
@@ -929,42 +1303,19 @@ function loadLocaleTemplates(entry, distDir) {
929
1303
  return result;
930
1304
  }
931
1305
  /** Resolve parent chain for a layout, returning outer-to-inner order */
932
- function resolveLayoutChain(layoutId, layoutEntries, templates, localeTemplatesMap) {
1306
+ function resolveLayoutChain(layoutId, layoutEntries, getTemplates) {
933
1307
  const chain = [];
934
1308
  let currentId = layoutId;
935
1309
  while (currentId) {
936
1310
  const entry = layoutEntries[currentId];
937
1311
  if (!entry) break;
1312
+ const { template, localeTemplates } = getTemplates(currentId, entry);
938
1313
  chain.push({
939
1314
  id: currentId,
940
- template: templates[currentId],
941
- localeTemplates: localeTemplatesMap[currentId],
942
- loaders: buildLoaderFns(entry.loaders ?? {})
943
- });
944
- currentId = entry.parent;
945
- }
946
- chain.reverse();
947
- return chain;
948
- }
949
- /** Resolve layout chain with lazy template getters (re-read from disk on each access) */
950
- function resolveLayoutChainDev(layoutId, layoutEntries, distDir, defaultLocale) {
951
- const chain = [];
952
- let currentId = layoutId;
953
- while (currentId) {
954
- const entry = layoutEntries[currentId];
955
- if (!entry) break;
956
- const layoutTemplatePath = join(distDir, resolveTemplatePath(entry, defaultLocale));
957
- const def = {
958
- id: currentId,
959
- template: "",
960
- localeTemplates: entry.templates ? makeLocaleTemplateGetters(entry.templates, distDir) : void 0,
1315
+ template,
1316
+ localeTemplates,
961
1317
  loaders: buildLoaderFns(entry.loaders ?? {})
962
- };
963
- Object.defineProperty(def, "template", {
964
- get: () => readFileSync(layoutTemplatePath, "utf-8"),
965
- enumerable: true
966
1318
  });
967
- chain.push(def);
968
1319
  currentId = entry.parent;
969
1320
  }
970
1321
  chain.reverse();
@@ -1055,7 +1406,10 @@ function loadBuildOutput(distDir) {
1055
1406
  for (const [path, entry] of Object.entries(manifest.routes)) {
1056
1407
  const template = readFileSync(join(distDir, resolveTemplatePath(entry, defaultLocale)), "utf-8");
1057
1408
  const loaders = buildLoaderFns(entry.loaders);
1058
- const layoutChain = entry.layout ? resolveLayoutChain(entry.layout, layoutEntries, layoutTemplates, layoutLocaleTemplates) : [];
1409
+ const layoutChain = entry.layout ? resolveLayoutChain(entry.layout, layoutEntries, (id) => ({
1410
+ template: layoutTemplates[id] ?? "",
1411
+ localeTemplates: layoutLocaleTemplates[id]
1412
+ })) : [];
1059
1413
  const i18nKeys = mergeI18nKeys(entry, layoutEntries);
1060
1414
  pages[path] = {
1061
1415
  template,
@@ -1065,7 +1419,8 @@ function loadBuildOutput(distDir) {
1065
1419
  headMeta: entry.head_meta,
1066
1420
  dataId: manifest.data_id,
1067
1421
  i18nKeys,
1068
- pageAssets: entry.assets
1422
+ pageAssets: entry.assets,
1423
+ projections: entry.projections
1069
1424
  };
1070
1425
  }
1071
1426
  return pages;
@@ -1080,7 +1435,18 @@ function loadBuildOutputDev(distDir) {
1080
1435
  for (const [path, entry] of Object.entries(manifest.routes)) {
1081
1436
  const templatePath = join(distDir, resolveTemplatePath(entry, defaultLocale));
1082
1437
  const loaders = buildLoaderFns(entry.loaders);
1083
- const layoutChain = entry.layout ? resolveLayoutChainDev(entry.layout, layoutEntries, distDir, defaultLocale) : [];
1438
+ const layoutChain = entry.layout ? resolveLayoutChain(entry.layout, layoutEntries, (id, layoutEntry) => {
1439
+ const tmplPath = join(distDir, resolveTemplatePath(layoutEntry, defaultLocale));
1440
+ const def = {
1441
+ template: "",
1442
+ localeTemplates: layoutEntry.templates ? makeLocaleTemplateGetters(layoutEntry.templates, distDir) : void 0
1443
+ };
1444
+ Object.defineProperty(def, "template", {
1445
+ get: () => readFileSync(tmplPath, "utf-8"),
1446
+ enumerable: true
1447
+ });
1448
+ return def;
1449
+ }) : [];
1084
1450
  const localeTemplates = entry.templates ? makeLocaleTemplateGetters(entry.templates, distDir) : void 0;
1085
1451
  const i18nKeys = mergeI18nKeys(entry, layoutEntries);
1086
1452
  const page = {
@@ -1090,7 +1456,8 @@ function loadBuildOutputDev(distDir) {
1090
1456
  layoutChain,
1091
1457
  dataId: manifest.data_id,
1092
1458
  i18nKeys,
1093
- pageAssets: entry.assets
1459
+ pageAssets: entry.assets,
1460
+ projections: entry.projections
1094
1461
  };
1095
1462
  Object.defineProperty(page, "template", {
1096
1463
  get: () => readFileSync(templatePath, "utf-8"),
@@ -1339,26 +1706,58 @@ function createStaticHandler(opts) {
1339
1706
  function watchReloadTrigger(distDir, onReload) {
1340
1707
  const triggerPath = join(distDir, ".reload-trigger");
1341
1708
  let watcher = null;
1709
+ let closed = false;
1710
+ let pending = [];
1711
+ const notify = () => {
1712
+ onReload();
1713
+ const batch = pending;
1714
+ pending = [];
1715
+ for (const p of batch) p.resolve();
1716
+ };
1717
+ const nextReload = () => {
1718
+ if (closed) return Promise.reject(/* @__PURE__ */ new Error("watcher closed"));
1719
+ return new Promise((resolve, reject) => {
1720
+ pending.push({
1721
+ resolve,
1722
+ reject
1723
+ });
1724
+ });
1725
+ };
1726
+ const closeAll = () => {
1727
+ closed = true;
1728
+ const batch = pending;
1729
+ pending = [];
1730
+ const err = /* @__PURE__ */ new Error("watcher closed");
1731
+ for (const p of batch) p.reject(err);
1732
+ };
1342
1733
  try {
1343
- watcher = watch(triggerPath, () => onReload());
1734
+ watcher = watch(triggerPath, () => notify());
1344
1735
  } catch {
1345
1736
  const dirWatcher = watch(distDir, (_event, filename) => {
1346
1737
  if (filename === ".reload-trigger") {
1347
1738
  dirWatcher.close();
1348
- watcher = watch(triggerPath, () => onReload());
1349
- onReload();
1739
+ watcher = watch(triggerPath, () => notify());
1740
+ notify();
1350
1741
  }
1351
1742
  });
1352
- return { close() {
1353
- dirWatcher.close();
1354
- watcher?.close();
1355
- } };
1743
+ return {
1744
+ close() {
1745
+ dirWatcher.close();
1746
+ watcher?.close();
1747
+ closeAll();
1748
+ },
1749
+ nextReload
1750
+ };
1356
1751
  }
1357
- return { close() {
1358
- watcher?.close();
1359
- } };
1752
+ return {
1753
+ close() {
1754
+ watcher?.close();
1755
+ closeAll();
1756
+ },
1757
+ nextReload
1758
+ };
1360
1759
  }
1361
1760
 
1362
1761
  //#endregion
1363
- export { SeamError, createChannel, createDevProxy, createHttpHandler, createRouter, createStaticHandler, defaultStrategies, definePage, drainStream, fromAcceptLanguage, fromCallback, fromCookie, fromUrlPrefix, fromUrlQuery, loadBuildOutput, loadBuildOutputDev, loadI18nMessages, loadRpcHashMap, resolveChain, serialize, sseCompleteEvent, sseDataEvent, sseErrorEvent, startChannelWs, t, toWebResponse, watchReloadTrigger };
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 };
1364
1763
  //# sourceMappingURL=index.js.map