@jay-framework/production-server 0.25.5 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1448 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+ import fs from "node:fs/promises";
5
+ import path from "node:path";
6
+ import { makePassthroughInstanceComponent, DevSlowlyChangingPhase, slowRenderInstances, resolveFreshness, mergeFreshness, resolveErrorPageToken, buildErrorViewState, headMetaToHeadTags, mergeHeadTags, serializeHeadTags, serializeScripts, getClientInitData, DEFAULT_GRACE_SECONDS, renderFastChangingData, actionRegistry, needsSameOriginCheck, crossOriginErrorResponse, isCrossOriginRequest, isSecureRequest, parseCookies, setClientInitData } from "@jay-framework/stack-server-runtime";
7
+ import { getLogger } from "@jay-framework/logger";
8
+ import { deepMergeViewStates } from "@jay-framework/view-state-merge";
9
+ import { asyncSwapScript } from "@jay-framework/ssr-runtime";
10
+ import crypto from "node:crypto";
11
+ import { isJayAction, isJayStreamAction } from "@jay-framework/fullstack-component";
12
+ async function generateSitemap(manifest, store, baseUrl, outputPath) {
13
+ const base = baseUrl.replace(/\/$/, "");
14
+ const urls = [];
15
+ for (const route of manifest.routes) {
16
+ if (route.devOnly) continue;
17
+ if (route.noIndex) continue;
18
+ let count = 0;
19
+ for await (const instance of store.listInstances(route)) {
20
+ count++;
21
+ const urlPath = buildUrlFromManifest(route.pattern, instance.params);
22
+ urls.push(`${base}${urlPath}`);
23
+ }
24
+ if (count === 0) {
25
+ const hasDynamic = route.segments.some((s) => s.type !== "static");
26
+ if (!hasDynamic) {
27
+ const urlPath = route.pattern === "/" ? "/" : route.pattern;
28
+ urls.push(`${base}${urlPath}`);
29
+ }
30
+ }
31
+ }
32
+ const tmpPath = outputPath + ".tmp";
33
+ const handle = await fs.open(tmpPath, "w");
34
+ try {
35
+ await handle.write('<?xml version="1.0" encoding="UTF-8"?>\n');
36
+ await handle.write('<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n');
37
+ for (const url of urls) {
38
+ await handle.write(` <url><loc>${escapeXml(url)}</loc></url>
39
+ `);
40
+ }
41
+ await handle.write("</urlset>\n");
42
+ } finally {
43
+ await handle.close();
44
+ }
45
+ await fs.rename(tmpPath, outputPath);
46
+ return urls.length;
47
+ }
48
+ function buildUrlFromManifest(pattern, params) {
49
+ return pattern.replace(/\[\[(\w+)\]\]/g, (_, name) => params[name] || "").replace(/\[\.\.\.(\w+)\]/g, (_, name) => params[name] || "").replace(/\[(\w+)\]/g, (_, name) => params[name] || "").replace(/\/\/+/g, "/").replace(/\/$/, "") || "/";
50
+ }
51
+ function escapeXml(str) {
52
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
53
+ }
54
+ function canonicalParams(params) {
55
+ const sorted = Object.keys(params).sort().reduce(
56
+ (acc, key) => {
57
+ acc[key] = params[key];
58
+ return acc;
59
+ },
60
+ {}
61
+ );
62
+ return JSON.stringify(sorted);
63
+ }
64
+ function instanceKey(routePattern, params) {
65
+ return `${routePattern}:${canonicalParams(params)}`;
66
+ }
67
+ function hashParams(params) {
68
+ const json = canonicalParams(params);
69
+ if (json === "{}") return "";
70
+ return "_" + crypto.createHash("md5").update(json).digest("hex").substring(0, 8);
71
+ }
72
+ function routeDirOf(routePattern) {
73
+ return routePattern.replace(/^\//, "") || "index";
74
+ }
75
+ function instanceIdOf(params) {
76
+ return `page${hashParams(params)}`;
77
+ }
78
+ function routeInstanceDir(routePattern) {
79
+ return `pre-rendered/${routeDirOf(routePattern)}`;
80
+ }
81
+ function deriveCachePath(routePattern, params) {
82
+ return `${routeInstanceDir(routePattern)}/${instanceIdOf(params)}.cache.json`;
83
+ }
84
+ function pagePartsConfigPath(routePattern) {
85
+ return `${routeInstanceDir(routePattern)}/page-parts.json`;
86
+ }
87
+ function synthesizeInstance(route, params) {
88
+ return {
89
+ params,
90
+ cachePath: deriveCachePath(route.pattern, params),
91
+ serverElementPath: route.serverElementPath ?? "",
92
+ clientBundlePath: route.routeClientBundlePath ?? "",
93
+ clientCssPath: route.routeCssPath
94
+ };
95
+ }
96
+ function toInstanceRef(route, params) {
97
+ return { params, cachePath: deriveCachePath(route.pattern, params) };
98
+ }
99
+ const DEFAULT_LIST_TTL_MS = 10 * 6e4;
100
+ class SwrCache {
101
+ constructor(ttlMs = DEFAULT_LIST_TTL_MS, now = Date.now) {
102
+ __publicField(this, "entries", /* @__PURE__ */ new Map());
103
+ __publicField(this, "inflight", /* @__PURE__ */ new Map());
104
+ this.ttlMs = ttlMs;
105
+ this.now = now;
106
+ }
107
+ async get(key, load, fresh = false) {
108
+ const entry = this.entries.get(key);
109
+ if (!entry || fresh) return this.reload(key, load);
110
+ if (this.now() - entry.loadedAt >= this.ttlMs) {
111
+ this.reload(key, load).catch(() => void 0);
112
+ }
113
+ return entry.value;
114
+ }
115
+ /** Forget a key so the next `get` reloads (e.g. after a local write). */
116
+ invalidate(key) {
117
+ this.entries.delete(key);
118
+ }
119
+ reload(key, load) {
120
+ const existing = this.inflight.get(key);
121
+ if (existing) return existing;
122
+ const promise = load().then((value) => {
123
+ this.entries.set(key, { value, loadedAt: this.now() });
124
+ return value;
125
+ }).finally(() => {
126
+ this.inflight.delete(key);
127
+ });
128
+ this.inflight.set(key, promise);
129
+ return promise;
130
+ }
131
+ }
132
+ class CacheNotFoundError extends Error {
133
+ constructor(cachePath) {
134
+ super(`Cache data not found: ${cachePath}`);
135
+ this.cachePath = cachePath;
136
+ this.name = "CacheNotFoundError";
137
+ }
138
+ }
139
+ function serializeCacheEntry(route, params, entry) {
140
+ return JSON.stringify({
141
+ slowViewState: entry.slowViewState,
142
+ carryForward: entry.carryForward,
143
+ ...entry.renderedAt !== void 0 && { renderedAt: entry.renderedAt },
144
+ ...entry.ttl !== void 0 && { ttl: entry.ttl },
145
+ ...entry.grace !== void 0 && { grace: entry.grace },
146
+ route: route.pattern,
147
+ params
148
+ });
149
+ }
150
+ function parseCacheEntry(json) {
151
+ const cacheData = JSON.parse(json);
152
+ return {
153
+ slowViewState: cacheData.slowViewState || {},
154
+ carryForward: cacheData.carryForward || {},
155
+ ...cacheData.renderedAt !== void 0 && { renderedAt: cacheData.renderedAt },
156
+ ...cacheData.ttl !== void 0 && { ttl: cacheData.ttl },
157
+ ...cacheData.grace !== void 0 && { grace: cacheData.grace },
158
+ ...cacheData.route !== void 0 && { route: cacheData.route },
159
+ ...cacheData.params !== void 0 && { params: cacheData.params }
160
+ };
161
+ }
162
+ class FilesystemArtifactStore {
163
+ constructor(basePath, options = {}) {
164
+ __publicField(this, "manifestCache");
165
+ __publicField(this, "metadataMtime");
166
+ __publicField(this, "moduleCache", /* @__PURE__ */ new Map());
167
+ __publicField(this, "listCache");
168
+ this.basePath = basePath;
169
+ this.listCache = new SwrCache(
170
+ options.listTtlMs ?? DEFAULT_LIST_TTL_MS,
171
+ options.now ?? Date.now
172
+ );
173
+ }
174
+ async readManifest() {
175
+ const metadataPath = path.join(this.basePath, "build-metadata.json");
176
+ const manifestPath = path.join(this.basePath, "route-manifest.json");
177
+ try {
178
+ const metaStat = await fs.stat(metadataPath);
179
+ if (this.manifestCache && this.metadataMtime === metaStat.mtimeMs) {
180
+ return this.manifestCache.manifest;
181
+ }
182
+ this.metadataMtime = metaStat.mtimeMs;
183
+ } catch {
184
+ }
185
+ const manifest = JSON.parse(await fs.readFile(manifestPath, "utf-8"));
186
+ const manifestStat = await fs.stat(manifestPath);
187
+ this.manifestCache = { manifest, mtime: manifestStat.mtimeMs };
188
+ return manifest;
189
+ }
190
+ async readCacheData(relativePath) {
191
+ const fullPath = path.join(this.basePath, relativePath);
192
+ let content;
193
+ try {
194
+ content = await fs.readFile(fullPath, "utf-8");
195
+ } catch (err) {
196
+ if (err?.code === "ENOENT") throw new CacheNotFoundError(relativePath);
197
+ throw err;
198
+ }
199
+ return parseCacheEntry(content);
200
+ }
201
+ async readPagePartsConfig(relativePath) {
202
+ return JSON.parse(await fs.readFile(path.join(this.basePath, relativePath), "utf-8"));
203
+ }
204
+ async loadServerElement(relativePath) {
205
+ return this.loadModule(relativePath);
206
+ }
207
+ getAssetPath(relativePath) {
208
+ return path.join(this.basePath, relativePath);
209
+ }
210
+ getBuildDir() {
211
+ return this.basePath;
212
+ }
213
+ async getInstance(route, params) {
214
+ const cachePath = deriveCachePath(route.pattern, params);
215
+ try {
216
+ await fs.stat(path.join(this.basePath, cachePath));
217
+ } catch (err) {
218
+ if (err?.code === "ENOENT") return void 0;
219
+ throw err;
220
+ }
221
+ return synthesizeInstance(route, params);
222
+ }
223
+ async *listInstances(route, options) {
224
+ const refs = await this.listCache.get(
225
+ route.pattern,
226
+ () => this.scanInstances(route),
227
+ options?.fresh === true
228
+ );
229
+ for (const ref of refs) yield ref;
230
+ }
231
+ async writeCacheData(route, params, entry) {
232
+ const cachePath = deriveCachePath(route.pattern, params);
233
+ const fullPath = path.join(this.basePath, cachePath);
234
+ await fs.mkdir(path.dirname(fullPath), { recursive: true });
235
+ const tmpPath = `${fullPath}.${process.pid}.${Date.now().toString(36)}.tmp`;
236
+ await fs.writeFile(tmpPath, serializeCacheEntry(route, params, entry), "utf-8");
237
+ await fs.rename(tmpPath, fullPath);
238
+ this.listCache.invalidate(route.pattern);
239
+ }
240
+ async removeInstance(route, params) {
241
+ const fullPath = path.join(this.basePath, deriveCachePath(route.pattern, params));
242
+ try {
243
+ await fs.unlink(fullPath);
244
+ } catch (err) {
245
+ if (err?.code !== "ENOENT") throw err;
246
+ }
247
+ this.listCache.invalidate(route.pattern);
248
+ }
249
+ async loadModule(modulePath, local) {
250
+ if (local !== false) {
251
+ const fullPath = path.isAbsolute(modulePath) ? modulePath : path.join(this.basePath, modulePath);
252
+ try {
253
+ const stat = await fs.stat(fullPath);
254
+ const cached = this.moduleCache.get(modulePath);
255
+ if (cached && stat.mtimeMs === cached.mtime) {
256
+ return cached.module;
257
+ }
258
+ const mod = await import(fullPath + "?t=" + stat.mtimeMs);
259
+ this.moduleCache.set(modulePath, { module: mod, mtime: stat.mtimeMs });
260
+ return mod;
261
+ } catch {
262
+ if (local) throw new Error(`Local module not found: ${fullPath}`);
263
+ }
264
+ }
265
+ return import(modulePath);
266
+ }
267
+ /** Scan a route's directory for `*.cache.json` and read each instance's params. */
268
+ async scanInstances(route) {
269
+ const dir = routeInstanceDir(route.pattern);
270
+ let files;
271
+ try {
272
+ files = await fs.readdir(path.join(this.basePath, dir));
273
+ } catch (err) {
274
+ if (err?.code === "ENOENT") return [];
275
+ throw err;
276
+ }
277
+ const refs = [];
278
+ for (const file of files.filter((f) => f.endsWith(".cache.json")).sort()) {
279
+ try {
280
+ const content = await fs.readFile(path.join(this.basePath, dir, file), "utf-8");
281
+ const { params, route: storedRoute } = JSON.parse(content);
282
+ if (!params || storedRoute !== void 0 && storedRoute !== route.pattern)
283
+ continue;
284
+ refs.push({ params, cachePath: `${dir}/${file}` });
285
+ } catch {
286
+ }
287
+ }
288
+ return refs;
289
+ }
290
+ }
291
+ function* matchRoutes(manifest, pathname) {
292
+ const urlSegments = pathname.split("/").filter((s) => s.length > 0);
293
+ for (const route of manifest.routes) {
294
+ const params = matchSegments(route.segments, urlSegments);
295
+ if (params) {
296
+ yield { route, params, pathname };
297
+ }
298
+ }
299
+ }
300
+ function matchSegments(routeSegments, urlSegments) {
301
+ const params = {};
302
+ let urlIdx = 0;
303
+ for (let i = 0; i < routeSegments.length; i++) {
304
+ const seg = routeSegments[i];
305
+ if (seg.type === "static") {
306
+ if (urlIdx >= urlSegments.length || urlSegments[urlIdx] !== seg.value) {
307
+ return void 0;
308
+ }
309
+ urlIdx++;
310
+ } else if (seg.type === "param") {
311
+ if (urlIdx >= urlSegments.length) return void 0;
312
+ params[seg.value] = urlSegments[urlIdx];
313
+ urlIdx++;
314
+ } else if (seg.type === "optional") {
315
+ if (urlIdx < urlSegments.length) {
316
+ params[seg.value] = urlSegments[urlIdx];
317
+ urlIdx++;
318
+ }
319
+ } else if (seg.type === "catchAll") {
320
+ if (urlIdx >= urlSegments.length) return void 0;
321
+ params[seg.value] = urlSegments.slice(urlIdx).join("/");
322
+ urlIdx = urlSegments.length;
323
+ } else if (seg.type === "optionalCatchAll") {
324
+ if (urlIdx < urlSegments.length) {
325
+ params[seg.value] = urlSegments.slice(urlIdx).join("/");
326
+ urlIdx = urlSegments.length;
327
+ }
328
+ }
329
+ }
330
+ if (urlIdx !== urlSegments.length) return void 0;
331
+ return params;
332
+ }
333
+ async function resolveRequest(manifest, pathname, store, renderer) {
334
+ for (const { route, params } of matchRoutes(manifest, pathname)) {
335
+ const existing = await store.getInstance(route, params);
336
+ if (existing) {
337
+ return { kind: "matched", match: { route, instance: existing, params, pathname } };
338
+ }
339
+ if (!renderer) continue;
340
+ const outcome = await renderer.renderMissing(route, params);
341
+ if (outcome.kind === "rendered") {
342
+ return {
343
+ kind: "matched",
344
+ match: { route, instance: outcome.instance, params, pathname }
345
+ };
346
+ }
347
+ if (outcome.kind === "redirect") {
348
+ return { kind: "redirect", status: outcome.status, location: outcome.location };
349
+ }
350
+ if (outcome.kind === "unavailable") {
351
+ return { kind: "unavailable", retryAfterSeconds: outcome.retryAfterSeconds };
352
+ }
353
+ }
354
+ return { kind: "notFound" };
355
+ }
356
+ const DEFAULT_NEGATIVE_TTL_MS = 10 * 6e4;
357
+ const DEFAULT_NEGATIVE_MAX_ENTRIES = 1e4;
358
+ class NegativeCache {
359
+ constructor(ttlMs = DEFAULT_NEGATIVE_TTL_MS, maxEntries = DEFAULT_NEGATIVE_MAX_ENTRIES) {
360
+ // Map iteration order is insertion order — re-inserting on hit/add keeps it LRU-ordered.
361
+ __publicField(this, "entries", /* @__PURE__ */ new Map());
362
+ this.ttlMs = ttlMs;
363
+ this.maxEntries = maxEntries;
364
+ }
365
+ has(key, now) {
366
+ const expiresAt = this.entries.get(key);
367
+ if (expiresAt === void 0) return false;
368
+ if (expiresAt <= now) {
369
+ this.entries.delete(key);
370
+ return false;
371
+ }
372
+ this.entries.delete(key);
373
+ this.entries.set(key, expiresAt);
374
+ return true;
375
+ }
376
+ add(key, now) {
377
+ this.entries.delete(key);
378
+ this.entries.set(key, now + this.ttlMs);
379
+ while (this.entries.size > this.maxEntries) {
380
+ const oldest = this.entries.keys().next().value;
381
+ this.entries.delete(oldest);
382
+ }
383
+ }
384
+ /** Forget a key (e.g. when the instance turned out to exist). */
385
+ delete(key) {
386
+ this.entries.delete(key);
387
+ }
388
+ get size() {
389
+ return this.entries.size;
390
+ }
391
+ }
392
+ async function loadPagePartsFromConfig(configPath, artifacts) {
393
+ const config = await artifacts.readPagePartsConfig(configPath);
394
+ async function importModule(entry) {
395
+ if (!entry.modulePath) {
396
+ throw new Error(
397
+ `Empty modulePath in page-parts.json for "${entry.exportName}" (source: ${entry.source}). Rebuild required.`
398
+ );
399
+ }
400
+ return artifacts.loadModule(entry.modulePath, entry.source === "local");
401
+ }
402
+ const parts = [];
403
+ for (const entry of config.parts) {
404
+ const mod = await importModule(entry);
405
+ parts.push({
406
+ compDefinition: mod[entry.exportName] ?? mod.default,
407
+ key: entry.key,
408
+ clientImport: "",
409
+ clientPart: "",
410
+ contractInfo: entry.contractInfo,
411
+ headlessProps: entry.headlessProps
412
+ });
413
+ }
414
+ const headlessInstanceComponents = [];
415
+ for (const entry of config.instanceComponents) {
416
+ const serveTimeContract = {
417
+ props: entry.propNames.map((name) => ({ name }))
418
+ };
419
+ if (entry.structural) {
420
+ headlessInstanceComponents.push({
421
+ contractName: entry.contractName,
422
+ compDefinition: makePassthroughInstanceComponent(entry.structuralTags ?? []),
423
+ contract: serveTimeContract
424
+ });
425
+ continue;
426
+ }
427
+ const mod = await importModule(entry);
428
+ headlessInstanceComponents.push({
429
+ contractName: entry.contractName,
430
+ compDefinition: mod[entry.exportName] ?? mod.default,
431
+ contract: serveTimeContract
432
+ });
433
+ }
434
+ return {
435
+ parts,
436
+ headlessContracts: [],
437
+ headlessInstanceComponents,
438
+ discoveredInstances: config.discoveredInstances ?? [],
439
+ forEachInstances: config.forEachInstances,
440
+ keyedPartModules: [],
441
+ headlessModuleInfos: []
442
+ };
443
+ }
444
+ const pagePartsCache = /* @__PURE__ */ new Map();
445
+ async function getPageParts(route, artifacts) {
446
+ const cached = pagePartsCache.get(route.pattern);
447
+ if (cached) return cached;
448
+ const parts = await loadPagePartsFromConfig(pagePartsConfigPath(route.pattern), artifacts);
449
+ pagePartsCache.set(route.pattern, parts);
450
+ return parts;
451
+ }
452
+ const TRANSIENT_CLIENT_ERRORS = /* @__PURE__ */ new Set([401, 403, 408, 429]);
453
+ const renderInstance = async (route, params, artifacts, now) => {
454
+ let pageParts;
455
+ try {
456
+ pageParts = await getPageParts(route, artifacts);
457
+ } catch (err) {
458
+ return { kind: "error", reason: `Failed to load page parts: ${err?.message ?? err}` };
459
+ }
460
+ const pageUrl = buildUrlFromManifest(route.pattern, params);
461
+ const slowPhase = new DevSlowlyChangingPhase();
462
+ let slowResult;
463
+ try {
464
+ slowResult = await slowPhase.runSlowlyForPage(
465
+ params,
466
+ { language: "en", url: pageUrl },
467
+ pageParts.parts,
468
+ pageParts.discoveredInstances,
469
+ pageParts.headlessInstanceComponents
470
+ );
471
+ } catch (err) {
472
+ return { kind: "error", reason: `Slow render threw: ${err?.message ?? err}` };
473
+ }
474
+ if (slowResult.kind !== "PhaseOutput") {
475
+ if (slowResult.kind === "ClientError" && !TRANSIENT_CLIENT_ERRORS.has(slowResult.status)) {
476
+ return { kind: "notFound" };
477
+ }
478
+ if (slowResult.kind === "Redirect") {
479
+ return {
480
+ kind: "redirect",
481
+ status: slowResult.status,
482
+ location: slowResult.location
483
+ };
484
+ }
485
+ const detail = `${slowResult.status ?? ""} ${slowResult.message ?? ""}`;
486
+ return {
487
+ kind: "error",
488
+ reason: `Slow render returned ${slowResult.kind} ${detail}`.trim()
489
+ };
490
+ }
491
+ const slowViewState = slowResult.rendered;
492
+ const carryForward = slowResult.carryForward;
493
+ let instanceFreshness = {};
494
+ try {
495
+ if (pageParts.discoveredInstances && pageParts.discoveredInstances.length > 0 && pageParts.headlessInstanceComponents.length > 0) {
496
+ const instanceSlowResult = await slowRenderInstances(
497
+ pageParts.discoveredInstances,
498
+ pageParts.headlessInstanceComponents,
499
+ {
500
+ pageViewState: slowViewState,
501
+ pageParams: params,
502
+ pageProps: { language: "en", url: pageUrl }
503
+ }
504
+ );
505
+ if (instanceSlowResult) {
506
+ instanceFreshness = instanceSlowResult.freshness;
507
+ const existingInstances = carryForward.__instances || {
508
+ discovered: [],
509
+ carryForwards: {}
510
+ };
511
+ carryForward.__instances = {
512
+ discovered: [
513
+ ...existingInstances.discovered,
514
+ ...instanceSlowResult.instancePhaseData.discovered
515
+ ],
516
+ carryForwards: {
517
+ ...existingInstances.carryForwards,
518
+ ...instanceSlowResult.instancePhaseData.carryForwards
519
+ },
520
+ slowViewStates: {
521
+ ...existingInstances.slowViewStates || {},
522
+ ...instanceSlowResult.instancePhaseData.slowViewStates
523
+ }
524
+ };
525
+ }
526
+ }
527
+ } catch (err) {
528
+ return { kind: "error", reason: `Instance slow render threw: ${err?.message ?? err}` };
529
+ }
530
+ if (pageParts.forEachInstances && pageParts.forEachInstances.length > 0) {
531
+ const existingInstances = carryForward.__instances || {
532
+ discovered: [],
533
+ carryForwards: {}
534
+ };
535
+ existingInstances.forEachInstances = pageParts.forEachInstances;
536
+ carryForward.__instances = existingInstances;
537
+ }
538
+ const freshness = resolveFreshness(
539
+ mergeFreshness([{ ttl: slowResult.ttl, grace: slowResult.grace }, instanceFreshness])
540
+ );
541
+ return {
542
+ kind: "rendered",
543
+ entry: {
544
+ slowViewState,
545
+ carryForward,
546
+ renderedAt: now,
547
+ ...freshness.ttl !== void 0 && { ttl: freshness.ttl },
548
+ ...freshness.grace !== void 0 && { grace: freshness.grace }
549
+ }
550
+ };
551
+ };
552
+ const DEFAULT_SLOW_RENDER_CONCURRENCY = 5;
553
+ const DEFAULT_MISS_TIMEOUT_MS = 8e3;
554
+ const DEFAULT_RETRY_AFTER_SECONDS = 2;
555
+ const BACKOFF_BASE_MS = 5e3;
556
+ const BACKOFF_CAP_MS = 5 * 6e4;
557
+ const REDIRECT_TTL_MS = 10 * 6e4;
558
+ const REDIRECT_MAX_ENTRIES = 1e3;
559
+ function envNumber(name, fallback) {
560
+ const raw = typeof process !== "undefined" ? process.env?.[name] : void 0;
561
+ const value = raw === void 0 ? NaN : Number(raw);
562
+ return Number.isFinite(value) && value > 0 ? value : fallback;
563
+ }
564
+ class InstanceRenderer {
565
+ constructor(store, options = {}) {
566
+ __publicField(this, "concurrency");
567
+ __publicField(this, "missTimeoutMs");
568
+ __publicField(this, "retryAfterSeconds");
569
+ __publicField(this, "negativeCache");
570
+ __publicField(this, "now");
571
+ __publicField(this, "render");
572
+ __publicField(this, "flights", /* @__PURE__ */ new Map());
573
+ __publicField(this, "backoff", /* @__PURE__ */ new Map());
574
+ __publicField(this, "redirects", /* @__PURE__ */ new Map());
575
+ __publicField(this, "slotsInUse", 0);
576
+ __publicField(this, "busyCount", 0);
577
+ this.store = store;
578
+ this.concurrency = options.concurrency ?? envNumber("JAY_SLOW_RENDER_CONCURRENCY", DEFAULT_SLOW_RENDER_CONCURRENCY);
579
+ this.missTimeoutMs = options.missTimeoutMs ?? envNumber("JAY_MISS_TIMEOUT_MS", DEFAULT_MISS_TIMEOUT_MS);
580
+ this.retryAfterSeconds = options.retryAfterSeconds ?? DEFAULT_RETRY_AFTER_SECONDS;
581
+ this.negativeCache = options.negativeCache ?? new NegativeCache();
582
+ this.now = options.now ?? Date.now;
583
+ this.render = options.render ?? renderInstance;
584
+ }
585
+ /** True when the store can persist rendered instances (otherwise refresh / render-on-miss are off). */
586
+ get canWrite() {
587
+ return typeof this.store.writeCacheData === "function";
588
+ }
589
+ /** Monitoring counters. */
590
+ stats() {
591
+ return {
592
+ slotsInUse: this.slotsInUse,
593
+ inFlight: this.flights.size,
594
+ busy: this.busyCount,
595
+ negativeEntries: this.negativeCache.size
596
+ };
597
+ }
598
+ /**
599
+ * Refresh an existing instance in the background. Never waits for a slot and never throws.
600
+ * Callers may await `done` (only for `started` / `joined`) up to their own budget.
601
+ */
602
+ refresh(route, params) {
603
+ if (!this.canWrite) return { kind: "skipped" };
604
+ const key = instanceKey(route.pattern, params);
605
+ const inFlight = this.flights.get(key);
606
+ if (inFlight) return { kind: "joined", done: this.toEntry(inFlight) };
607
+ if (this.inBackoff(key)) return { kind: "skipped" };
608
+ if (this.slotsInUse >= this.concurrency) {
609
+ this.busyCount++;
610
+ return { kind: "busy" };
611
+ }
612
+ return { kind: "started", done: this.toEntry(this.startFlight(key, route, params, true)) };
613
+ }
614
+ /**
615
+ * Serve an instance that does not exist yet: render it once for all concurrent requests,
616
+ * persist it, and return the synthesized instance. A real 404 is negative-cached.
617
+ */
618
+ async renderMissing(route, params) {
619
+ const key = instanceKey(route.pattern, params);
620
+ if (this.negativeCache.has(key, this.now())) return { kind: "notFound" };
621
+ const known = this.redirects.get(key);
622
+ if (known && known.expiresAt > this.now()) {
623
+ return { kind: "redirect", status: known.status, location: known.location };
624
+ }
625
+ if (!this.canWrite) return { kind: "notFound" };
626
+ let flight = this.flights.get(key);
627
+ if (!flight) {
628
+ if (this.inBackoff(key) || this.slotsInUse >= this.concurrency) {
629
+ if (this.slotsInUse >= this.concurrency) this.busyCount++;
630
+ return this.unavailable();
631
+ }
632
+ flight = this.startFlight(key, route, params, false);
633
+ }
634
+ const result = await this.withTimeout(flight, this.missTimeoutMs);
635
+ if (result === void 0) return this.unavailable();
636
+ if (result.kind === "rendered") {
637
+ return { kind: "rendered", instance: synthesizeInstance(route, params) };
638
+ }
639
+ if (result.kind === "notFound") return { kind: "notFound" };
640
+ if (result.kind === "redirect") {
641
+ return { kind: "redirect", status: result.status, location: result.location };
642
+ }
643
+ return this.unavailable();
644
+ }
645
+ /** True when `(route, params)` is remembered as non-existent. */
646
+ isKnownMissing(route, params) {
647
+ return this.negativeCache.has(instanceKey(route.pattern, params), this.now());
648
+ }
649
+ unavailable() {
650
+ return { kind: "unavailable", retryAfterSeconds: this.retryAfterSeconds };
651
+ }
652
+ inBackoff(key) {
653
+ const backoff = this.backoff.get(key);
654
+ return backoff !== void 0 && backoff.nextAttemptAt > this.now();
655
+ }
656
+ startFlight(key, route, params, existing) {
657
+ this.slotsInUse++;
658
+ const flight = this.runFlight(key, route, params, existing).finally(() => {
659
+ this.slotsInUse--;
660
+ this.flights.delete(key);
661
+ });
662
+ this.flights.set(key, flight);
663
+ return flight;
664
+ }
665
+ async runFlight(key, route, params, existing) {
666
+ const logger = getLogger();
667
+ try {
668
+ const outcome = await this.render(route, params, this.store, this.now());
669
+ if (outcome.kind === "rendered") {
670
+ await this.store.writeCacheData(route, params, outcome.entry);
671
+ this.backoff.delete(key);
672
+ this.negativeCache.delete(key);
673
+ return { kind: "rendered", entry: outcome.entry };
674
+ }
675
+ if (outcome.kind === "notFound") {
676
+ this.backoff.delete(key);
677
+ this.negativeCache.add(key, this.now());
678
+ if (existing) await this.store.removeInstance?.(route, params);
679
+ return { kind: "notFound" };
680
+ }
681
+ if (outcome.kind === "redirect") {
682
+ if (!existing) {
683
+ this.backoff.delete(key);
684
+ this.rememberRedirect(key, outcome.status, outcome.location);
685
+ return { kind: "redirect", status: outcome.status, location: outcome.location };
686
+ }
687
+ this.recordFailure(key);
688
+ logger.warn(`[Refresh] ${key}: slow render now redirects to ${outcome.location}`);
689
+ return { kind: "error" };
690
+ }
691
+ this.recordFailure(key);
692
+ logger.warn(`[Refresh] ${key}: ${outcome.reason}`);
693
+ return { kind: "error" };
694
+ } catch (err) {
695
+ this.recordFailure(key);
696
+ logger.warn(`[Refresh] ${key}: ${err?.message ?? err}`);
697
+ return { kind: "error" };
698
+ }
699
+ }
700
+ rememberRedirect(key, status, location) {
701
+ this.redirects.delete(key);
702
+ this.redirects.set(key, { status, location, expiresAt: this.now() + REDIRECT_TTL_MS });
703
+ while (this.redirects.size > REDIRECT_MAX_ENTRIES) {
704
+ this.redirects.delete(this.redirects.keys().next().value);
705
+ }
706
+ }
707
+ recordFailure(key) {
708
+ const failures = (this.backoff.get(key)?.failures ?? 0) + 1;
709
+ const delay = Math.min(BACKOFF_BASE_MS * 2 ** (failures - 1), BACKOFF_CAP_MS);
710
+ this.backoff.set(key, { failures, nextAttemptAt: this.now() + delay });
711
+ }
712
+ toEntry(flight) {
713
+ return flight.then((result) => result.kind === "rendered" ? result.entry : void 0);
714
+ }
715
+ async withTimeout(promise, ms) {
716
+ let timer;
717
+ const timeout = new Promise((resolve) => {
718
+ timer = setTimeout(() => resolve(void 0), ms);
719
+ });
720
+ try {
721
+ return await Promise.race([promise, timeout]);
722
+ } finally {
723
+ if (timer) clearTimeout(timer);
724
+ }
725
+ }
726
+ }
727
+ function buildImportMap(sharedManifest, publicBasePath, sharedDir = "shared") {
728
+ const imports = {};
729
+ for (const [pkgName, hashedFile] of Object.entries(sharedManifest)) {
730
+ imports[pkgName] = `${publicBasePath}${sharedDir}/${hashedFile}`;
731
+ }
732
+ return imports;
733
+ }
734
+ async function renderErrorPage(manifest, artifacts, staticBaseUrl, info) {
735
+ const plainText = () => new Response(info.message || info.title || "Error", {
736
+ status: info.status,
737
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
738
+ });
739
+ const errorPages = manifest.errorPages;
740
+ if (!errorPages) return plainText();
741
+ const token = resolveErrorPageToken(Object.keys(errorPages), info.status);
742
+ if (!token) return plainText();
743
+ const entry = errorPages[token];
744
+ try {
745
+ return await streamErrorPage(entry, manifest, artifacts, staticBaseUrl, info);
746
+ } catch (err) {
747
+ getLogger().error(
748
+ `[Server] Error page "${token}" render failed (${info.status} ${info.url}): ${err?.message}`
749
+ );
750
+ return plainText();
751
+ }
752
+ }
753
+ async function streamErrorPage(entry, manifest, artifacts, staticBaseUrl, info) {
754
+ const slowViewState = entry.slowCache?.slowViewState ?? {};
755
+ const fullViewState = buildErrorViewState(info, slowViewState);
756
+ const headTagSources = [];
757
+ if (manifest.siteHeadTags && manifest.siteHeadTags.length > 0)
758
+ headTagSources.push(manifest.siteHeadTags);
759
+ const templateHeadTags = headMetaToHeadTags(entry.headMeta, fullViewState);
760
+ const headTags = headTagSources.length > 0 || templateHeadTags.length > 0 ? mergeHeadTags(headTagSources, templateHeadTags) : [];
761
+ const hasCustomTitle = headTags.some((t) => t.tag?.toLowerCase() === "title");
762
+ const titleTag = hasCustomTitle ? "" : ` <title>${info.status} Error</title>
763
+ `;
764
+ const preconnectTags = headTags.filter(
765
+ (t) => t.tag === "link" && t.attrs?.rel === "preconnect"
766
+ );
767
+ const otherHeadTags = headTags.filter(
768
+ (t) => !(t.tag === "link" && t.attrs?.rel === "preconnect")
769
+ );
770
+ const preconnectHtml = preconnectTags.length > 0 ? serializeHeadTags(preconnectTags) + "\n" : "";
771
+ const headTagsHtml = otherHeadTags.length > 0 ? serializeHeadTags(otherHeadTags) + "\n" : "";
772
+ const headScriptsHtml = serializeScripts(entry.scripts, "head");
773
+ const bodyScriptsHtml = serializeScripts(entry.scripts, "body");
774
+ const serverElement = await artifacts.loadServerElement(entry.serverElementPath);
775
+ const asyncPromises = [];
776
+ const importMap = buildImportMap(manifest.sharedManifest, staticBaseUrl);
777
+ if (entry.routeHydratePath) {
778
+ importMap["jay-route-hydrate"] = `${staticBaseUrl}${entry.routeHydratePath}`;
779
+ }
780
+ const clientBundleUrl = entry.routeClientBundlePath ? `${staticBaseUrl}${entry.routeClientBundlePath}` : "";
781
+ const sharedPreloads = entry.sharedDeps ? entry.sharedDeps.map((dep) => importMap[dep]).filter(Boolean) : Object.values(importMap);
782
+ const preloadUrls = clientBundleUrl ? [...sharedPreloads, clientBundleUrl] : [];
783
+ const modulePreloads = preloadUrls.map((url) => ` <link rel="modulepreload" href="${url}" />`).join("\n");
784
+ const cssUrl = entry.routeCssPath ? `${staticBaseUrl}${entry.routeCssPath}` : "";
785
+ const cssPreload = cssUrl ? ` <link rel="preload" href="${cssUrl}" as="style" />` : "";
786
+ const cssLink = cssUrl ? ` <link rel="stylesheet" href="${cssUrl}" />` : "";
787
+ const cssImportPreloads = (entry.cssImports ?? []).map((url) => ` <link rel="preload" href="${url}" as="style" />`).join("\n");
788
+ const encoder = new TextEncoder();
789
+ const stream = new ReadableStream({
790
+ async start(controller) {
791
+ const write = (s) => controller.enqueue(encoder.encode(s));
792
+ const headParts = [headTagsHtml, modulePreloads, cssLink, headScriptsHtml].filter(Boolean).join("\n");
793
+ write(`<!doctype html>
794
+ <html lang="en">
795
+ <head>
796
+ <meta charset="UTF-8" />
797
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
798
+ ${titleTag}${[preconnectHtml, cssPreload, cssImportPreloads].filter(Boolean).map((l) => l + "\n").join(
799
+ ""
800
+ )} <script type="importmap">${JSON.stringify({ imports: importMap })}<\/script>
801
+ ${headParts}
802
+ </head>
803
+ <body>
804
+ <div id="target">`);
805
+ serverElement.renderToStream(fullViewState, {
806
+ write: (chunk) => write(chunk),
807
+ onAsync: (promise, id, templates) => {
808
+ asyncPromises.push(
809
+ promise.then(
810
+ (val) => asyncSwapScript(id, templates.resolved(val)),
811
+ (err) => asyncSwapScript(id, templates.rejected(err))
812
+ )
813
+ );
814
+ }
815
+ });
816
+ write("</div>");
817
+ const asyncScripts = (await Promise.all(asyncPromises)).filter((s) => s).join("");
818
+ if (asyncScripts) write(asyncScripts);
819
+ if (clientBundleUrl) {
820
+ const clientInitData = getClientInitData();
821
+ const initArgs = `${JSON.stringify({})}, ${JSON.stringify(fullViewState)}, ${JSON.stringify({})}, ${JSON.stringify(clientInitData)}`;
822
+ write(`
823
+ <script type="module">
824
+ import { init } from '${clientBundleUrl}';
825
+ await init(${initArgs});
826
+ <\/script>`);
827
+ }
828
+ if (bodyScriptsHtml) write("\n " + bodyScriptsHtml);
829
+ write(`
830
+ </body>
831
+ </html>`);
832
+ controller.close();
833
+ }
834
+ });
835
+ return new Response(stream, {
836
+ status: info.status,
837
+ headers: { "Content-Type": "text/html; charset=utf-8" }
838
+ });
839
+ }
840
+ const FRESHNESS_HEADER = "jay-freshness";
841
+ function hasFreshnessContract(entry) {
842
+ return entry.ttl !== void 0 && entry.renderedAt !== void 0;
843
+ }
844
+ function ageMs(entry, now) {
845
+ return entry.renderedAt === void 0 ? 0 : Math.max(0, now - entry.renderedAt);
846
+ }
847
+ function classify(entry, now) {
848
+ if (!hasFreshnessContract(entry)) return "fresh";
849
+ const ttl = entry.ttl;
850
+ const ageSec = ageMs(entry, now) / 1e3;
851
+ if (ageSec < ttl) return "fresh";
852
+ const grace = Math.max(entry.grace ?? DEFAULT_GRACE_SECONDS, ttl);
853
+ return ageSec < grace ? "stale" : "expired";
854
+ }
855
+ function findKey(headers, name) {
856
+ const lower = name.toLowerCase();
857
+ return Object.keys(headers).find((key) => key.toLowerCase() === lower);
858
+ }
859
+ function remove(headers, name) {
860
+ const key = findKey(headers, name);
861
+ if (key !== void 0) delete headers[key];
862
+ }
863
+ function set(headers, name, value) {
864
+ remove(headers, name);
865
+ headers[name] = value;
866
+ }
867
+ function clampCacheControl(value, remainingSeconds) {
868
+ const directives = value.split(",").map((d) => d.trim()).filter(Boolean);
869
+ const kept = [];
870
+ for (const directive of directives) {
871
+ const [rawName, rawArg] = directive.split("=");
872
+ const name = rawName.trim().toLowerCase();
873
+ if (name === "stale-while-revalidate" || name === "stale-if-error") continue;
874
+ if ((name === "max-age" || name === "s-maxage") && rawArg !== void 0) {
875
+ const current = Number(rawArg.trim().replace(/^"|"$/g, ""));
876
+ const clamped = Number.isFinite(current) ? Math.min(current, remainingSeconds) : remainingSeconds;
877
+ kept.push(`${name}=${clamped}`);
878
+ continue;
879
+ }
880
+ kept.push(directive);
881
+ }
882
+ return kept.join(", ");
883
+ }
884
+ function withRenderedAt(etag, renderedAt) {
885
+ const match = /^(W\/)?"(.*)"$/.exec(etag.trim());
886
+ if (!match) return etag;
887
+ return `${match[1] ?? ""}"${match[2]}-${renderedAt}"`;
888
+ }
889
+ function applyFreshness(headers, entry, state, now, options = {}) {
890
+ if (!hasFreshnessContract(entry)) return headers;
891
+ const result = { ...headers };
892
+ const age = ageMs(entry, now);
893
+ if (state === "stale" || state === "expired") {
894
+ set(result, "Cache-Control", "no-store");
895
+ remove(result, "Expires");
896
+ } else {
897
+ const cacheControlKey = findKey(result, "Cache-Control");
898
+ if (cacheControlKey !== void 0) {
899
+ const remainingSeconds = Math.max(0, Math.floor(entry.ttl - age / 1e3));
900
+ result[cacheControlKey] = clampCacheControl(result[cacheControlKey], remainingSeconds);
901
+ }
902
+ }
903
+ const etagKey = findKey(result, "ETag");
904
+ if (etagKey !== void 0) {
905
+ result[etagKey] = withRenderedAt(result[etagKey], entry.renderedAt);
906
+ }
907
+ if (options.freshnessHeader !== false) {
908
+ set(result, FRESHNESS_HEADER, `${state}, ${Math.round(age)}ms`);
909
+ }
910
+ return result;
911
+ }
912
+ function staleWaitMs() {
913
+ const value = Number(process.env.JAY_STALE_WAIT_MS);
914
+ return Number.isFinite(value) && value >= 0 && process.env.JAY_STALE_WAIT_MS !== void 0 ? value : 2e3;
915
+ }
916
+ function freshnessHeaderEnabled() {
917
+ return process.env.JAY_FRESHNESS_HEADER !== "off";
918
+ }
919
+ async function waitFor(promise, ms) {
920
+ let timer;
921
+ const timeout = new Promise((resolve) => {
922
+ timer = setTimeout(() => resolve(void 0), ms);
923
+ });
924
+ try {
925
+ return await Promise.race([promise, timeout]);
926
+ } finally {
927
+ if (timer) clearTimeout(timer);
928
+ }
929
+ }
930
+ async function resolveEntry(cached, match, renderer) {
931
+ const now = Date.now();
932
+ const state = classify(cached, now);
933
+ if (state === "fresh" || !renderer) return { entry: cached, state };
934
+ const refresh = renderer.refresh(match.route, match.params);
935
+ if (state === "expired" && (refresh.kind === "started" || refresh.kind === "joined")) {
936
+ const renewed = await waitFor(refresh.done, staleWaitMs());
937
+ if (renewed) return { entry: renewed, state: "fresh" };
938
+ }
939
+ return { entry: cached, state };
940
+ }
941
+ async function fetchPageRequest(match, manifest, requestUrl, artifacts, staticBaseUrl, cookies = {}, renderer) {
942
+ const { route, instance } = match;
943
+ const t0 = Date.now();
944
+ let tCache = 0, tParts = 0;
945
+ const [storedEntry, pageParts] = await Promise.all([
946
+ artifacts.readCacheData(instance.cachePath).then((r) => {
947
+ tCache = Date.now() - t0;
948
+ return r;
949
+ }),
950
+ getPageParts(route, artifacts).then((r) => {
951
+ tParts = Date.now() - t0;
952
+ return r;
953
+ })
954
+ ]);
955
+ const tBeforeResolve = Date.now();
956
+ const { entry: cached, state: freshnessState } = await resolveEntry(
957
+ storedEntry,
958
+ match,
959
+ renderer
960
+ );
961
+ const tData = Date.now();
962
+ const query = Object.fromEntries(requestUrl.searchParams.entries());
963
+ const cf = cached.carryForward;
964
+ const fastResult = await renderFastChangingData(
965
+ match.params,
966
+ { params: match.params, query },
967
+ cached.carryForward,
968
+ pageParts.parts,
969
+ cf.__instances,
970
+ pageParts.forEachInstances,
971
+ pageParts.headlessInstanceComponents,
972
+ cached.slowViewState,
973
+ query,
974
+ cookies
975
+ );
976
+ const tFast = Date.now();
977
+ if (fastResult.kind === "Redirect") {
978
+ return new Response(null, {
979
+ status: fastResult.status,
980
+ headers: { Location: fastResult.location }
981
+ });
982
+ }
983
+ if (fastResult.kind === "ServerError" || fastResult.kind === "ClientError") {
984
+ return renderErrorPage(manifest, artifacts, staticBaseUrl, {
985
+ status: fastResult.status,
986
+ url: requestUrl.pathname,
987
+ title: fastResult.title,
988
+ message: fastResult.message,
989
+ code: fastResult.code,
990
+ details: fastResult.details
991
+ });
992
+ }
993
+ const fastViewState = fastResult.rendered || {};
994
+ const fastCarryForward = fastResult.carryForward || {};
995
+ const instanceSlowVS = cf.__instances?.slowViewStates;
996
+ const fullSlowViewState = instanceSlowVS && Object.keys(instanceSlowVS).length > 0 ? { ...cached.slowViewState, __headlessInstances: instanceSlowVS } : cached.slowViewState;
997
+ const fullViewState = deepMergeViewStates(
998
+ fullSlowViewState,
999
+ fastViewState,
1000
+ route.trackByMap || {}
1001
+ );
1002
+ fullViewState.__jay = {
1003
+ params: match.params,
1004
+ url: { path: requestUrl.pathname }
1005
+ };
1006
+ const headTagSources = [];
1007
+ if (manifest.siteHeadTags && manifest.siteHeadTags.length > 0)
1008
+ headTagSources.push(manifest.siteHeadTags);
1009
+ const slowHeadTags = cf.__slowHeadTags;
1010
+ if (slowHeadTags) headTagSources.push(...slowHeadTags);
1011
+ if (fastResult.headTags) headTagSources.push(fastResult.headTags);
1012
+ const templateHeadTags = headMetaToHeadTags(route.headMeta, fullViewState);
1013
+ const headTags = headTagSources.length > 0 || templateHeadTags.length > 0 ? mergeHeadTags(headTagSources, templateHeadTags) : [];
1014
+ const hasCustomTitle = headTags.some((t) => t.tag?.toLowerCase() === "title");
1015
+ const titleTag = hasCustomTitle ? "" : " <title>Vite + TS</title>\n";
1016
+ const preconnectTags = headTags.filter(
1017
+ (t) => t.tag === "link" && t.attrs?.rel === "preconnect"
1018
+ );
1019
+ const otherHeadTags = headTags.filter(
1020
+ (t) => !(t.tag === "link" && t.attrs?.rel === "preconnect")
1021
+ );
1022
+ const preconnectHtml = preconnectTags.length > 0 ? serializeHeadTags(preconnectTags) + "\n" : "";
1023
+ const headTagsHtml = otherHeadTags.length > 0 ? serializeHeadTags(otherHeadTags) + "\n" : "";
1024
+ const headScriptsHtml = serializeScripts(route.scripts, "head");
1025
+ const bodyScriptsHtml = serializeScripts(route.scripts, "body");
1026
+ const serverElementPath = route.serverElementPath || instance.serverElementPath;
1027
+ const tLoadStart = Date.now();
1028
+ const serverElement = await artifacts.loadServerElement(serverElementPath);
1029
+ const tLoad = Date.now();
1030
+ const asyncPromises = [];
1031
+ const importMap = buildImportMap(manifest.sharedManifest, staticBaseUrl);
1032
+ if (route.routeHydratePath) {
1033
+ importMap["jay-route-hydrate"] = `${staticBaseUrl}${route.routeHydratePath}`;
1034
+ }
1035
+ const clientBundleUrl = route.routeClientBundlePath ? `${staticBaseUrl}${route.routeClientBundlePath}` : `${staticBaseUrl}${instance.clientBundlePath}`;
1036
+ const sharedPreloads = route.sharedDeps ? route.sharedDeps.map((dep) => importMap[dep]).filter(Boolean) : Object.values(importMap);
1037
+ const preloadUrls = [...sharedPreloads, clientBundleUrl];
1038
+ const modulePreloads = preloadUrls.map((url) => ` <link rel="modulepreload" href="${url}" />`).join("\n");
1039
+ const cssUrl = instance.clientCssPath ? `${staticBaseUrl}${instance.clientCssPath}` : "";
1040
+ const cssPreload = cssUrl ? ` <link rel="preload" href="${cssUrl}" as="style" />` : "";
1041
+ const cssLink = cssUrl ? ` <link rel="stylesheet" href="${cssUrl}" />` : "";
1042
+ const cssImportPreloads = (route.cssImports ?? []).map((url) => ` <link rel="preload" href="${url}" as="style" />`).join("\n");
1043
+ const encoder = new TextEncoder();
1044
+ const responseHeaders = applyFreshness(
1045
+ fastResult.responseHeaders || {},
1046
+ cached,
1047
+ freshnessState,
1048
+ Date.now(),
1049
+ { freshnessHeader: freshnessHeaderEnabled() }
1050
+ );
1051
+ const freshnessLog = responseHeaders[FRESHNESS_HEADER];
1052
+ const stream = new ReadableStream({
1053
+ async start(controller) {
1054
+ const write = (s) => controller.enqueue(encoder.encode(s));
1055
+ const headParts = [headTagsHtml, modulePreloads, cssLink, headScriptsHtml].filter(Boolean).join("\n");
1056
+ write(`<!doctype html>
1057
+ <html lang="en">
1058
+ <head>
1059
+ <meta charset="UTF-8" />
1060
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
1061
+ ${titleTag}${[preconnectHtml, cssPreload, cssImportPreloads].filter(Boolean).map((l) => l + "\n").join(
1062
+ ""
1063
+ )} <script type="importmap">${JSON.stringify({ imports: importMap })}<\/script>
1064
+ ${headParts}
1065
+ </head>
1066
+ <body>
1067
+ <div id="target">`);
1068
+ const tSsrStart = Date.now();
1069
+ serverElement.renderToStream(fullViewState, {
1070
+ write: (chunk) => write(chunk),
1071
+ onAsync: (promise, id, templates) => {
1072
+ asyncPromises.push(
1073
+ promise.then(
1074
+ (val) => asyncSwapScript(id, templates.resolved(val)),
1075
+ (err) => asyncSwapScript(id, templates.rejected(err))
1076
+ )
1077
+ );
1078
+ }
1079
+ });
1080
+ const tSsr = Date.now();
1081
+ write("</div>");
1082
+ const asyncScripts = (await Promise.all(asyncPromises)).filter((s) => s).join("");
1083
+ if (asyncScripts) write(asyncScripts);
1084
+ const clientInitData = getClientInitData();
1085
+ const initArgs = route.routeClientBundlePath ? `${JSON.stringify(fullSlowViewState)}, ${JSON.stringify(fastViewState)}, ${JSON.stringify(fastCarryForward)}, ${JSON.stringify(clientInitData)}` : `${JSON.stringify(fastViewState)}, ${JSON.stringify(fastCarryForward)}, ${JSON.stringify(clientInitData)}`;
1086
+ const tTotal = Date.now() - t0;
1087
+ const tLoadMs = tLoad - tLoadStart;
1088
+ const serverTiming = {
1089
+ cache: tCache,
1090
+ parts: tParts,
1091
+ refresh: tData - tBeforeResolve,
1092
+ fast: tFast - tData,
1093
+ load: tLoadMs,
1094
+ ssr: tSsr - tSsrStart,
1095
+ total: tTotal
1096
+ };
1097
+ write(`
1098
+ <script>console.log('[jay] server: cache=${serverTiming.cache}ms parts=${serverTiming.parts}ms refresh=${serverTiming.refresh}ms fast=${serverTiming.fast}ms load=${serverTiming.load}ms ssr=${serverTiming.ssr}ms total=${serverTiming.total}ms${freshnessLog ? ` freshness=${freshnessLog}` : ""}')<\/script>
1099
+ <script type="module">
1100
+ import { init } from '${clientBundleUrl}';
1101
+ const _t=performance.now();
1102
+ await init(${initArgs});
1103
+ console.log('[jay] hydrate: '+(performance.now()-_t).toFixed(1)+'ms');
1104
+ <\/script>${bodyScriptsHtml ? "\n " + bodyScriptsHtml : ""}
1105
+ </body>
1106
+ </html>`);
1107
+ controller.close();
1108
+ const timingParts = [
1109
+ `cache: ${tCache}ms`,
1110
+ `parts: ${tParts}ms`,
1111
+ `refresh: ${serverTiming.refresh}ms`,
1112
+ `fast: ${serverTiming.fast}ms`,
1113
+ `load: ${tLoadMs}ms`,
1114
+ `ssr: ${serverTiming.ssr}ms`
1115
+ ];
1116
+ if (freshnessLog) timingParts.push(`${freshnessLog}`);
1117
+ console.log(`GET ${match.pathname} [${timingParts.join(" | ")}] ${tTotal}ms`);
1118
+ }
1119
+ });
1120
+ return new Response(stream, {
1121
+ headers: { "Content-Type": "text/html; charset=utf-8", ...responseHeaders }
1122
+ });
1123
+ }
1124
+ const ACTION_PREFIX = "/_jay/actions/";
1125
+ function isActionRequest(pathname) {
1126
+ return pathname.startsWith(ACTION_PREFIX);
1127
+ }
1128
+ async function fetchActionRequest(request, registry = actionRegistry) {
1129
+ const url = new URL(request.url);
1130
+ const actionName = url.pathname.slice(ACTION_PREFIX.length);
1131
+ if (!actionName) {
1132
+ return jsonResponse(400, {
1133
+ success: false,
1134
+ error: {
1135
+ code: "MISSING_ACTION_NAME",
1136
+ message: "Action name is required",
1137
+ isActionError: false
1138
+ }
1139
+ });
1140
+ }
1141
+ const action = registry.get(actionName);
1142
+ if (!action) {
1143
+ return jsonResponse(404, {
1144
+ success: false,
1145
+ error: {
1146
+ code: "ACTION_NOT_FOUND",
1147
+ message: `Action '${actionName}' is not registered`,
1148
+ isActionError: false
1149
+ }
1150
+ });
1151
+ }
1152
+ const requestMethod = request.method.toUpperCase();
1153
+ if (requestMethod !== action.method) {
1154
+ return jsonResponse(405, {
1155
+ success: false,
1156
+ error: {
1157
+ code: "METHOD_NOT_ALLOWED",
1158
+ message: `Action '${actionName}' expects ${action.method}, got ${requestMethod}`,
1159
+ isActionError: false
1160
+ }
1161
+ });
1162
+ }
1163
+ if (needsSameOriginCheck(action) && isCrossOrigin(request, url)) {
1164
+ return jsonResponse(403, crossOriginErrorResponse(actionName));
1165
+ }
1166
+ let input;
1167
+ try {
1168
+ if (requestMethod === "GET") {
1169
+ const inputParam = url.searchParams.get("_input");
1170
+ if (inputParam) {
1171
+ input = JSON.parse(inputParam);
1172
+ } else {
1173
+ input = Object.fromEntries(url.searchParams.entries());
1174
+ delete input._input;
1175
+ }
1176
+ } else {
1177
+ const text = await request.text();
1178
+ input = text ? JSON.parse(text) : {};
1179
+ }
1180
+ } catch {
1181
+ return jsonResponse(400, {
1182
+ success: false,
1183
+ error: {
1184
+ code: "INVALID_INPUT",
1185
+ message: "Failed to parse request input",
1186
+ isActionError: false
1187
+ }
1188
+ });
1189
+ }
1190
+ const incoming = action.needsRequest ? readIncomingRequest(request, url) : void 0;
1191
+ const perUserHeaders = action.needsRequest ? { "Cache-Control": registry.getCacheHeaders(actionName) } : {};
1192
+ if (registry.isStreaming(actionName)) {
1193
+ const encoder = new TextEncoder();
1194
+ const stream = new ReadableStream({
1195
+ async start(controller) {
1196
+ try {
1197
+ const generator = registry.executeStream(actionName, input, incoming);
1198
+ for await (const chunk of generator) {
1199
+ controller.enqueue(encoder.encode(JSON.stringify({ chunk }) + "\n"));
1200
+ }
1201
+ controller.enqueue(encoder.encode(JSON.stringify({ done: true }) + "\n"));
1202
+ } catch (err) {
1203
+ controller.enqueue(
1204
+ encoder.encode(JSON.stringify({ error: err.message }) + "\n")
1205
+ );
1206
+ }
1207
+ controller.close();
1208
+ }
1209
+ });
1210
+ return new Response(stream, {
1211
+ headers: { "Content-Type": "application/x-ndjson", ...perUserHeaders }
1212
+ });
1213
+ }
1214
+ const result = await registry.execute(actionName, input, incoming);
1215
+ if (result.success) {
1216
+ const headers = new Headers(perUserHeaders);
1217
+ if (requestMethod === "GET") {
1218
+ const cacheHeaders = registry.getCacheHeaders(actionName);
1219
+ if (cacheHeaders) {
1220
+ headers.set("Cache-Control", cacheHeaders);
1221
+ }
1222
+ }
1223
+ for (const setCookie of result.setCookies ?? []) {
1224
+ headers.append("Set-Cookie", setCookie);
1225
+ }
1226
+ return jsonResponse(200, { success: true, data: result.data }, headers);
1227
+ } else {
1228
+ const statusCode = getStatusCode(result.error.code, result.error.isActionError);
1229
+ return jsonResponse(
1230
+ statusCode,
1231
+ { success: false, error: result.error },
1232
+ new Headers(perUserHeaders)
1233
+ );
1234
+ }
1235
+ }
1236
+ function isCrossOrigin(request, url) {
1237
+ return isCrossOriginRequest({
1238
+ secFetchSite: request.headers.get("sec-fetch-site"),
1239
+ origin: request.headers.get("origin"),
1240
+ requestOrigin: url.origin
1241
+ });
1242
+ }
1243
+ function readIncomingRequest(request, url) {
1244
+ return {
1245
+ cookies: parseCookies(request.headers.get("cookie")),
1246
+ secure: isSecureRequest(url.protocol, request.headers.get("x-forwarded-proto"))
1247
+ };
1248
+ }
1249
+ function jsonResponse(status, body, extraHeaders = new Headers()) {
1250
+ extraHeaders.set("Content-Type", "application/json");
1251
+ return new Response(JSON.stringify(body), { status, headers: extraHeaders });
1252
+ }
1253
+ async function registerActionsFromManifest(actions, buildDir, registry = actionRegistry) {
1254
+ const logger = getLogger();
1255
+ let count = 0;
1256
+ for (const entry of actions) {
1257
+ try {
1258
+ const modulePath = entry.isPlugin ? entry.packageName : `${buildDir}/${entry.serverModule}`;
1259
+ const mod = await import(modulePath);
1260
+ for (const [, exported] of Object.entries(mod)) {
1261
+ if (isJayAction(exported)) {
1262
+ registry.register(exported);
1263
+ count++;
1264
+ } else if (isJayStreamAction(exported)) {
1265
+ registry.registerStream(exported);
1266
+ count++;
1267
+ }
1268
+ }
1269
+ } catch (err) {
1270
+ logger.error(
1271
+ `[Server] Failed to load action module ${entry.serverModule}: ${err.message}`
1272
+ );
1273
+ }
1274
+ }
1275
+ logger.info(`[Server] Registered ${count} actions`);
1276
+ }
1277
+ async function registerActionsFromModules(modules, registry = actionRegistry) {
1278
+ const logger = getLogger();
1279
+ let count = 0;
1280
+ for (const { module, name } of modules) {
1281
+ for (const [, exported] of Object.entries(module)) {
1282
+ if (isJayAction(exported)) {
1283
+ registry.register(exported);
1284
+ count++;
1285
+ } else if (isJayStreamAction(exported)) {
1286
+ registry.registerStream(exported);
1287
+ count++;
1288
+ }
1289
+ }
1290
+ }
1291
+ logger.info(`[Server] Registered ${count} actions from pre-imported modules`);
1292
+ }
1293
+ function getStatusCode(code, isActionError) {
1294
+ if (isActionError) return 422;
1295
+ switch (code) {
1296
+ case "ACTION_NOT_FOUND":
1297
+ return 404;
1298
+ case "INVALID_INPUT":
1299
+ case "VALIDATION_ERROR":
1300
+ return 400;
1301
+ case "UNAUTHORIZED":
1302
+ return 401;
1303
+ case "FORBIDDEN":
1304
+ return 403;
1305
+ default:
1306
+ return 500;
1307
+ }
1308
+ }
1309
+ const MIME_TYPES = {
1310
+ ".js": "application/javascript",
1311
+ ".css": "text/css",
1312
+ ".json": "application/json",
1313
+ ".html": "text/html",
1314
+ ".svg": "image/svg+xml",
1315
+ ".png": "image/png",
1316
+ ".jpg": "image/jpeg",
1317
+ ".jpeg": "image/jpeg",
1318
+ ".gif": "image/gif",
1319
+ ".ico": "image/x-icon",
1320
+ ".woff2": "font/woff2",
1321
+ ".woff": "font/woff",
1322
+ ".ttf": "font/ttf",
1323
+ ".webp": "image/webp",
1324
+ ".xml": "application/xml",
1325
+ ".txt": "text/plain",
1326
+ // Agent-readiness resources (DL#217): SKILL.md + packaged skill archives.
1327
+ ".md": "text/markdown",
1328
+ ".gz": "application/gzip",
1329
+ // `.tar.gz` resolves via its last extension `.gz`
1330
+ ".tar": "application/x-tar"
1331
+ };
1332
+ async function fetchStaticFile(pathname, frontendDir) {
1333
+ const normalizedBase = path.resolve(frontendDir);
1334
+ for (const candidate of [path.join(frontendDir, pathname)]) {
1335
+ const normalizedFile = path.resolve(candidate);
1336
+ if (!normalizedFile.startsWith(normalizedBase)) continue;
1337
+ try {
1338
+ const content = await fs.readFile(candidate);
1339
+ const ext = path.extname(candidate);
1340
+ const contentType = MIME_TYPES[ext] || "application/octet-stream";
1341
+ const isHashed = /[-][a-zA-Z0-9_-]{6,}\./.test(path.basename(candidate));
1342
+ const cacheControl = isHashed ? "public, max-age=31536000, immutable" : "public, max-age=3600";
1343
+ const headers = {
1344
+ "Content-Type": contentType,
1345
+ "Content-Length": String(content.length),
1346
+ "Cache-Control": cacheControl
1347
+ };
1348
+ if (pathname.startsWith("/.well-known/")) {
1349
+ headers["Access-Control-Allow-Origin"] = "*";
1350
+ }
1351
+ return new Response(new Uint8Array(content), { headers });
1352
+ } catch {
1353
+ }
1354
+ }
1355
+ return null;
1356
+ }
1357
+ async function initializeServicesFromModules(plugins, label) {
1358
+ const logger = getLogger();
1359
+ for (const plugin of plugins) {
1360
+ try {
1361
+ if (plugin.init?._serverInit) {
1362
+ logger.info(`[${label}] Running plugin init: ${plugin.name}`);
1363
+ const data = await plugin.init._serverInit();
1364
+ if (data) setClientInitData(plugin.name, data);
1365
+ }
1366
+ } catch (err) {
1367
+ logger.warn(`[${label}] Plugin init failed: ${plugin.name}: ${err.message}`);
1368
+ }
1369
+ }
1370
+ }
1371
+ async function initializeServices(buildDir, projectRoot, label) {
1372
+ const logger = getLogger();
1373
+ const { discoverPluginsWithInit, sortPluginsByDependencies } = await import("@jay-framework/stack-server-runtime");
1374
+ try {
1375
+ const pluginsWithInit = sortPluginsByDependencies(
1376
+ await discoverPluginsWithInit({ projectRoot })
1377
+ );
1378
+ for (const pluginInit of pluginsWithInit) {
1379
+ try {
1380
+ let modulePath;
1381
+ if (pluginInit.isLocal) {
1382
+ const pluginDirName = path.basename(pluginInit.pluginPath);
1383
+ const initModule = pluginInit.initModule || "index";
1384
+ modulePath = path.join(
1385
+ buildDir,
1386
+ "server",
1387
+ "plugins",
1388
+ pluginDirName,
1389
+ initModule + ".js"
1390
+ );
1391
+ } else {
1392
+ modulePath = pluginInit.packageName;
1393
+ }
1394
+ const pluginModule = await import(modulePath);
1395
+ const init = pluginModule.init || pluginModule[pluginInit.initExport || "init"];
1396
+ if (init?._serverInit) {
1397
+ logger.info(`[${label}] Running plugin init: ${pluginInit.name}`);
1398
+ const data = await init._serverInit();
1399
+ if (data) setClientInitData(pluginInit.name, data);
1400
+ }
1401
+ } catch (err) {
1402
+ logger.warn(`[${label}] Plugin init failed: ${pluginInit.name}: ${err.message}`);
1403
+ }
1404
+ }
1405
+ } catch {
1406
+ }
1407
+ const initModulePath = path.join(buildDir, "server", "init.js");
1408
+ try {
1409
+ const initModule = await import(initModulePath);
1410
+ const init = initModule.init || initModule.default;
1411
+ if (init?._serverInit) {
1412
+ logger.info(`[${label}] Running server init...`);
1413
+ const data = await init._serverInit();
1414
+ if (data) setClientInitData("project", data);
1415
+ }
1416
+ } catch {
1417
+ }
1418
+ }
1419
+ export {
1420
+ CacheNotFoundError as C,
1421
+ DEFAULT_LIST_TTL_MS as D,
1422
+ FilesystemArtifactStore as F,
1423
+ InstanceRenderer as I,
1424
+ SwrCache as S,
1425
+ fetchPageRequest as a,
1426
+ fetchStaticFile as b,
1427
+ canonicalParams as c,
1428
+ deriveCachePath as d,
1429
+ initializeServicesFromModules as e,
1430
+ fetchActionRequest as f,
1431
+ instanceKey as g,
1432
+ isActionRequest as h,
1433
+ initializeServices as i,
1434
+ registerActionsFromModules as j,
1435
+ resolveRequest as k,
1436
+ routeInstanceDir as l,
1437
+ matchRoutes as m,
1438
+ synthesizeInstance as n,
1439
+ buildUrlFromManifest as o,
1440
+ parseCacheEntry as p,
1441
+ renderErrorPage as q,
1442
+ registerActionsFromManifest as r,
1443
+ serializeCacheEntry as s,
1444
+ toInstanceRef as t,
1445
+ renderInstance as u,
1446
+ generateSitemap as v,
1447
+ loadPagePartsFromConfig as w
1448
+ };