@akanjs/devkit 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/DEV_RUNTIME_KNOBS.md +9 -6
- package/akanApp/akanApp.host.ts +79 -22
- package/akanApp/devHostPolicy.test.ts +56 -11
- package/akanApp/devHostPolicy.ts +10 -10
- package/frontendBuild/clientEntryDiscovery.test.ts +75 -0
- package/frontendBuild/clientEntryDiscovery.ts +112 -70
- package/frontendBuild/csrDevModuleCompiler.test.ts +89 -0
- package/frontendBuild/csrDevModuleCompiler.ts +6 -2
- package/frontendBuild/csrDevPatcher.test.ts +234 -2
- package/frontendBuild/csrDevPatcher.ts +184 -10
- package/frontendBuild/csrDevPaths.ts +31 -11
- package/frontendBuild/csrDevResolver.test.ts +76 -2
- package/frontendBuild/csrDevResolver.ts +59 -10
- package/frontendBuild/csrDevTypes.ts +2 -0
- package/frontendBuild/devRegistryBundler.ts +18 -3
- package/frontendBuild/ssrDevBundler.ts +1 -7
- package/incrementalBuilder/incrementalBuilder.host.test.ts +34 -0
- package/incrementalBuilder/incrementalBuilder.host.ts +18 -3
- package/incrementalBuilder/incrementalBuilder.proc.test.ts +62 -1
- package/incrementalBuilder/incrementalBuilder.proc.ts +58 -24
- package/integration/devStability.integration.test.ts +9 -2
- package/package.json +2 -2
|
@@ -8,7 +8,7 @@ import { bundleDefine } from "./bundleDefine";
|
|
|
8
8
|
import type { ClientEntryDiscovery } from "./clientBuildTypes";
|
|
9
9
|
import { GraphClientEntryDiscovery } from "./clientEntryDiscovery";
|
|
10
10
|
import { CsrDevPaths } from "./csrDevPaths";
|
|
11
|
-
import type { CsrDevContext
|
|
11
|
+
import type { CsrDevContext } from "./csrDevTypes";
|
|
12
12
|
import { DevRegistryBundler } from "./devRegistryBundler";
|
|
13
13
|
import { VENDOR_SPECIFIERS } from "./vendorSpecifiers";
|
|
14
14
|
|
|
@@ -79,12 +79,6 @@ export class SsrDevBundler extends DevRegistryBundler {
|
|
|
79
79
|
return this.#discovery;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
override async wantedRoots(files: string[], graph: CsrDevGraph): Promise<Set<string>> {
|
|
83
|
-
const entries = new Set(await this.clientEntries());
|
|
84
|
-
const reached = await super.wantedRoots(files, graph);
|
|
85
|
-
return new Set(files.filter((file) => reached.has(file) || entries.has(CsrDevPaths.realpath(file))));
|
|
86
|
-
}
|
|
87
|
-
|
|
88
82
|
protected async rootFiles(entryFiles: string[]): Promise<string[]> {
|
|
89
83
|
return [...entryFiles, ...(await this.clientEntries())];
|
|
90
84
|
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { afterEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { CSR_DEV_PATCHING_MARKER, SSR_DEV_DIRNAME } from "akanjs/server/hmr/csrDevManifest";
|
|
2
6
|
import { MemoryLimit } from "akanjs/server/memoryLimit";
|
|
3
7
|
import { IncrementalBuilderHost } from "./incrementalBuilder.host";
|
|
4
8
|
|
|
@@ -172,6 +176,36 @@ describe("IncrementalBuilderHost", () => {
|
|
|
172
176
|
host.stop();
|
|
173
177
|
expect(host.recycle("after stop")).toBe(false);
|
|
174
178
|
});
|
|
179
|
+
|
|
180
|
+
test("a patcher a crash turned off stays off in the host that replaces this one", async () => {
|
|
181
|
+
const cwdPath = await mkdtemp(path.join(os.tmpdir(), "akan-builder-patcher-"));
|
|
182
|
+
try {
|
|
183
|
+
const spawns = mockSpawns();
|
|
184
|
+
const app = { cwdPath } as never;
|
|
185
|
+
const host = new IncrementalBuilderHost({ app, entry: "/tmp/builder.ts", env: {}, onMessage: () => undefined });
|
|
186
|
+
host.start();
|
|
187
|
+
spawns[0]?.options.ipc?.({ type: "builder-ready" });
|
|
188
|
+
const marker = path.join(cwdPath, ".akan/artifact", SSR_DEV_DIRNAME, CSR_DEV_PATCHING_MARKER);
|
|
189
|
+
await Bun.write(marker, "");
|
|
190
|
+
spawns[0]?.options.onExit?.();
|
|
191
|
+
await wait(1_050);
|
|
192
|
+
expect(spawns[1]?.options.env?.AKAN_DEV_CSR_PATCHER).toBe("off");
|
|
193
|
+
expect(host.patcherOff).toBe(true);
|
|
194
|
+
host.stop();
|
|
195
|
+
const next = new IncrementalBuilderHost({
|
|
196
|
+
app,
|
|
197
|
+
entry: "/tmp/builder.ts",
|
|
198
|
+
env: {},
|
|
199
|
+
onMessage: () => undefined,
|
|
200
|
+
patcherOff: host.patcherOff,
|
|
201
|
+
});
|
|
202
|
+
next.start();
|
|
203
|
+
expect(spawns[2]?.options.env?.AKAN_DEV_CSR_PATCHER).toBe("off");
|
|
204
|
+
next.stop();
|
|
205
|
+
} finally {
|
|
206
|
+
await rm(cwdPath, { recursive: true, force: true });
|
|
207
|
+
}
|
|
208
|
+
});
|
|
175
209
|
});
|
|
176
210
|
|
|
177
211
|
describe("IncrementalBuilderHost.maxRssBytes", () => {
|
|
@@ -30,6 +30,8 @@ interface IncrementalBuilderHostOptions {
|
|
|
30
30
|
onMessage: (message: BuilderMessage) => void;
|
|
31
31
|
/** Required with `stdio: "pipe"` (an undrained pipe blocks the builder); receives decoded chunks, not lines. */
|
|
32
32
|
onOutput?: (kind: "stdout" | "stderr", text: string) => void;
|
|
33
|
+
/** Carried over from the host this one replaces: a patcher a crash turned off stays off. */
|
|
34
|
+
patcherOff?: boolean;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
/** `recycling`: the builder finishes accepted work but refuses new requests; hold them for the replacement. */
|
|
@@ -73,17 +75,29 @@ export class IncrementalBuilderHost {
|
|
|
73
75
|
// its own exit, so an unanswered page request would spin forever.
|
|
74
76
|
readonly #inFlight = new Map<number, "build-route" | "build-csr">();
|
|
75
77
|
#startOptions: IncrementalBuilderStartOptions = {};
|
|
76
|
-
constructor({
|
|
78
|
+
constructor({
|
|
79
|
+
app,
|
|
80
|
+
entry,
|
|
81
|
+
env,
|
|
82
|
+
stdio = "inherit",
|
|
83
|
+
onMessage,
|
|
84
|
+
onOutput,
|
|
85
|
+
patcherOff = false,
|
|
86
|
+
}: IncrementalBuilderHostOptions) {
|
|
77
87
|
this.app = app;
|
|
78
88
|
this.entry = entry;
|
|
79
89
|
this.env = env;
|
|
80
90
|
this.#stdio = stdio;
|
|
81
91
|
this.#onMessage = onMessage;
|
|
82
92
|
this.#onOutput = onOutput ?? null;
|
|
93
|
+
this.#patcherOff = patcherOff;
|
|
83
94
|
}
|
|
84
95
|
get status() {
|
|
85
96
|
return this.#status;
|
|
86
97
|
}
|
|
98
|
+
get patcherOff(): boolean {
|
|
99
|
+
return this.#patcherOff;
|
|
100
|
+
}
|
|
87
101
|
/** For reading RSS between builds: the builder's own metrics sample the post-work peak, stale once arenas return. */
|
|
88
102
|
get pid(): number | null {
|
|
89
103
|
return this.#proc?.pid ?? null;
|
|
@@ -292,7 +306,8 @@ export class IncrementalBuilderHost {
|
|
|
292
306
|
{
|
|
293
307
|
stdio = "inherit",
|
|
294
308
|
onOutput,
|
|
295
|
-
|
|
309
|
+
patcherOff = false,
|
|
310
|
+
}: Pick<IncrementalBuilderHostOptions, "stdio" | "onOutput" | "patcherOff"> = {},
|
|
296
311
|
) {
|
|
297
312
|
const candidates = [
|
|
298
313
|
path.join(app.workspace.workspaceRoot, "pkgs/@akanjs/devkit/incrementalBuilder/incrementalBuilder.proc.ts"),
|
|
@@ -305,7 +320,7 @@ export class IncrementalBuilderHost {
|
|
|
305
320
|
];
|
|
306
321
|
for (const c of candidates)
|
|
307
322
|
if (await Bun.file(c).exists())
|
|
308
|
-
return new IncrementalBuilderHost({ app, entry: c, env, stdio, onMessage, onOutput });
|
|
323
|
+
return new IncrementalBuilderHost({ app, entry: c, env, stdio, onMessage, onOutput, patcherOff });
|
|
309
324
|
throw new Error(`[cli] frontend builder entry not found; looked in: ${candidates.join(", ")}`);
|
|
310
325
|
}
|
|
311
326
|
}
|
|
@@ -77,7 +77,7 @@ process.send({ type: "build-batch-result", data: { generation, errors: {}, artif
|
|
|
77
77
|
error: expect.stringContaining(reason),
|
|
78
78
|
});
|
|
79
79
|
expect(messages.find((message) => message.type === "build-status")).toMatchObject({
|
|
80
|
-
data: { generation: 6, phase: "route", ok: false, message: expect.stringContaining(reason) },
|
|
80
|
+
data: { generation: 6, phase: "route", ok: false, message: expect.stringContaining(reason), scope: "page:/" },
|
|
81
81
|
});
|
|
82
82
|
expect(log).toContain(reason);
|
|
83
83
|
}, 30_000);
|
|
@@ -148,6 +148,67 @@ process.send({ type: "build-batch-result", data: { generation, errors: {}, artif
|
|
|
148
148
|
}
|
|
149
149
|
}, 60_000);
|
|
150
150
|
|
|
151
|
+
test("a route build's check whose own worker is killed is tried again after the crash window", async () => {
|
|
152
|
+
const { root } = track(await createTempApp("demo", path.join(import.meta.dir, "..", "local")));
|
|
153
|
+
const appDir = path.join(root, "apps/demo");
|
|
154
|
+
const runs = path.join(root, "ssr-runs.log");
|
|
155
|
+
// The boot build succeeds, the route check's worker is killed, and the retry after the window succeeds.
|
|
156
|
+
await writeText(
|
|
157
|
+
path.join(root, "pkgs/@akanjs/devkit/incrementalBuilder/buildBatch.proc.ts"),
|
|
158
|
+
`import fs from "node:fs";
|
|
159
|
+
const { generation, needs } = JSON.parse(process.argv[2]);
|
|
160
|
+
if (needs.includes("ssr")) {
|
|
161
|
+
fs.appendFileSync(${JSON.stringify(runs)}, Date.now() + "\\n");
|
|
162
|
+
if (fs.readFileSync(${JSON.stringify(runs)}, "utf8").trim().split("\\n").length === 2) process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
process.send({ type: "build-batch-result", data: { generation, errors: {}, artifact: {}, optimizedFonts: { css: "", files: [] } } });
|
|
165
|
+
`,
|
|
166
|
+
);
|
|
167
|
+
const page = path.join(appDir, "page/_index.tsx");
|
|
168
|
+
await writeText(page, 'import { Card } from "../ui/Card";\nexport default Card;\n');
|
|
169
|
+
await writeText(path.join(appDir, "ui/Card.tsx"), '"use client";\nexport const Card = () => null;\n');
|
|
170
|
+
|
|
171
|
+
const messages: BuilderMessage[] = [];
|
|
172
|
+
const proc = Bun.spawn(["bun", path.join(import.meta.dir, "incrementalBuilder.proc.ts")], {
|
|
173
|
+
cwd: appDir,
|
|
174
|
+
env: {
|
|
175
|
+
...process.env,
|
|
176
|
+
AKAN_WORKSPACE_ROOT: root,
|
|
177
|
+
AKAN_PUBLIC_APP_NAME: "demo",
|
|
178
|
+
AKAN_PUBLIC_REPO_NAME: "repo",
|
|
179
|
+
AKAN_PUBLIC_SERVE_DOMAIN: "localhost",
|
|
180
|
+
AKAN_PUBLIC_ENV: "local",
|
|
181
|
+
AKAN_WATCH: "0",
|
|
182
|
+
},
|
|
183
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
184
|
+
serialization: "advanced",
|
|
185
|
+
ipc: (message: BuilderMessage) => {
|
|
186
|
+
messages.push(message);
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
const ssrRuns = async () =>
|
|
190
|
+
(await Bun.file(runs).exists()) ? (await Bun.file(runs).text()).trim().split("\n").map(Number) : [];
|
|
191
|
+
try {
|
|
192
|
+
await until(proc, "boot-armed", () => messages.some((message) => message.type === "boot-armed"));
|
|
193
|
+
proc.send({
|
|
194
|
+
type: "build-route",
|
|
195
|
+
id: 1,
|
|
196
|
+
routeId: "page:/",
|
|
197
|
+
seeds: [page],
|
|
198
|
+
knownEntries: [],
|
|
199
|
+
generation: 3,
|
|
200
|
+
} satisfies BuilderReq);
|
|
201
|
+
await until(proc, "build-route-res", () => messages.some((message) => message.type === "build-route-res"));
|
|
202
|
+
const deadline = Date.now() + 20_000;
|
|
203
|
+
while ((await ssrRuns()).length < 3 && Date.now() < deadline) await wait(100);
|
|
204
|
+
const [, killed = 0, retried = 0] = await ssrRuns();
|
|
205
|
+
expect(retried - killed).toBeGreaterThanOrEqual(9_500);
|
|
206
|
+
} finally {
|
|
207
|
+
proc.kill();
|
|
208
|
+
await proc.exited;
|
|
209
|
+
}
|
|
210
|
+
}, 60_000);
|
|
211
|
+
|
|
151
212
|
test("a route build's registry check that has nothing to add reports no ssr status, so it cannot hide a failure", async () => {
|
|
152
213
|
const { root } = track(await createTempApp("demo", path.join(import.meta.dir, "..", "local")));
|
|
153
214
|
const appDir = path.join(root, "apps/demo");
|
|
@@ -165,7 +165,7 @@ class IncrementalBuilder {
|
|
|
165
165
|
this.#logger.verbose(`build-route ok routeId=${msg.routeId} newEntries=${delta.newEntries.length}`);
|
|
166
166
|
//? The builder's generation, not the route cache's: the cache's does not move when a fixed file maps to no entry,
|
|
167
167
|
//? so the rebuild's ok would carry the failure's own number.
|
|
168
|
-
this.#sendBuildStatus("route", { generation: this.#generation, ok: true, files: msg.seeds });
|
|
168
|
+
this.#sendBuildStatus("route", { generation: this.#generation, ok: true, files: msg.seeds, scope: msg.routeId });
|
|
169
169
|
return {
|
|
170
170
|
type: "build-route-res",
|
|
171
171
|
id: msg.id,
|
|
@@ -185,24 +185,30 @@ class IncrementalBuilder {
|
|
|
185
185
|
} catch (err) {
|
|
186
186
|
const errMsg = ApplicationBuildReporter.formatError(err, this.#app.workspace.workspaceRoot);
|
|
187
187
|
this.#logger.error(`build-route failed routeId=${msg.routeId}: ${errMsg}`);
|
|
188
|
-
this.#sendBuildStatus("route", {
|
|
188
|
+
this.#sendBuildStatus("route", {
|
|
189
|
+
generation: this.#generation,
|
|
190
|
+
ok: false,
|
|
191
|
+
files: msg.seeds,
|
|
192
|
+
message: errMsg,
|
|
193
|
+
scope: msg.routeId,
|
|
194
|
+
});
|
|
189
195
|
return { type: "build-route-res", id: msg.id, ok: false, error: errMsg };
|
|
190
196
|
}
|
|
191
197
|
}
|
|
192
198
|
#sendBuildStatus(
|
|
193
199
|
phase: BuildPhase,
|
|
194
|
-
{
|
|
200
|
+
{
|
|
201
|
+
generation,
|
|
202
|
+
ok,
|
|
203
|
+
files,
|
|
204
|
+
message,
|
|
205
|
+
scope,
|
|
206
|
+
}: { generation?: number; ok: boolean; files?: string[]; message?: string; scope?: string },
|
|
195
207
|
): void {
|
|
196
208
|
if (typeof generation !== "number") return;
|
|
197
209
|
BuilderChannel.emit({
|
|
198
210
|
type: "build-status",
|
|
199
|
-
data: {
|
|
200
|
-
generation,
|
|
201
|
-
phase,
|
|
202
|
-
ok,
|
|
203
|
-
files: files ?? [],
|
|
204
|
-
message,
|
|
205
|
-
},
|
|
211
|
+
data: { generation, phase, ok, files: files ?? [], message, ...(scope !== undefined ? { scope } : {}) },
|
|
206
212
|
});
|
|
207
213
|
}
|
|
208
214
|
async #enqueueFast<T>(label: string, fn: () => Promise<T>): Promise<T> {
|
|
@@ -445,11 +451,8 @@ class IncrementalBuilder {
|
|
|
445
451
|
if (ssrToWorker) needs.unshift("ssr");
|
|
446
452
|
const batch: BatchJob = { generation, needs, changedFiles: files, trace, ...(discovery ? { discovery } : {}) };
|
|
447
453
|
// A worker's registry build rewrites the directory its patcher reads, so the next save waits for it.
|
|
448
|
-
if (needs.includes("csr") || needs.includes("ssr"))
|
|
449
|
-
|
|
450
|
-
if (needs.includes("csr")) this.#patcher?.forget();
|
|
451
|
-
if (needs.includes("ssr")) this.#ssrPatcher?.forget();
|
|
452
|
-
} else void this.#workQueue.enqueueBatch(batch).catch(this.#slowLaneFailed("batch"));
|
|
454
|
+
if (needs.includes("csr") || needs.includes("ssr")) await this.#workQueue.enqueueBatch(batch);
|
|
455
|
+
else void this.#workQueue.enqueueBatch(batch).catch(this.#slowLaneFailed("batch"));
|
|
453
456
|
return;
|
|
454
457
|
}
|
|
455
458
|
if (discovery)
|
|
@@ -525,6 +528,9 @@ class IncrementalBuilder {
|
|
|
525
528
|
const hold = await ServerGraphFile.touches(await ServerGraphFile.read(this.#artifactDir), files, (file) =>
|
|
526
529
|
ServerGraphFile.clientExportsOf(file),
|
|
527
530
|
);
|
|
531
|
+
//? Ahead of this save's own discovery job in the slow lane: which client entries a page still names decides which
|
|
532
|
+
//? pending roots this patch keeps.
|
|
533
|
+
this.#discovery.invalidate?.(files);
|
|
528
534
|
return await this.#withSsrLock(
|
|
529
535
|
async () => await this.#runSsrPatcher(patcher, files, { trace, hold, batchGeneration: generation }),
|
|
530
536
|
);
|
|
@@ -532,14 +538,16 @@ class IncrementalBuilder {
|
|
|
532
538
|
|
|
533
539
|
//* Adds the entries a route build names to the registry, since the tab requires them by id. A registry that cannot
|
|
534
540
|
//* take them fails no route build: the page answers, and its tab waits for the registry while the overlay says why.
|
|
535
|
-
async #ensureSsrEntries(entries: string[]): Promise<void> {
|
|
541
|
+
async #ensureSsrEntries(entries: string[], { crashRetry = false }: { crashRetry?: boolean } = {}): Promise<void> {
|
|
536
542
|
if (entries.length === 0) return;
|
|
537
543
|
//? The boot build takes every entry the routes reach, so the first page answers now and its tab waits for the
|
|
538
544
|
//? registry (boot.json) instead; whatever that build missed is added right after it lands.
|
|
539
545
|
const arming = this.#ssrArming;
|
|
540
546
|
if (arming) {
|
|
541
547
|
void this.#runBeside(
|
|
542
|
-
arming
|
|
548
|
+
arming
|
|
549
|
+
.then(async () => await this.#ensureSsrEntries(entries, { crashRetry }))
|
|
550
|
+
.catch(this.#slowLaneFailed("ssr-ensure")),
|
|
543
551
|
);
|
|
544
552
|
return;
|
|
545
553
|
}
|
|
@@ -558,6 +566,8 @@ class IncrementalBuilder {
|
|
|
558
566
|
//? the SSR lock that save's patch waits on.
|
|
559
567
|
if (this.#ssrBroken) return;
|
|
560
568
|
if (this.#ssrCrashedAt !== null && Date.now() - this.#ssrCrashedAt < IncrementalBuilder.#crashRetryMs) {
|
|
569
|
+
//? Handed over just now with nothing run for them: the retry must find them to hand over again.
|
|
570
|
+
patcher?.releaseHandedOver();
|
|
561
571
|
this.#retryAfterCrash(this.#ssrCrashedAt, entries);
|
|
562
572
|
return;
|
|
563
573
|
}
|
|
@@ -566,6 +576,9 @@ class IncrementalBuilder {
|
|
|
566
576
|
`ssr-registry could not take a route's entries; the save that fixes it rebuilds it: ${ApplicationBuildReporter.formatError(err, this.#app.workspace.workspaceRoot)}`,
|
|
567
577
|
);
|
|
568
578
|
});
|
|
579
|
+
//? A worker that died took these entries with it and left them pending nowhere: they are tried once more after the
|
|
580
|
+
//? crash window, but a retry that crashes again waits for the next route build or save.
|
|
581
|
+
if (this.#ssrCrashedAt !== null && !crashRetry) this.#retryAfterCrash(this.#ssrCrashedAt, entries);
|
|
569
582
|
});
|
|
570
583
|
}
|
|
571
584
|
|
|
@@ -580,7 +593,9 @@ class IncrementalBuilder {
|
|
|
580
593
|
const retried = [...this.#crashRetryEntries];
|
|
581
594
|
this.#crashRetryEntries.clear();
|
|
582
595
|
if (this.shuttingDown || retried.length === 0) return;
|
|
583
|
-
void this.#runBeside(this.#ensureSsrEntries(retried)).catch(
|
|
596
|
+
void this.#runBeside(this.#ensureSsrEntries(retried, { crashRetry: true })).catch(
|
|
597
|
+
this.#slowLaneFailed("ssr-crash-retry"),
|
|
598
|
+
);
|
|
584
599
|
},
|
|
585
600
|
Math.max(0, crashedAt + IncrementalBuilder.#crashRetryMs - Date.now()),
|
|
586
601
|
);
|
|
@@ -654,8 +669,12 @@ class IncrementalBuilder {
|
|
|
654
669
|
|
|
655
670
|
// Runs the worker directly, under the SSR lock its callers hold: queueing it could wait on a batch that waits on them.
|
|
656
671
|
async #runSsrWorker(generation: number): Promise<void> {
|
|
657
|
-
|
|
658
|
-
|
|
672
|
+
let result: BuildBatchResult;
|
|
673
|
+
try {
|
|
674
|
+
result = await this.#runBatch({ generation, needs: ["ssr"], changedFiles: [] });
|
|
675
|
+
} finally {
|
|
676
|
+
this.#ssrPatcher?.forget();
|
|
677
|
+
}
|
|
659
678
|
this.#noteSsrWorker(result);
|
|
660
679
|
if (result.errors.ssr) throw new Error(result.errors.ssr);
|
|
661
680
|
}
|
|
@@ -741,9 +760,20 @@ class IncrementalBuilder {
|
|
|
741
760
|
//? The slow lane runs in order, so every batch up to this one has had its discovery job.
|
|
742
761
|
this.#discoveredThrough = Math.max(this.#discoveredThrough, work.generation);
|
|
743
762
|
// A worker writing ssr-dev must not overlap the boot build, which no longer runs in this lane.
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
763
|
+
//? Forgotten inside the lock: a route build's check waiting on it would patch the old state over the worker's.
|
|
764
|
+
try {
|
|
765
|
+
if (work.needs.includes("ssr"))
|
|
766
|
+
await this.#withSsrLock(async () => {
|
|
767
|
+
try {
|
|
768
|
+
this.#noteSsrWorker(await this.#runBatch(work));
|
|
769
|
+
} finally {
|
|
770
|
+
this.#ssrPatcher?.forget();
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
else await this.#runBatch(work);
|
|
774
|
+
} finally {
|
|
775
|
+
if (work.needs.includes("csr")) this.#patcher?.forget();
|
|
776
|
+
}
|
|
747
777
|
}
|
|
748
778
|
|
|
749
779
|
async #refreshDiscovery({ files, refresh, generation }: DiscoveryJob): Promise<void> {
|
|
@@ -762,8 +792,12 @@ class IncrementalBuilder {
|
|
|
762
792
|
if (result.cssAssets) this.#artifact = { ...this.#artifact, cssAssets: result.cssAssets };
|
|
763
793
|
// A need the worker died before reporting goes red here instead of looking silently successful.
|
|
764
794
|
if (result.crashed) {
|
|
795
|
+
const crashedNeeds = result.crashedNeeds ?? needs;
|
|
796
|
+
//? A worker that died wrote no failure for the roots handed to it: they must not wait on one that never comes.
|
|
797
|
+
if (crashedNeeds.includes("csr")) this.#patcher?.releaseHandedOver();
|
|
798
|
+
if (crashedNeeds.includes("ssr")) this.#ssrPatcher?.releaseHandedOver();
|
|
765
799
|
// `base` is not a `BuildPhase` and never comes through here: `#buildBootDeps` throws into the degraded boot.
|
|
766
|
-
for (const need of
|
|
800
|
+
for (const need of crashedNeeds)
|
|
767
801
|
if (need !== "base")
|
|
768
802
|
this.#sendBuildStatus(need, {
|
|
769
803
|
generation,
|
|
@@ -589,6 +589,7 @@ describe("dev resource budgets", () => {
|
|
|
589
589
|
// A ceiling the replacement is back over within the interval is reported, and still enforced (recycles are
|
|
590
590
|
// throttled), never disabled for the session. Whether it is back over depends on the machine's allocator, so the
|
|
591
591
|
// warning is asserted only when it is; the decision itself is unit-tested (devHostPolicy).
|
|
592
|
+
const settledFrom = host.markLog();
|
|
592
593
|
for (let i = 1; i <= 3; i++) {
|
|
593
594
|
const { mark } = await harness.editUntilSeen(host, (attempt) =>
|
|
594
595
|
harness.replaceText("ui/ClientMarker.tsx", /marker(-[\w-]+)?/, `marker-settled-${i}-${attempt}`),
|
|
@@ -596,8 +597,14 @@ describe("dev resource budgets", () => {
|
|
|
596
597
|
await host.waitForLogSince(mark, /pages-rebundle ok/, WAIT_MS).catch(() => undefined);
|
|
597
598
|
}
|
|
598
599
|
const settled = await DevStabilityHarness.builderProcess(host.proc.pid);
|
|
599
|
-
|
|
600
|
-
|
|
600
|
+
const warned = /ceiling costs about one boot build per interval/.test(host.logs.join("").slice(start));
|
|
601
|
+
//? Past the interval (a slow machine) the next report recycles instead of warning: either answer settles it.
|
|
602
|
+
if ((settled?.rssBytes ?? 0) >= 200 * MB && !warned)
|
|
603
|
+
await host.waitForLogSince(
|
|
604
|
+
settledFrom,
|
|
605
|
+
/ceiling costs about one boot build per interval|recycling builder pid=\d+|skipped: the builder fell to/,
|
|
606
|
+
WAIT_MS,
|
|
607
|
+
);
|
|
601
608
|
expect(host.logs.join("").slice(start)).not.toMatch(/no longer enforcing it this session/);
|
|
602
609
|
});
|
|
603
610
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akanjs/devkit",
|
|
3
|
-
"version": "3.0.0-beta.
|
|
3
|
+
"version": "3.0.0-beta.26",
|
|
4
4
|
"sourceType": "module",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"@earendil-works/pi-tui": "0.86.1",
|
|
48
48
|
"@inquirer/prompts": "^8.4.3",
|
|
49
49
|
"@tailwindcss/node": "^4.3.0",
|
|
50
|
-
"akanjs": "3.0.0-beta.
|
|
50
|
+
"akanjs": "3.0.0-beta.26",
|
|
51
51
|
"chalk": "^5.6.2",
|
|
52
52
|
"commander": "^14.0.3",
|
|
53
53
|
"dayjs": "^1.11.20",
|