akanjs 3.0.0-beta.25 → 3.0.0-beta.26
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/common/hmrPhase.ts +2 -0
- package/package.json +1 -1
- package/server/artifact/devBuildRecovery.ts +12 -0
- package/server/artifact/index.ts +1 -0
- package/server/artifact/ipcTypes.ts +2 -0
- package/server/di/diLifecycle.ts +1 -1
- package/server/hmr/clientScript.ts +18 -5
- package/server/hmr/csrDevRuntime.ts +11 -7
- package/server/hmr/csrDevShell.ts +26 -5
- package/server/hmr/devArtifactPruner.ts +77 -0
- package/server/hmr/devHmrController.ts +69 -44
- package/server/hmr/ssrDevShim.ts +7 -1
- package/server/rscWorker.tsx +7 -1
- package/server/rscWorkerHost.ts +126 -40
- package/server/webRouter.ts +25 -19
- package/types/common/hmrPhase.d.ts +2 -0
- package/types/server/artifact/devBuildRecovery.d.ts +4 -0
- package/types/server/artifact/index.d.ts +1 -0
- package/types/server/artifact/ipcTypes.d.ts +2 -0
- package/types/server/hmr/devArtifactPruner.d.ts +9 -0
- package/types/server/hmr/devHmrController.d.ts +0 -2
- package/types/server/rscWorkerHost.d.ts +5 -4
- package/types/server/webRouter.d.ts +4 -2
package/common/hmrPhase.ts
CHANGED
|
@@ -5,6 +5,7 @@ declare global {
|
|
|
5
5
|
var __AKAN_HMR_PHASE__: AkanHmrPhase | undefined;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
/** @deprecated Always `null`: the dev client that set the phase applies modules through its registry now. */
|
|
8
9
|
export function getAkanHmrPhase(): AkanHmrPhase {
|
|
9
10
|
if (typeof globalThis === "undefined") return null;
|
|
10
11
|
const phase = globalThis.__AKAN_HMR_PHASE__;
|
|
@@ -12,6 +13,7 @@ export function getAkanHmrPhase(): AkanHmrPhase {
|
|
|
12
13
|
return null;
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
/** @deprecated Always `false`, for the reason `getAkanHmrPhase` is always `null`. */
|
|
15
17
|
export function isAkanHmrApplying(): boolean {
|
|
16
18
|
return getAkanHmrPhase() !== null;
|
|
17
19
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { DevBuildStatus } from "./ipcTypes";
|
|
2
|
+
|
|
3
|
+
export class DevBuildRecovery {
|
|
4
|
+
|
|
5
|
+
static recovers(previous: DevBuildStatus | undefined, status: DevBuildStatus): boolean {
|
|
6
|
+
if (!previous || previous.ok || !status.ok) return false;
|
|
7
|
+
if (status.phase === "route" && status.scope !== undefined && previous.scope !== undefined)
|
|
8
|
+
return status.scope === previous.scope && status.generation >= previous.generation;
|
|
9
|
+
if (status.generation !== previous.generation) return status.generation > previous.generation;
|
|
10
|
+
return status.phase === "backend";
|
|
11
|
+
}
|
|
12
|
+
}
|
package/server/artifact/index.ts
CHANGED
package/server/di/diLifecycle.ts
CHANGED
|
@@ -118,7 +118,7 @@ export class DiLifecycle {
|
|
|
118
118
|
constructor({ env, modules = [], disableModules = [], disableLibs = [] }: DiLifecycleProps, ...libs: AkanLib[]) {
|
|
119
119
|
this.#env = env;
|
|
120
120
|
const databaseMode = DiLifecycle.#databaseMode();
|
|
121
|
-
this.logger.
|
|
121
|
+
this.logger.debug(`Database mode: ${DatabaseModes.describe(databaseMode)}`);
|
|
122
122
|
|
|
123
123
|
this.#predefinedAdaptor = { ...getPredefinedAdaptor(databaseMode) };
|
|
124
124
|
this.#libs = libs;
|
|
@@ -52,12 +52,12 @@ export const HMR_CLIENT_SCRIPT = `(function(){
|
|
|
52
52
|
function connect(){
|
|
53
53
|
try { socket = new WebSocket(url); }
|
|
54
54
|
catch(e){ console.error("[akan-hmr] ws init failed", e); schedule(); return; }
|
|
55
|
-
socket.addEventListener("open", function(){ attempts = 0; });
|
|
56
55
|
socket.addEventListener("message", function(ev){
|
|
57
56
|
var msg;
|
|
58
57
|
try { msg = JSON.parse(ev.data); } catch (e){ return; }
|
|
59
58
|
if (!msg || typeof msg.type !== "string") return;
|
|
60
59
|
if (msg.type === "hello") {
|
|
60
|
+
attempts = 0;
|
|
61
61
|
var dropped = Array.isArray(msg.failingPhases) && dropBuildErrorsExcept(msg.failingPhases);
|
|
62
62
|
if (dropped && systemPage) {
|
|
63
63
|
reloadForUpdate("the build that failed to render this page recovered");
|
|
@@ -387,7 +387,7 @@ export const HMR_CLIENT_SCRIPT = `(function(){
|
|
|
387
387
|
var current = buildErrorStates[phase];
|
|
388
388
|
if (!current) return;
|
|
389
389
|
var generation = typeof msg.generation === "number" ? msg.generation : 0;
|
|
390
|
-
var sameGeneration = phase === "backend" || phase === "
|
|
390
|
+
var sameGeneration = phase === "backend" || phase === "route";
|
|
391
391
|
var recovered = sameGeneration ? generation >= current.generation : generation > current.generation;
|
|
392
392
|
if (!recovered) return;
|
|
393
393
|
delete buildErrorStates[phase];
|
|
@@ -405,6 +405,12 @@ export const HMR_CLIENT_SCRIPT = `(function(){
|
|
|
405
405
|
return dropped;
|
|
406
406
|
}
|
|
407
407
|
|
|
408
|
+
function clearRenderError(){
|
|
409
|
+
if (!buildErrorStates.build) return;
|
|
410
|
+
delete buildErrorStates.build;
|
|
411
|
+
showBuildRecovered({});
|
|
412
|
+
}
|
|
413
|
+
|
|
408
414
|
function showBuildRecovered(msg){
|
|
409
415
|
if (hasBuildErrors()) {
|
|
410
416
|
renderBuildErrorOverlay();
|
|
@@ -462,9 +468,12 @@ export const HMR_CLIENT_SCRIPT = `(function(){
|
|
|
462
468
|
return prefix ? prefix + "\\n" + msg.message : msg.message;
|
|
463
469
|
}
|
|
464
470
|
|
|
471
|
+
var rscRefreshSeq = 0;
|
|
472
|
+
var rscRefreshedSeq = 0;
|
|
465
473
|
function refreshRsc(msg){
|
|
466
474
|
var receivedAt = Date.now();
|
|
467
|
-
var
|
|
475
|
+
var seq = ++rscRefreshSeq;
|
|
476
|
+
var refresh = function(){ if (seq === rscRefreshSeq) doRefreshRsc(msg, receivedAt, seq); };
|
|
468
477
|
var settle = function(){
|
|
469
478
|
return self.__akan && typeof self.__akan.whenSettled === "function" ? self.__akan.whenSettled() : null;
|
|
470
479
|
};
|
|
@@ -477,7 +486,7 @@ export const HMR_CLIENT_SCRIPT = `(function(){
|
|
|
477
486
|
settled.then(refresh, refresh);
|
|
478
487
|
}
|
|
479
488
|
|
|
480
|
-
function doRefreshRsc(msg, receivedAt){
|
|
489
|
+
function doRefreshRsc(msg, receivedAt, seq){
|
|
481
490
|
var started = performance.now();
|
|
482
491
|
var overlayToken = beginHmrOverlay("Refreshing page...");
|
|
483
492
|
try { self.__AKAN_RSC_CLEAR_CACHE__ && self.__AKAN_RSC_CLEAR_CACHE__(); } catch(e){}
|
|
@@ -488,9 +497,13 @@ export const HMR_CLIENT_SCRIPT = `(function(){
|
|
|
488
497
|
return;
|
|
489
498
|
}
|
|
490
499
|
Promise.resolve(self.__AKAN_RSC_REFRESH__({ buildId: msg.buildId })).then(function(){
|
|
491
|
-
|
|
500
|
+
if (!(seq < rscRefreshedSeq)) {
|
|
501
|
+
rscRefreshedSeq = seq;
|
|
502
|
+
lastBuildId = msg.buildId;
|
|
503
|
+
}
|
|
492
504
|
recordTrace("rsc-refresh", msg, receivedAt, Date.now());
|
|
493
505
|
endHmrOverlay(overlayToken);
|
|
506
|
+
clearRenderError();
|
|
494
507
|
console.debug && console.debug("[akan-hmr] RSC refreshed", {
|
|
495
508
|
buildId: msg.buildId,
|
|
496
509
|
generation: msg.generation,
|
|
@@ -207,21 +207,25 @@ export const installCsrDevRuntime = (host: CsrDevRuntimeHost): void => {
|
|
|
207
207
|
const entry = script?.dataset.akanCsrEntry;
|
|
208
208
|
if (!entry) throw new Error("[akan-csr] the app script carries no data-akan-csr-entry");
|
|
209
209
|
this.#generation = generation;
|
|
210
|
-
this.#
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
210
|
+
this.#run(entry, () => {
|
|
211
|
+
const runtime = this.#load(refresh).exports as RefreshRuntime;
|
|
212
|
+
|
|
213
|
+
runtime.injectIntoGlobalHook(host);
|
|
214
|
+
this.#refresh = runtime;
|
|
215
|
+
});
|
|
214
216
|
}
|
|
215
217
|
|
|
216
218
|
startLibrary({ generation, refresh, bootstrap, vendorFile, epoch }: LibraryStart) {
|
|
217
219
|
this.#library = { vendorFile, epoch };
|
|
218
220
|
this.#generation = generation;
|
|
219
|
-
this.#
|
|
220
|
-
|
|
221
|
+
this.#run(bootstrap, () => {
|
|
222
|
+
this.#refresh = this.#load(refresh).exports as RefreshRuntime;
|
|
223
|
+
});
|
|
221
224
|
}
|
|
222
225
|
|
|
223
|
-
#run(entry: string) {
|
|
226
|
+
#run(entry: string, loadRefresh: () => void) {
|
|
224
227
|
try {
|
|
228
|
+
loadRefresh();
|
|
225
229
|
this.#load(entry);
|
|
226
230
|
} catch (error) {
|
|
227
231
|
this.#startFailed = true;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import { resolveStaticPath } from "../staticPath";
|
|
3
4
|
import {
|
|
@@ -38,6 +39,8 @@ export class CsrDevShell {
|
|
|
38
39
|
readonly #appWaitMs: number;
|
|
39
40
|
readonly #appPollMs: number;
|
|
40
41
|
readonly #bootWaitMs: number;
|
|
42
|
+
|
|
43
|
+
readonly #sourceMaps = new Map<string, { stamp: string; body: string }>();
|
|
41
44
|
|
|
42
45
|
constructor(
|
|
43
46
|
artifactDir: string,
|
|
@@ -109,7 +112,8 @@ ${stylesheet} </head>
|
|
|
109
112
|
const deadline = Date.now() + this.#appWaitMs;
|
|
110
113
|
for (;;) {
|
|
111
114
|
const manifest = await this.readManifest();
|
|
112
|
-
if (!manifest ||
|
|
115
|
+
if (!manifest || manifest.generation < generation || appGenerationOf(manifest) >= generation) return;
|
|
116
|
+
if (Date.now() >= deadline || signal?.aborted) return;
|
|
113
117
|
await Bun.sleep(this.#appPollMs);
|
|
114
118
|
}
|
|
115
119
|
}
|
|
@@ -135,9 +139,26 @@ ${stylesheet} </head>
|
|
|
135
139
|
|
|
136
140
|
async #serveSourceMap(name: string): Promise<Response> {
|
|
137
141
|
const generatedFile = name.slice(0, -".map".length);
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
142
|
+
const layoutPath = path.join(this.#dir, `${generatedFile}.layout.json`);
|
|
143
|
+
const stat = await fs.stat(layoutPath).catch(() => null);
|
|
144
|
+
if (!stat) return new Response("Not Found", { status: 404 });
|
|
145
|
+
const stamp = `${stat.ino}:${stat.size}:${stat.mtimeMs}`;
|
|
146
|
+
const cached = this.#sourceMaps.get(generatedFile);
|
|
147
|
+
const body = cached?.stamp === stamp ? cached.body : await this.#mergeSourceMap(generatedFile, layoutPath);
|
|
148
|
+
if (!body) return new Response("Not Found", { status: 404 });
|
|
149
|
+
this.#sourceMaps.delete(generatedFile);
|
|
150
|
+
this.#sourceMaps.set(generatedFile, { stamp, body });
|
|
151
|
+
for (const oldest of this.#sourceMaps.keys()) {
|
|
152
|
+
if (this.#sourceMaps.size <= 4) break;
|
|
153
|
+
this.#sourceMaps.delete(oldest);
|
|
154
|
+
}
|
|
155
|
+
return new Response(body, { headers: CsrDevShell.#headers("application/json", "no-store") });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async #mergeSourceMap(generatedFile: string, layoutPath: string): Promise<string | null> {
|
|
159
|
+
const layoutText = await fs.readFile(layoutPath, "utf8").catch(() => null);
|
|
160
|
+
if (!layoutText) return null;
|
|
161
|
+
const layout = JSON.parse(layoutText) as CsrDevLayout;
|
|
141
162
|
const sections = await Promise.all(
|
|
142
163
|
layout.modules.map(async ([id, line]) => {
|
|
143
164
|
const file = Bun.file(path.join(this.#dir, csrDevModuleFile(id, ".js.map")));
|
|
@@ -149,7 +170,7 @@ ${stylesheet} </head>
|
|
|
149
170
|
sections.filter((section) => section !== null),
|
|
150
171
|
layout.lineCount,
|
|
151
172
|
);
|
|
152
|
-
return
|
|
173
|
+
return JSON.stringify(map);
|
|
153
174
|
}
|
|
154
175
|
|
|
155
176
|
static #js(body: string | Blob, cacheControl: string): Response {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { Logger } from "akanjs/common";
|
|
4
|
+
import type { CssAsset } from "../types";
|
|
5
|
+
|
|
6
|
+
export class DevArtifactPruner {
|
|
7
|
+
|
|
8
|
+
static readonly #defaultGraceMs = 60_000;
|
|
9
|
+
static readonly #pagesFile = /^pages-[^/\\]+\.js$/;
|
|
10
|
+
readonly #logger = new Logger("DevArtifactPruner");
|
|
11
|
+
readonly #artifactDir: string;
|
|
12
|
+
readonly #graceMs: number;
|
|
13
|
+
#pruning: Promise<void> = Promise.resolve();
|
|
14
|
+
|
|
15
|
+
constructor(artifactDir: string, { graceMs = DevArtifactPruner.#defaultGraceMs }: { graceMs?: number } = {}) {
|
|
16
|
+
this.#artifactDir = path.resolve(artifactDir);
|
|
17
|
+
this.#graceMs = graceMs;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
prunePages(runningPath: string): Promise<void> {
|
|
21
|
+
return this.#queue(async () => {
|
|
22
|
+
const running = await fs.stat(runningPath).catch(() => null);
|
|
23
|
+
if (!running) return;
|
|
24
|
+
const keep = new Set([path.resolve(runningPath), ...(await this.#baseArtifact()).pages]);
|
|
25
|
+
const serverDir = path.join(this.#artifactDir, "server");
|
|
26
|
+
const before = Math.min(running.mtimeMs, Date.now() - this.#graceMs);
|
|
27
|
+
await this.#removeOlder(serverDir, (name) => DevArtifactPruner.#pagesFile.test(name), keep, before);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
pruneStyles(cssAssets: Record<string, CssAsset>): Promise<void> {
|
|
32
|
+
return this.#queue(async () => {
|
|
33
|
+
const current = Object.values(cssAssets).map((asset) => path.join(this.#artifactDir, asset.cssRelPath));
|
|
34
|
+
const keep = new Set([...current, ...(await this.#baseArtifact()).styles]);
|
|
35
|
+
const stylesDir = path.join(this.#artifactDir, "styles");
|
|
36
|
+
await this.#removeOlder(stylesDir, (name) => name.endsWith(".css"), keep, Date.now() - this.#graceMs);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#queue(prune: () => Promise<void>): Promise<void> {
|
|
41
|
+
this.#pruning = this.#pruning.then(prune).catch((error: unknown) => {
|
|
42
|
+
this.#logger.verbose(`[dev-artifact] prune skipped: ${String(error)}`);
|
|
43
|
+
});
|
|
44
|
+
return this.#pruning;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async #removeOlder(dir: string, matches: (name: string) => boolean, keep: Set<string>, before: number) {
|
|
48
|
+
const names = await fs.readdir(dir).catch(() => [] as string[]);
|
|
49
|
+
let removed = 0;
|
|
50
|
+
for (const name of names) {
|
|
51
|
+
const file = path.join(dir, name);
|
|
52
|
+
if (!matches(name) || keep.has(file)) continue;
|
|
53
|
+
const stat = await fs.stat(file).catch(() => null);
|
|
54
|
+
if (!stat || stat.mtimeMs >= before) continue;
|
|
55
|
+
|
|
56
|
+
if (
|
|
57
|
+
await fs.rm(file, { force: true }).then(
|
|
58
|
+
() => true,
|
|
59
|
+
() => false,
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
removed += 1;
|
|
63
|
+
}
|
|
64
|
+
if (removed > 0) this.#logger.verbose(`[dev-artifact] removed ${removed} superseded file(s) from ${dir}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async #baseArtifact(): Promise<{ pages: string[]; styles: string[] }> {
|
|
68
|
+
const text = await fs.readFile(path.join(this.#artifactDir, "base-artifact.json"), "utf8").catch(() => null);
|
|
69
|
+
const artifact = text
|
|
70
|
+
? (JSON.parse(text) as { pagesBundlePath?: string; cssAssets?: Record<string, CssAsset> })
|
|
71
|
+
: null;
|
|
72
|
+
return {
|
|
73
|
+
pages: artifact?.pagesBundlePath ? [path.resolve(this.#artifactDir, artifact.pagesBundlePath)] : [],
|
|
74
|
+
styles: Object.values(artifact?.cssAssets ?? {}).map((asset) => path.join(this.#artifactDir, asset.cssRelPath)),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { Logger } from "akanjs/common";
|
|
4
|
+
import { CrossSiteGuard } from "../../signal/CrossSiteGuard";
|
|
4
5
|
import {
|
|
5
6
|
BuilderRpc,
|
|
6
7
|
type ClientManifest,
|
|
8
|
+
DevBuildRecovery,
|
|
7
9
|
type DevBuildStatus,
|
|
8
10
|
type HmrTrace,
|
|
9
11
|
RouteClientCache,
|
|
@@ -13,6 +15,7 @@ import {
|
|
|
13
15
|
import type { RscReloadFailure, RscWorker } from "../rscWorkerHost";
|
|
14
16
|
import type { RenderState } from "../types";
|
|
15
17
|
import { CSR_DEV_DIRNAME, CSR_DEV_MANIFEST_FILE, type CsrDevManifest, SSR_DEV_DIRNAME } from "./csrDevManifest";
|
|
18
|
+
import { DevArtifactPruner } from "./devArtifactPruner";
|
|
16
19
|
import { isAkanRuntimeMetadataFile } from "./runtimeMetadataFile";
|
|
17
20
|
import { type SsrUpdateMessage, SsrUpdateQueue } from "./ssrUpdateQueue";
|
|
18
21
|
import { type ChangeKind, type HmrMessage, type HmrWsData, HmrWsHub } from "./wsHub";
|
|
@@ -49,11 +52,7 @@ export function devBuildStatusToHmrMessage(
|
|
|
49
52
|
files: status.files.length,
|
|
50
53
|
};
|
|
51
54
|
}
|
|
52
|
-
if (!previous
|
|
53
|
-
|
|
54
|
-
const sameGeneration = status.phase === "backend" || status.phase === "scan" || status.phase === "route";
|
|
55
|
-
const recovered = sameGeneration ? status.generation >= previous.generation : status.generation > previous.generation;
|
|
56
|
-
if (!recovered) return null;
|
|
55
|
+
if (!DevBuildRecovery.recovers(previous, status)) return null;
|
|
57
56
|
return {
|
|
58
57
|
type: "build-status",
|
|
59
58
|
status: "ok",
|
|
@@ -85,6 +84,8 @@ export class DevHmrController {
|
|
|
85
84
|
readonly #ssrUpdates = new SsrUpdateQueue((message) => this.#sendSsrUpdate(message));
|
|
86
85
|
readonly #builderRpc: BuilderRpc;
|
|
87
86
|
readonly routeCache: RouteClientCache;
|
|
87
|
+
#recheckingRoute = false;
|
|
88
|
+
#recheckAgain: DevBuildStatus | null = null;
|
|
88
89
|
readonly #recentClientEntries = new Set<string>();
|
|
89
90
|
/** File to the newest save that invalidated its routes already: that save's pages build must not drop them again. */
|
|
90
91
|
readonly #earlyInvalidated = new Map<string, number>();
|
|
@@ -97,10 +98,12 @@ export class DevHmrController {
|
|
|
97
98
|
readonly #buildStatusByPhase = new Map<DevBuildStatus["phase"], DevBuildStatus>();
|
|
98
99
|
#graphSeeds: string[];
|
|
99
100
|
#runningBundlePath: string | null;
|
|
101
|
+
readonly #pruner: DevArtifactPruner;
|
|
100
102
|
|
|
101
103
|
constructor({ artifactDir, renderState, rsc, seedIndex, upgradeHmrWs, pagesBundlePath }: DevHmrControllerOptions) {
|
|
102
104
|
this.#artifactDir = artifactDir;
|
|
103
105
|
this.#runningBundlePath = pagesBundlePath ? path.resolve(pagesBundlePath) : null;
|
|
106
|
+
this.#pruner = new DevArtifactPruner(artifactDir);
|
|
104
107
|
this.#renderState = renderState;
|
|
105
108
|
this.#rsc = rsc;
|
|
106
109
|
this.#seedIndex = seedIndex;
|
|
@@ -155,28 +158,17 @@ export class DevHmrController {
|
|
|
155
158
|
}
|
|
156
159
|
|
|
157
160
|
handleWs(req: Request): Response | undefined {
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
CrossSiteGuard.assertOrigin(req, new URL(req.url), "hmr");
|
|
164
|
+
} catch {
|
|
165
|
+
return new Response("Forbidden", { status: 403 });
|
|
166
|
+
}
|
|
158
167
|
const client = new URL(req.url).searchParams.get("client") === "csr" ? "csr" : "ssr";
|
|
159
168
|
if (this.#upgradeHmrWs(req, { kind: "akan-hmr", openedAt: Date.now(), client })) return;
|
|
160
169
|
return new Response("Failed to upgrade HMR WebSocket", { status: 500 });
|
|
161
170
|
}
|
|
162
171
|
|
|
163
|
-
async ensureRoute(url: URL) {
|
|
164
|
-
const started = Date.now();
|
|
165
|
-
const matched =
|
|
166
|
-
RouteSeedIndexStore.match(url.pathname, this.#seedIndex.entries) ??
|
|
167
|
-
RouteSeedIndexStore.matchPrefix(url.pathname, this.#seedIndex.entries);
|
|
168
|
-
if (matched) await this.routeCache.ensure(matched.entry.routeId, matched.entry.seeds);
|
|
169
|
-
this.#logger.verbose(
|
|
170
|
-
`[route-cache] ensure pathname=${url.pathname} routeId=${matched?.entry.routeId ?? "(none)"} in ${Date.now() - started}ms`,
|
|
171
|
-
);
|
|
172
|
-
return this.routeCache.snapshot();
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
routeIdsForPath(pathname: string): string[] | undefined {
|
|
176
|
-
const matched = RouteSeedIndexStore.match(pathname, this.#seedIndex.entries);
|
|
177
|
-
return matched ? [matched.entry.routeId] : undefined;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
172
|
#sendSsrUpdate(message: SsrUpdateMessage): void {
|
|
181
173
|
this.#renderState.ssrGeneration = message.generation;
|
|
182
174
|
this.#hub.broadcast({ ...message, trace: DevHmrController.#broadcastTrace(message.trace) });
|
|
@@ -235,6 +227,7 @@ export class DevHmrController {
|
|
|
235
227
|
this.#renderState.cssBytesByUrl = cssBytesByUrl;
|
|
236
228
|
this.#rsc.updateCssAssets(this.#renderState.cssAssets);
|
|
237
229
|
this.#hub.broadcast({ type: "css-update", cssAssets: this.#renderState.cssAssets });
|
|
230
|
+
void this.#pruner.pruneStyles(this.#renderState.cssAssets);
|
|
238
231
|
this.#logger.verbose(
|
|
239
232
|
`css-update assets=${Object.keys(this.#renderState.cssAssets).length} generation=${css.generation ?? "(unknown)"} files=${css.changedFiles?.length ?? 0} in ${Date.now() - started}ms (ipc)`,
|
|
240
233
|
);
|
|
@@ -266,6 +259,7 @@ export class DevHmrController {
|
|
|
266
259
|
const manifest = this.routeCache.snapshot();
|
|
267
260
|
|
|
268
261
|
const running = bundlePath ? path.resolve(bundlePath) : null;
|
|
262
|
+
let tabBuildId = buildId;
|
|
269
263
|
if (serverTouched === false && !clearAll && running === this.#runningBundlePath) {
|
|
270
264
|
this.#logger.verbose(
|
|
271
265
|
`[SSR] pages bundle unchanged for the server; buildId ${this.#renderState.buildId} kept`,
|
|
@@ -275,25 +269,29 @@ export class DevHmrController {
|
|
|
275
269
|
this.#renderState.buildId = buildId;
|
|
276
270
|
const reloadStarted = Date.now();
|
|
277
271
|
try {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
272
|
+
const adopted = await this.#rsc.reload({
|
|
273
|
+
clientManifest: manifest.clientManifest,
|
|
274
|
+
cssAssets: this.#renderState.cssAssets,
|
|
275
|
+
buildId,
|
|
276
|
+
pagesBundlePath: bundlePath,
|
|
277
|
+
});
|
|
278
|
+
this.#adoptBundle(adopted.pagesBundlePath);
|
|
279
|
+
|
|
280
|
+
tabBuildId = adopted.buildId;
|
|
286
281
|
} catch (error) {
|
|
287
282
|
const failure = DevHmrController.#reloadFailure(error);
|
|
288
283
|
|
|
289
284
|
if (failure) {
|
|
290
285
|
this.#renderState.buildId = failure.adopted.buildId;
|
|
291
|
-
this.#
|
|
286
|
+
this.#adoptBundle(failure.adopted.pagesBundlePath);
|
|
292
287
|
} else if (this.#renderState.buildId === buildId) this.#renderState.buildId = previousBuildId;
|
|
293
288
|
|
|
294
|
-
if (failure?.
|
|
295
|
-
|
|
296
|
-
|
|
289
|
+
if (failure?.adopted.buildId !== buildId) {
|
|
290
|
+
|
|
291
|
+
if (failure && failure.failed.buildId !== buildId) this.#ssrUpdates.release(generation);
|
|
292
|
+
else this.#failPagesReload(generation, files, error);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
297
295
|
}
|
|
298
296
|
this.#logger.verbose(`[SSR] rsc reload buildId=${buildId} in ${Date.now() - reloadStarted}ms`);
|
|
299
297
|
}
|
|
@@ -301,7 +299,7 @@ export class DevHmrController {
|
|
|
301
299
|
const broadcastTrace = DevHmrController.#broadcastTrace(trace);
|
|
302
300
|
if (shouldReload) this.#ssrUpdates.clear(generation);
|
|
303
301
|
const released = shouldReload ? { released: 0, reload: false } : this.#ssrUpdates.release(generation);
|
|
304
|
-
if (shouldReload) this.#hub.broadcast({ type: "reload", buildId });
|
|
302
|
+
if (shouldReload) this.#hub.broadcast({ type: "reload", buildId: tabBuildId });
|
|
305
303
|
else if (released.reload)
|
|
306
304
|
this.#logger.verbose(`[hmr] generation=${generation} released a registry reload; no RSC refresh needed`);
|
|
307
305
|
else if (serverTouched === false)
|
|
@@ -311,7 +309,7 @@ export class DevHmrController {
|
|
|
311
309
|
else
|
|
312
310
|
this.#hub.broadcast({
|
|
313
311
|
type: "rsc-refresh",
|
|
314
|
-
buildId,
|
|
312
|
+
buildId: tabBuildId,
|
|
315
313
|
generation,
|
|
316
314
|
changedFiles,
|
|
317
315
|
routeIds,
|
|
@@ -324,6 +322,13 @@ export class DevHmrController {
|
|
|
324
322
|
});
|
|
325
323
|
}
|
|
326
324
|
|
|
325
|
+
#adoptBundle(bundlePath: string): void {
|
|
326
|
+
const running = path.resolve(bundlePath);
|
|
327
|
+
if (running === this.#runningBundlePath) return;
|
|
328
|
+
this.#runningBundlePath = running;
|
|
329
|
+
void this.#pruner.prunePages(running);
|
|
330
|
+
}
|
|
331
|
+
|
|
327
332
|
static #reloadFailure(error: unknown): RscReloadFailure | null {
|
|
328
333
|
return error instanceof Error && "adopted" in error ? (error as RscReloadFailure) : null;
|
|
329
334
|
}
|
|
@@ -351,6 +356,7 @@ export class DevHmrController {
|
|
|
351
356
|
}
|
|
352
357
|
|
|
353
358
|
#recordBuildStatus(status: DevBuildStatus): void {
|
|
359
|
+
this.#recheckFailedRoute(status);
|
|
354
360
|
const previous = this.#buildStatusByPhase.get(status.phase);
|
|
355
361
|
const message = devBuildStatusToHmrMessage(status, previous);
|
|
356
362
|
|
|
@@ -363,6 +369,26 @@ export class DevHmrController {
|
|
|
363
369
|
);
|
|
364
370
|
}
|
|
365
371
|
|
|
372
|
+
#recheckFailedRoute(status: DevBuildStatus, { again = false }: { again?: boolean } = {}): void {
|
|
373
|
+
if (!status.ok || (status.phase !== "pages" && status.phase !== "ssr")) return;
|
|
374
|
+
if (this.#recheckingRoute) {
|
|
375
|
+
this.#recheckAgain = status;
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const failed = this.#buildStatusByPhase.get("route");
|
|
379
|
+
if (!failed || failed.ok || !failed.scope || (!again && failed.generation >= status.generation)) return;
|
|
380
|
+
this.#recheckingRoute = true;
|
|
381
|
+
void this.routeCache
|
|
382
|
+
.ensure(failed.scope, failed.files)
|
|
383
|
+
.catch(() => undefined)
|
|
384
|
+
.finally(() => {
|
|
385
|
+
this.#recheckingRoute = false;
|
|
386
|
+
const next = this.#recheckAgain;
|
|
387
|
+
this.#recheckAgain = null;
|
|
388
|
+
if (next) this.#recheckFailedRoute(next, { again: true });
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
366
392
|
#createRouteCache() {
|
|
367
393
|
return new RouteClientCache({
|
|
368
394
|
buildRoute: async (routeId, { seeds, knownEntries, generation }) => {
|
|
@@ -387,17 +413,16 @@ export class DevHmrController {
|
|
|
387
413
|
if (delta.newEntries.length === 0 && removedEntries.size === 0) return;
|
|
388
414
|
this.#rememberClientDeps(routeId, delta.clientDeps, delta.clientDepsByEntry);
|
|
389
415
|
try {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
);
|
|
416
|
+
const adopted = await this.#rsc.reload({
|
|
417
|
+
clientManifest: nextMerged.clientManifest,
|
|
418
|
+
cssAssets: this.#renderState.cssAssets,
|
|
419
|
+
buildId: this.#renderState.buildId,
|
|
420
|
+
});
|
|
421
|
+
this.#adoptBundle(adopted.pagesBundlePath);
|
|
397
422
|
} catch (error) {
|
|
398
423
|
|
|
399
424
|
const failure = DevHmrController.#reloadFailure(error);
|
|
400
|
-
if (failure) this.#
|
|
425
|
+
if (failure) this.#adoptBundle(failure.adopted.pagesBundlePath);
|
|
401
426
|
this.#logger.warn(`[SSR] route ${routeId} merged, but the worker did not reload: ${String(error)}`);
|
|
402
427
|
}
|
|
403
428
|
this.#logger.verbose(
|
package/server/hmr/ssrDevShim.ts
CHANGED
|
@@ -52,9 +52,15 @@ export class SsrDevShim {
|
|
|
52
52
|
var hello = self.__AKAN_SSR_HELLO_GENERATION__;
|
|
53
53
|
if (typeof hello === "number") self.__akan.catchUp(hello, c.prefix);
|
|
54
54
|
}
|
|
55
|
+
function runtime(attempt) {
|
|
56
|
+
return script(c.runtime).catch(function (error) {
|
|
57
|
+
if (attempt >= 3) throw error;
|
|
58
|
+
return wait(500).then(state).then(function () { return runtime(attempt + 1); });
|
|
59
|
+
});
|
|
60
|
+
}
|
|
55
61
|
function boot() {
|
|
56
62
|
if (!booting)
|
|
57
|
-
booting = self.__AKAN_SSR_BOOT__ =
|
|
63
|
+
booting = self.__AKAN_SSR_BOOT__ = runtime(0).then(function () {
|
|
58
64
|
var early = self.__AKAN_SSR_EARLY_UPDATES__ || [];
|
|
59
65
|
self.__AKAN_SSR_EARLY_UPDATES__ = null;
|
|
60
66
|
for (var i = 0; i < early.length; i++) self.__akan.hot(early[i]);
|
package/server/rscWorker.tsx
CHANGED
|
@@ -387,7 +387,12 @@ export class RscRenderer {
|
|
|
387
387
|
this.#resultCache.clear();
|
|
388
388
|
this.#patchResultCache.clear();
|
|
389
389
|
this.#logger.verbose(`reload complete buildId=${msg.buildId} in ${Date.now() - startedAt}ms`);
|
|
390
|
-
this.#send({
|
|
390
|
+
this.#send({
|
|
391
|
+
type: "reloaded",
|
|
392
|
+
buildId: msg.buildId,
|
|
393
|
+
reloadId: msg.reloadId,
|
|
394
|
+
pagesBundlePath: nextPagesBundlePath,
|
|
395
|
+
});
|
|
391
396
|
} catch (error) {
|
|
392
397
|
this.#logger.error(
|
|
393
398
|
`reload failed buildId=${msg.buildId}: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`,
|
|
@@ -399,6 +404,7 @@ export class RscRenderer {
|
|
|
399
404
|
requestId: "__reload__",
|
|
400
405
|
buildId: msg.buildId,
|
|
401
406
|
reloadId: msg.reloadId,
|
|
407
|
+
running: { pagesBundlePath: this.#pagesBundlePath, buildId: this.#pagesBundleBuildId },
|
|
402
408
|
message: error instanceof Error ? error.message : String(error),
|
|
403
409
|
});
|
|
404
410
|
}
|
package/server/rscWorkerHost.ts
CHANGED
|
@@ -297,7 +297,7 @@ export function createRscHostRenderStream(input: {
|
|
|
297
297
|
type RscInMsg =
|
|
298
298
|
| { type: "hello" }
|
|
299
299
|
| { type: "ready" }
|
|
300
|
-
| { type: "reloaded"; buildId: number; reloadId?: number }
|
|
300
|
+
| { type: "reloaded"; buildId: number; reloadId?: number; pagesBundlePath?: string }
|
|
301
301
|
| { type: "meta"; requestId: string; theme?: AkanTheme; status?: number; trace?: RscTraceMetadata }
|
|
302
302
|
| { type: "cache-state"; requestId: string; state: RouteCacheRenderState }
|
|
303
303
|
| { type: "chunk"; requestId: string; data: Uint8Array }
|
|
@@ -315,7 +315,14 @@ type RscInMsg =
|
|
|
315
315
|
| { type: "log.records"; records: LogRecord[]; dropped?: number }
|
|
316
316
|
| { type: "page-prompts.result"; requestId: string; result: PagePromptEntry[] }
|
|
317
317
|
| { type: "page-prompt.result"; requestId: string; result: PagePromptRun }
|
|
318
|
-
| {
|
|
318
|
+
| {
|
|
319
|
+
type: "error";
|
|
320
|
+
requestId: string;
|
|
321
|
+
message: string;
|
|
322
|
+
buildId?: number;
|
|
323
|
+
reloadId?: number;
|
|
324
|
+
running?: RscAdoptedBundle;
|
|
325
|
+
};
|
|
319
326
|
|
|
320
327
|
export interface RscWorkerReloadInput {
|
|
321
328
|
clientManifest: ClientManifest;
|
|
@@ -325,20 +332,22 @@ export interface RscWorkerReloadInput {
|
|
|
325
332
|
pagesBundlePath?: string;
|
|
326
333
|
}
|
|
327
334
|
|
|
328
|
-
interface RscAdoptedBundle {
|
|
335
|
+
export interface RscAdoptedBundle {
|
|
329
336
|
pagesBundlePath: string;
|
|
330
337
|
buildId: number;
|
|
331
338
|
}
|
|
332
339
|
|
|
333
340
|
interface RscReloadWaiter {
|
|
334
|
-
resolve: (
|
|
341
|
+
resolve: (adopted: RscAdoptedBundle) => void;
|
|
335
342
|
reject: (err: RscReloadFailure) => void;
|
|
336
343
|
}
|
|
337
344
|
|
|
338
345
|
/** A reload the worker did not take: what it runs instead, and whether a later reload failed in this one's place. */
|
|
339
346
|
export interface RscReloadFailure extends Error {
|
|
347
|
+
/** What the worker runs now. */
|
|
340
348
|
adopted: RscAdoptedBundle;
|
|
341
|
-
|
|
349
|
+
/** The latest state it was asked to take, which a later reload may have moved past the caller's own. */
|
|
350
|
+
failed: RscAdoptedBundle;
|
|
342
351
|
}
|
|
343
352
|
|
|
344
353
|
type WorkerStatus = "starting" | "ready" | "restarting" | "stopped";
|
|
@@ -371,6 +380,9 @@ export class RscWorker {
|
|
|
371
380
|
buildId: number;
|
|
372
381
|
} | null = null;
|
|
373
382
|
#nextReloadId = 1;
|
|
383
|
+
#answeredReloadId = 0;
|
|
384
|
+
|
|
385
|
+
#droppedWhileBooting = false;
|
|
374
386
|
#reloadState = 0;
|
|
375
387
|
#init: RscAdoptedBundle & { reloadState: number } = { pagesBundlePath: "", buildId: 0, reloadState: 0 };
|
|
376
388
|
/** What the worker runs: a respawn falls back to it when the bundle a reload named failed to import. */
|
|
@@ -378,7 +390,7 @@ export class RscWorker {
|
|
|
378
390
|
|
|
379
391
|
#status: WorkerStatus = "starting";
|
|
380
392
|
#killed = false;
|
|
381
|
-
#queuedSends:
|
|
393
|
+
#queuedSends: { requestId: string; send: () => void }[] = [];
|
|
382
394
|
#restartAttempts = 0;
|
|
383
395
|
#restartCount = 0;
|
|
384
396
|
#recycleCount = 0;
|
|
@@ -391,9 +403,11 @@ export class RscWorker {
|
|
|
391
403
|
#hostPendingChunkOverflowCount = 0;
|
|
392
404
|
#restartTimer: ReturnType<typeof setTimeout> | null = null;
|
|
393
405
|
#recycleTimer: ReturnType<typeof setTimeout> | null = null;
|
|
394
|
-
#rollingRecycle: { oldProc: RscProcess; reason: string } | null = null;
|
|
406
|
+
#rollingRecycle: { oldProc: RscProcess; reason: string; reloadsSinceSpawn: number } | null = null;
|
|
407
|
+
#booting: RscProcess | null = null;
|
|
395
408
|
|
|
396
409
|
readonly #failBeforeReady: boolean;
|
|
410
|
+
static readonly #queuedRespawns = 2;
|
|
397
411
|
|
|
398
412
|
constructor(artifact: BaseBuildArtifact, { failBeforeReady = false }: { failBeforeReady?: boolean } = {}) {
|
|
399
413
|
this.#failBeforeReady = failBeforeReady;
|
|
@@ -462,7 +476,7 @@ export class RscWorker {
|
|
|
462
476
|
if (this.#status === "ready") send();
|
|
463
477
|
else if (this.#status === "stopped")
|
|
464
478
|
this.#settleCall(requestId, (call) => call.reject(new Error("rsc worker is stopped")));
|
|
465
|
-
else this.#queuedSends.push(send);
|
|
479
|
+
else this.#queuedSends.push({ requestId, send });
|
|
466
480
|
});
|
|
467
481
|
}
|
|
468
482
|
|
|
@@ -475,7 +489,10 @@ export class RscWorker {
|
|
|
475
489
|
}
|
|
476
490
|
|
|
477
491
|
invalidateRouteResultCache(invalidation?: string | RouteCacheInvalidation): void {
|
|
478
|
-
if (this.#status !== "ready")
|
|
492
|
+
if (this.#status !== "ready") {
|
|
493
|
+
this.#droppedWhileBooting = true;
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
479
496
|
try {
|
|
480
497
|
this.#proc.send(createRscWorkerInvalidateCacheMessage(invalidation));
|
|
481
498
|
} catch (error) {
|
|
@@ -499,6 +516,7 @@ export class RscWorker {
|
|
|
499
516
|
|
|
500
517
|
setLogLevel(minSev: number | null) {
|
|
501
518
|
this.#logLevel = minSev;
|
|
519
|
+
if (this.#status !== "ready") this.#droppedWhileBooting = true;
|
|
502
520
|
this.#sendLogLevel();
|
|
503
521
|
}
|
|
504
522
|
|
|
@@ -527,7 +545,7 @@ export class RscWorker {
|
|
|
527
545
|
if (this.#killed || this.#status === "stopped" || this.#status === "starting" || this.#status === "restarting") {
|
|
528
546
|
return false;
|
|
529
547
|
}
|
|
530
|
-
if (this.#pending.size > 0 || this.#queuedSends.length > 0) {
|
|
548
|
+
if (this.#pending.size > 0 || this.#calls.size > 0 || this.#queuedSends.length > 0) {
|
|
531
549
|
if (!this.#recycleTimer) {
|
|
532
550
|
const graceMs = MemoryLimit.parsePositiveIntEnv("AKAN_RSC_WORKER_RECYCLE_GRACE_MS") ?? 5_000;
|
|
533
551
|
this.#recycleTimer = setTimeout(() => {
|
|
@@ -542,14 +560,17 @@ export class RscWorker {
|
|
|
542
560
|
this.#lastRecycleAtMono = performance.now();
|
|
543
561
|
this.#logger.info(`[rsc] rolling recycle worker reason=${reason} oldPid=${this.#proc.pid}`);
|
|
544
562
|
this.#status = "restarting";
|
|
545
|
-
this.#rollingRecycle = { oldProc: this.#proc, reason };
|
|
563
|
+
this.#rollingRecycle = { oldProc: this.#proc, reason, reloadsSinceSpawn: this.#reloadsSinceSpawn };
|
|
546
564
|
this.#proc = this.#spawn();
|
|
547
565
|
return true;
|
|
548
566
|
}
|
|
549
567
|
|
|
550
568
|
updateCssAssets(cssAssets: Record<string, CssAsset>): void {
|
|
551
569
|
this.#cssAssets = cssAssets;
|
|
552
|
-
if (this.#status !== "ready")
|
|
570
|
+
if (this.#status !== "ready") {
|
|
571
|
+
this.#droppedWhileBooting = true;
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
553
574
|
try {
|
|
554
575
|
this.#proc.send({ type: "updateCssAssets", cssAssets });
|
|
555
576
|
} catch {
|
|
@@ -557,13 +578,14 @@ export class RscWorker {
|
|
|
557
578
|
}
|
|
558
579
|
|
|
559
580
|
/** Resolves with the pages bundle the worker runs once it has taken this reload, or a later one. */
|
|
560
|
-
reload(input: RscWorkerReloadInput): Promise<
|
|
581
|
+
reload(input: RscWorkerReloadInput): Promise<RscAdoptedBundle> {
|
|
582
|
+
if (this.#killed) return Promise.reject(new Error("rsc worker is stopped"));
|
|
561
583
|
this.#reloadState += 1;
|
|
562
584
|
this.#clientManifest = input.clientManifest;
|
|
563
585
|
this.#cssAssets = input.cssAssets ?? this.#cssAssets;
|
|
564
586
|
this.#pagesBundleBuildId = input.buildId;
|
|
565
587
|
if (input.pagesBundlePath) this.#pagesBundlePath = input.pagesBundlePath;
|
|
566
|
-
return new Promise<
|
|
588
|
+
return new Promise<RscAdoptedBundle>((resolve, reject) => {
|
|
567
589
|
|
|
568
590
|
this.#pendingReload = {
|
|
569
591
|
waiters: [...(this.#pendingReload?.waiters ?? []), { resolve, reject }],
|
|
@@ -580,7 +602,9 @@ export class RscWorker {
|
|
|
580
602
|
const pending = this.#pendingReload;
|
|
581
603
|
if (!pending) return;
|
|
582
604
|
|
|
583
|
-
|
|
605
|
+
const answering = this.#answeredReloadId < this.#nextReloadId - 1;
|
|
606
|
+
if (!answering && this.#shouldRecycleForReloadAccumulation() && this.restartWhenIdle("pages-reload-accumulation"))
|
|
607
|
+
return;
|
|
584
608
|
this.#reloadsSinceSpawn += 1;
|
|
585
609
|
pending.reloadId = this.#nextReloadId++;
|
|
586
610
|
try {
|
|
@@ -604,7 +628,7 @@ export class RscWorker {
|
|
|
604
628
|
const pending = this.#pendingReload;
|
|
605
629
|
if (!pending) return;
|
|
606
630
|
this.#pendingReload = null;
|
|
607
|
-
for (const waiter of pending.waiters) waiter.resolve(adopted
|
|
631
|
+
for (const waiter of pending.waiters) waiter.resolve(adopted);
|
|
608
632
|
}
|
|
609
633
|
|
|
610
634
|
#rejectReload(message: string): void {
|
|
@@ -613,10 +637,9 @@ export class RscWorker {
|
|
|
613
637
|
const pending = this.#pendingReload;
|
|
614
638
|
if (!pending) return;
|
|
615
639
|
this.#pendingReload = null;
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
);
|
|
640
|
+
const failed = { pagesBundlePath: pending.pagesBundlePath, buildId: pending.buildId };
|
|
641
|
+
for (const waiter of pending.waiters)
|
|
642
|
+
waiter.reject(Object.assign(new Error(message), { adopted: this.#adopted, failed }));
|
|
620
643
|
}
|
|
621
644
|
|
|
622
645
|
#spawn(): RscProcess {
|
|
@@ -695,6 +718,7 @@ export class RscWorker {
|
|
|
695
718
|
switch (message.type) {
|
|
696
719
|
case "hello":
|
|
697
720
|
|
|
721
|
+
this.#booting = proc;
|
|
698
722
|
this.#init = {
|
|
699
723
|
pagesBundlePath: this.#pagesBundlePath,
|
|
700
724
|
buildId: this.#pagesBundleBuildId,
|
|
@@ -711,7 +735,10 @@ export class RscWorker {
|
|
|
711
735
|
});
|
|
712
736
|
return;
|
|
713
737
|
case "ready":
|
|
738
|
+
this.#booting = null;
|
|
714
739
|
this.#status = "ready";
|
|
740
|
+
this.#answeredReloadId = this.#nextReloadId - 1;
|
|
741
|
+
this.#droppedWhileBooting = false;
|
|
715
742
|
this.#restartAttempts = 0;
|
|
716
743
|
this.#resolveReady();
|
|
717
744
|
this.#finishRollingRecycle();
|
|
@@ -725,6 +752,15 @@ export class RscWorker {
|
|
|
725
752
|
return;
|
|
726
753
|
case "reloaded": {
|
|
727
754
|
const pending = this.#pendingReload;
|
|
755
|
+
|
|
756
|
+
if (proc === this.#proc && message.pagesBundlePath) {
|
|
757
|
+
this.#adopted = { pagesBundlePath: message.pagesBundlePath, buildId: message.buildId };
|
|
758
|
+
this.#answeredReloadId = Math.max(this.#answeredReloadId, message.reloadId ?? 0);
|
|
759
|
+
if (!pending) {
|
|
760
|
+
this.#pagesBundlePath = message.pagesBundlePath;
|
|
761
|
+
this.#pagesBundleBuildId = message.buildId;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
728
764
|
if (pending && pending.reloadId !== null && pending.reloadId === message.reloadId)
|
|
729
765
|
this.#resolveReload({ pagesBundlePath: pending.pagesBundlePath, buildId: pending.buildId });
|
|
730
766
|
return;
|
|
@@ -770,23 +806,14 @@ export class RscWorker {
|
|
|
770
806
|
this.#rejectReload(String(message.message));
|
|
771
807
|
this.#rejectReady(new Error(String(message.message)));
|
|
772
808
|
} else {
|
|
773
|
-
this.#
|
|
774
|
-
|
|
775
|
-
const covered = !this.#pendingReload || this.#pendingReload.reloadState <= this.#init.reloadState;
|
|
776
|
-
if (covered) this.#rejectReload(String(message.message));
|
|
777
|
-
const rolling = this.#rollingRecycle;
|
|
778
|
-
if (covered && rolling && proc === this.#proc) {
|
|
779
|
-
|
|
780
|
-
this.#rollingRecycle = null;
|
|
781
|
-
this.#proc = rolling.oldProc;
|
|
782
|
-
this.#status = "ready";
|
|
783
|
-
this.#flushQueuedSends();
|
|
784
|
-
}
|
|
809
|
+
this.#bootFailed(proc, String(message.message));
|
|
785
810
|
proc.kill();
|
|
786
811
|
}
|
|
787
812
|
return;
|
|
788
813
|
}
|
|
789
814
|
if (message.requestId === "__reload__") {
|
|
815
|
+
if (proc === this.#proc) this.#answeredReloadId = Math.max(this.#answeredReloadId, message.reloadId ?? 0);
|
|
816
|
+
if (proc === this.#proc && message.running) this.#adopted = message.running;
|
|
790
817
|
const pending = this.#pendingReload;
|
|
791
818
|
if (pending && pending.reloadId !== null && pending.reloadId === message.reloadId)
|
|
792
819
|
this.#rejectReload(String(message.message));
|
|
@@ -824,7 +851,7 @@ export class RscWorker {
|
|
|
824
851
|
else if (this.#status === "stopped") {
|
|
825
852
|
this.#resolvePending(requestId, (p) => p.onError("rsc worker is stopped"));
|
|
826
853
|
} else {
|
|
827
|
-
this.#queuedSends.push(send);
|
|
854
|
+
this.#queuedSends.push({ requestId, send });
|
|
828
855
|
}
|
|
829
856
|
}
|
|
830
857
|
|
|
@@ -839,7 +866,7 @@ export class RscWorker {
|
|
|
839
866
|
#flushQueuedSends(): void {
|
|
840
867
|
const queue = this.#queuedSends;
|
|
841
868
|
this.#queuedSends = [];
|
|
842
|
-
for (const send of queue) {
|
|
869
|
+
for (const { send } of queue) {
|
|
843
870
|
try {
|
|
844
871
|
send();
|
|
845
872
|
} catch (err) {
|
|
@@ -848,6 +875,46 @@ export class RscWorker {
|
|
|
848
875
|
}
|
|
849
876
|
}
|
|
850
877
|
|
|
878
|
+
#failRequests(err: Error, which: (requestId: string) => boolean = () => true): void {
|
|
879
|
+
for (const [requestId, pending] of this.#pending) {
|
|
880
|
+
if (!which(requestId)) continue;
|
|
881
|
+
this.#pending.delete(requestId);
|
|
882
|
+
pending.onError(err.message);
|
|
883
|
+
}
|
|
884
|
+
for (const [requestId, call] of this.#calls) {
|
|
885
|
+
if (!which(requestId)) continue;
|
|
886
|
+
this.#calls.delete(requestId);
|
|
887
|
+
call.reject(err);
|
|
888
|
+
}
|
|
889
|
+
this.#queuedSends = this.#queuedSends.filter((entry) => !which(entry.requestId));
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
#bootFailed(proc: RscProcess, message: string): boolean {
|
|
893
|
+
this.#booting = null;
|
|
894
|
+
this.#logger.error(`[rsc] worker init error on restart: ${message}`);
|
|
895
|
+
const covered = !this.#pendingReload || this.#pendingReload.reloadState <= this.#init.reloadState;
|
|
896
|
+
if (covered) this.#rejectReload(message);
|
|
897
|
+
const rolling = proc === this.#proc ? this.#rollingRecycle : null;
|
|
898
|
+
this.#rollingRecycle = null;
|
|
899
|
+
if (!rolling || !RscWorker.#isAlive(rolling.oldProc)) return false;
|
|
900
|
+
this.#proc = rolling.oldProc;
|
|
901
|
+
this.#status = "ready";
|
|
902
|
+
this.#reloadsSinceSpawn = rolling.reloadsSinceSpawn;
|
|
903
|
+
if (this.#droppedWhileBooting) {
|
|
904
|
+
this.#droppedWhileBooting = false;
|
|
905
|
+
this.updateCssAssets(this.#cssAssets);
|
|
906
|
+
this.#sendLogLevel();
|
|
907
|
+
this.invalidateRouteResultCache();
|
|
908
|
+
}
|
|
909
|
+
this.#flushQueuedSends();
|
|
910
|
+
if (!covered) this.#sendPendingReload();
|
|
911
|
+
return true;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
static #isAlive(proc: RscProcess): boolean {
|
|
915
|
+
return proc.exitCode === null && proc.signalCode === null && !proc.killed;
|
|
916
|
+
}
|
|
917
|
+
|
|
851
918
|
#finishRollingRecycle(): void {
|
|
852
919
|
const recycle = this.#rollingRecycle;
|
|
853
920
|
if (!recycle) return;
|
|
@@ -861,22 +928,41 @@ export class RscWorker {
|
|
|
861
928
|
|
|
862
929
|
#handleExit(proc: RscProcess, code: number | null): void {
|
|
863
930
|
|
|
931
|
+
if (proc === this.#rollingRecycle?.oldProc) {
|
|
932
|
+
this.#rollingRecycle = null;
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
|
|
864
936
|
if (proc !== this.#proc) return;
|
|
865
937
|
|
|
938
|
+
if (proc === this.#booting && !this.#killed && proc.signalCode === null) {
|
|
939
|
+
const message = `rsc worker exited with code ${code} while loading the pages bundle`;
|
|
940
|
+
if (this.#readyResolved && this.#bootFailed(proc, message)) return;
|
|
941
|
+
if (!this.#readyResolved) {
|
|
942
|
+
this.#booting = null;
|
|
943
|
+
this.#status = "stopped";
|
|
944
|
+
this.#failRequests(new Error(message));
|
|
945
|
+
this.#rejectReload(message);
|
|
946
|
+
this.#rejectReady(new Error(message));
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
866
951
|
const err = new Error(`rsc worker exited with code ${code}`);
|
|
867
|
-
|
|
868
|
-
this.#
|
|
869
|
-
|
|
870
|
-
this.#
|
|
871
|
-
this.#queuedSends = [];
|
|
952
|
+
|
|
953
|
+
const queued = new Set(this.#queuedSends.map((entry) => entry.requestId));
|
|
954
|
+
const keepsQueue = this.#restartAttempts < RscWorker.#queuedRespawns;
|
|
955
|
+
this.#failRequests(err, (requestId) => !keepsQueue || !queued.has(requestId));
|
|
872
956
|
|
|
873
957
|
if (this.#killed) {
|
|
874
958
|
this.#status = "stopped";
|
|
959
|
+
this.#failRequests(err);
|
|
875
960
|
this.#rejectReload(err.message);
|
|
876
961
|
return;
|
|
877
962
|
}
|
|
878
963
|
if (this.#failBeforeReady && !this.#readyResolved) {
|
|
879
964
|
this.#status = "stopped";
|
|
965
|
+
this.#failRequests(err);
|
|
880
966
|
this.#rejectReload(err.message);
|
|
881
967
|
this.#rejectReady(err);
|
|
882
968
|
return;
|
|
@@ -913,7 +999,7 @@ export class RscWorker {
|
|
|
913
999
|
}
|
|
914
1000
|
|
|
915
1001
|
#maybeRecycleFromMetrics(metrics: AkanMetricsReport): void {
|
|
916
|
-
if (this.#pending.size > 0) return;
|
|
1002
|
+
if (this.#pending.size > 0 || this.#calls.size > 0) return;
|
|
917
1003
|
const maxRssBytes = RscWorker.#getRscMaxRssBytes();
|
|
918
1004
|
if (maxRssBytes && metrics.rssBytes && metrics.rssBytes >= maxRssBytes) {
|
|
919
1005
|
this.restartWhenIdle(`rss>${Math.round(maxRssBytes / 1024 / 1024)}MiB`);
|
package/server/webRouter.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
RouteClientCache,
|
|
20
20
|
type RouteSeedIndex,
|
|
21
21
|
RouteSeedIndexStore,
|
|
22
|
+
type RoutesManifest,
|
|
22
23
|
RoutesManifestStore,
|
|
23
24
|
} from "./artifact";
|
|
24
25
|
import {
|
|
@@ -251,6 +252,8 @@ interface WebRouterOptions {
|
|
|
251
252
|
rsc: RscWorker;
|
|
252
253
|
seedIndex: RouteSeedIndex;
|
|
253
254
|
upgradeHmrWs: (req: Request, data: HmrWsData) => boolean;
|
|
255
|
+
/** The production build's route manifest, which the worker was already handed at boot. */
|
|
256
|
+
prebuilt?: RoutesManifest | null;
|
|
254
257
|
}
|
|
255
258
|
|
|
256
259
|
interface CachedHtmlResult {
|
|
@@ -300,7 +303,8 @@ export class WebRouter {
|
|
|
300
303
|
/** What this router actually mounts, already intersected with what the artifact carries. */
|
|
301
304
|
readonly web: AkanWebConfig;
|
|
302
305
|
#seedIndex: RouteSeedIndex;
|
|
303
|
-
|
|
306
|
+
readonly #prebuilt: RoutesManifest | null;
|
|
307
|
+
constructor({ artifact, web, cssBytesByUrl, rsc, seedIndex, upgradeHmrWs, prebuilt = null }: WebRouterOptions) {
|
|
304
308
|
this.#logger.verbose(`[SSR] loaded ${Object.keys(cssBytesByUrl).length} CSS assets`);
|
|
305
309
|
this.web = web;
|
|
306
310
|
if (process.env.NODE_ENV === "production" && !this.#prodMode)
|
|
@@ -320,6 +324,7 @@ export class WebRouter {
|
|
|
320
324
|
cssBytesByUrl,
|
|
321
325
|
};
|
|
322
326
|
this.#seedIndex = seedIndex;
|
|
327
|
+
this.#prebuilt = prebuilt;
|
|
323
328
|
if (this.#prodMode) {
|
|
324
329
|
this.#builderRpc = null;
|
|
325
330
|
this.#routeCache = new RouteClientCache({
|
|
@@ -350,16 +355,7 @@ export class WebRouter {
|
|
|
350
355
|
}
|
|
351
356
|
|
|
352
357
|
async initializeRoute() {
|
|
353
|
-
|
|
354
|
-
if (prebuilt) {
|
|
355
|
-
this.#routeCache.seed(prebuilt);
|
|
356
|
-
await this.#rsc.reload({
|
|
357
|
-
clientManifest: this.#mergeRuntimeManifest().clientManifest,
|
|
358
|
-
cssAssets: this.renderState.cssAssets,
|
|
359
|
-
|
|
360
|
-
buildId: this.#artifact.pagesBundleBuildId,
|
|
361
|
-
});
|
|
362
|
-
}
|
|
358
|
+
if (this.#prebuilt) this.#routeCache.seed(this.#prebuilt);
|
|
363
359
|
|
|
364
360
|
const clientServePrefix = `/_akan/client`;
|
|
365
361
|
const clientOutputDir = `${this.#artifactDir}/client`;
|
|
@@ -398,7 +394,7 @@ export class WebRouter {
|
|
|
398
394
|
<script type="module" src="/csr.js"></script>
|
|
399
395
|
</body>
|
|
400
396
|
</html>`;
|
|
401
|
-
return new Response(this.#withCsrHmr(htmlText), { headers:
|
|
397
|
+
return new Response(this.#withCsrHmr(htmlText), { headers: this.#htmlResponseHeaders(200) });
|
|
402
398
|
},
|
|
403
399
|
...(this.#csrDevShell
|
|
404
400
|
? {
|
|
@@ -549,7 +545,7 @@ export class WebRouter {
|
|
|
549
545
|
const csrHtml = await this.#resolveCsrHtml(csrOutputDir, url.pathname);
|
|
550
546
|
if (!csrHtml) return this.#csrUnavailableResponse(url.pathname);
|
|
551
547
|
const html = await Bun.file(csrHtml).text();
|
|
552
|
-
return new Response(this.#withCsrHmr(html), { headers:
|
|
548
|
+
return new Response(this.#withCsrHmr(html), { headers: this.#htmlResponseHeaders(200) });
|
|
553
549
|
}
|
|
554
550
|
|
|
555
551
|
const csrAssetPath = path.extname(url.pathname) ? resolveStaticPath(csrOutputDir, url.pathname) : null;
|
|
@@ -608,7 +604,7 @@ export class WebRouter {
|
|
|
608
604
|
const htmlCacheEntry = htmlCacheDecision.entry;
|
|
609
605
|
const cachedHtml = htmlCacheEntry ? this.#getCachedHtml(htmlCacheEntry.key) : null;
|
|
610
606
|
if (cachedHtml) {
|
|
611
|
-
const cachedHeaders =
|
|
607
|
+
const cachedHeaders = this.#htmlResponseHeaders(200);
|
|
612
608
|
cachedHeaders.set("X-Akan-Cache", "HIT");
|
|
613
609
|
return new Response(cachedHtml, { headers: cachedHeaders });
|
|
614
610
|
}
|
|
@@ -649,7 +645,7 @@ export class WebRouter {
|
|
|
649
645
|
},
|
|
650
646
|
});
|
|
651
647
|
const responseStatus = rscResult.status ?? 200;
|
|
652
|
-
const responseHeaders =
|
|
648
|
+
const responseHeaders = this.#htmlResponseHeaders(responseStatus);
|
|
653
649
|
if (req.method === "HEAD") {
|
|
654
650
|
const headers = new Headers(responseHeaders);
|
|
655
651
|
if (htmlCacheEntry && responseStatus === 200) headers.set("X-Akan-Cache", "MISS");
|
|
@@ -971,9 +967,9 @@ export class WebRouter {
|
|
|
971
967
|
return this.renderState.cssAssets[basePath ?? ""]?.cssUrl ?? null;
|
|
972
968
|
}
|
|
973
969
|
|
|
974
|
-
|
|
970
|
+
#htmlResponseHeaders(status: number): Headers {
|
|
975
971
|
const headers = new Headers({ "Content-Type": "text/html; charset=utf-8" });
|
|
976
|
-
if (status >= 400) headers.set("Cache-Control", "no-store");
|
|
972
|
+
if (status >= 400 || !this.#prodMode) headers.set("Cache-Control", "no-store");
|
|
977
973
|
return WebRouter.#applySecurityHeaders(headers, { html: true });
|
|
978
974
|
}
|
|
979
975
|
#withCsrHmr(html: string): string {
|
|
@@ -994,7 +990,7 @@ export class WebRouter {
|
|
|
994
990
|
cssHref: this.#getStylesheetHref(req, pathname),
|
|
995
991
|
});
|
|
996
992
|
if (!html) return this.#csrUnavailableResponse(pathname);
|
|
997
|
-
return new Response(this.#withCsrHmr(html), { headers:
|
|
993
|
+
return new Response(this.#withCsrHmr(html), { headers: this.#htmlResponseHeaders(200) });
|
|
998
994
|
}
|
|
999
995
|
async #resolveCsrHtml(csrOutputDir: string, pathname: string): Promise<string | null> {
|
|
1000
996
|
const resolved = WebRouter.#resolveCsrHtmlPath(csrOutputDir, pathname, this.#artifact);
|
|
@@ -1044,7 +1040,16 @@ export class WebRouter {
|
|
|
1044
1040
|
const cssBytesByUrl = await WebRouter.#loadCssBytesByUrl(artifact, artifactDir);
|
|
1045
1041
|
const prodMode = process.env.NODE_ENV === "production" && process.env.AKAN_COMMAND_TYPE !== "start";
|
|
1046
1042
|
|
|
1047
|
-
const
|
|
1043
|
+
const prebuilt = prodMode ? await RoutesManifestStore.read(artifactDir) : null;
|
|
1044
|
+
const rsc = new RscWorker(
|
|
1045
|
+
prebuilt
|
|
1046
|
+
? {
|
|
1047
|
+
...artifact,
|
|
1048
|
+
rscRuntimeClientManifest: { ...artifact.rscRuntimeClientManifest, ...prebuilt.clientManifest },
|
|
1049
|
+
}
|
|
1050
|
+
: artifact,
|
|
1051
|
+
{ failBeforeReady: prodMode },
|
|
1052
|
+
);
|
|
1048
1053
|
if (prodMode)
|
|
1049
1054
|
void rsc.ready.catch((error: unknown) => {
|
|
1050
1055
|
new Logger("WebRouter").error(`RSC worker failed to load the pages bundle: ${String(error)}`);
|
|
@@ -1059,6 +1064,7 @@ export class WebRouter {
|
|
|
1059
1064
|
rsc,
|
|
1060
1065
|
seedIndex,
|
|
1061
1066
|
upgradeHmrWs,
|
|
1067
|
+
prebuilt,
|
|
1062
1068
|
});
|
|
1063
1069
|
}
|
|
1064
1070
|
|
|
@@ -2,5 +2,7 @@ export type AkanHmrPhase = "refresh-import" | "react-refresh" | null;
|
|
|
2
2
|
declare global {
|
|
3
3
|
var __AKAN_HMR_PHASE__: AkanHmrPhase | undefined;
|
|
4
4
|
}
|
|
5
|
+
/** @deprecated Always `null`: the dev client that set the phase applies modules through its registry now. */
|
|
5
6
|
export declare function getAkanHmrPhase(): AkanHmrPhase;
|
|
7
|
+
/** @deprecated Always `false`, for the reason `getAkanHmrPhase` is always `null`. */
|
|
6
8
|
export declare function isAkanHmrApplying(): boolean;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CssAsset } from "../types.d.ts";
|
|
2
|
+
export declare class DevArtifactPruner {
|
|
3
|
+
#private;
|
|
4
|
+
constructor(artifactDir: string, { graceMs }?: {
|
|
5
|
+
graceMs?: number;
|
|
6
|
+
});
|
|
7
|
+
prunePages(runningPath: string): Promise<void>;
|
|
8
|
+
pruneStyles(cssAssets: Record<string, CssAsset>): Promise<void>;
|
|
9
|
+
}
|
|
@@ -30,6 +30,4 @@ export declare class DevHmrController {
|
|
|
30
30
|
get builderRpc(): BuilderRpc;
|
|
31
31
|
dispose(): void;
|
|
32
32
|
handleWs(req: Request): Response | undefined;
|
|
33
|
-
ensureRoute(url: URL): Promise<import("..").MergedManifest>;
|
|
34
|
-
routeIdsForPath(pathname: string): string[] | undefined;
|
|
35
33
|
}
|
|
@@ -67,14 +67,16 @@ export interface RscWorkerReloadInput {
|
|
|
67
67
|
/** Undefined keeps the latest bundle a reload named (e.g. a client-manifest-only reload after a lazy route build). */
|
|
68
68
|
pagesBundlePath?: string;
|
|
69
69
|
}
|
|
70
|
-
interface RscAdoptedBundle {
|
|
70
|
+
export interface RscAdoptedBundle {
|
|
71
71
|
pagesBundlePath: string;
|
|
72
72
|
buildId: number;
|
|
73
73
|
}
|
|
74
74
|
/** A reload the worker did not take: what it runs instead, and whether a later reload failed in this one's place. */
|
|
75
75
|
export interface RscReloadFailure extends Error {
|
|
76
|
+
/** What the worker runs now. */
|
|
76
77
|
adopted: RscAdoptedBundle;
|
|
77
|
-
|
|
78
|
+
/** The latest state it was asked to take, which a later reload may have moved past the caller's own. */
|
|
79
|
+
failed: RscAdoptedBundle;
|
|
78
80
|
}
|
|
79
81
|
export declare class RscWorker {
|
|
80
82
|
#private;
|
|
@@ -96,6 +98,5 @@ export declare class RscWorker {
|
|
|
96
98
|
restartWhenIdle(reason: string): boolean;
|
|
97
99
|
updateCssAssets(cssAssets: Record<string, CssAsset>): void;
|
|
98
100
|
/** Resolves with the pages bundle the worker runs once it has taken this reload, or a later one. */
|
|
99
|
-
reload(input: RscWorkerReloadInput): Promise<
|
|
101
|
+
reload(input: RscWorkerReloadInput): Promise<RscAdoptedBundle>;
|
|
100
102
|
}
|
|
101
|
-
export {};
|
|
@@ -3,7 +3,7 @@ import { type AkanI18nConfig, type LogRecord } from "akanjs/common";
|
|
|
3
3
|
import { type AkanRequestStore } from "akanjs/fetch";
|
|
4
4
|
import type { AkanMetricsReport } from "akanjs/service";
|
|
5
5
|
import type { PagePromptSource } from "../signal/mcp/pagePrompt.d.ts";
|
|
6
|
-
import { type BuilderRpc, type RouteSeedIndex } from "./artifact.d.ts";
|
|
6
|
+
import { type BuilderRpc, type RouteSeedIndex, type RoutesManifest } from "./artifact.d.ts";
|
|
7
7
|
import { type RouteCacheInvalidation, type RouteCacheRenderState } from "./cachePolicy.d.ts";
|
|
8
8
|
import type { HmrWsData, HmrWsHub } from "./hmr/wsHub.d.ts";
|
|
9
9
|
import { type RscRedirectMethod, type RscRedirectStatus, type RscRenderResult, RscWorker } from "./rscWorkerHost.d.ts";
|
|
@@ -49,13 +49,15 @@ interface WebRouterOptions {
|
|
|
49
49
|
rsc: RscWorker;
|
|
50
50
|
seedIndex: RouteSeedIndex;
|
|
51
51
|
upgradeHmrWs: (req: Request, data: HmrWsData) => boolean;
|
|
52
|
+
/** The production build's route manifest, which the worker was already handed at boot. */
|
|
53
|
+
prebuilt?: RoutesManifest | null;
|
|
52
54
|
}
|
|
53
55
|
export declare class WebRouter {
|
|
54
56
|
#private;
|
|
55
57
|
renderState: RenderState;
|
|
56
58
|
/** What this router actually mounts, already intersected with what the artifact carries. */
|
|
57
59
|
readonly web: AkanWebConfig;
|
|
58
|
-
constructor({ artifact, web, cssBytesByUrl, rsc, seedIndex, upgradeHmrWs }: WebRouterOptions);
|
|
60
|
+
constructor({ artifact, web, cssBytesByUrl, rsc, seedIndex, upgradeHmrWs, prebuilt }: WebRouterOptions);
|
|
59
61
|
initializeRoute(): Promise<{
|
|
60
62
|
renderEnvRoutes: Bun.Serve.Routes<unknown, string> | Bun.Serve.RoutesWithUpgrade<unknown, string>;
|
|
61
63
|
hmrHub: HmrWsHub | null;
|