mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
236 lines
7.6 KiB
TypeScript
236 lines
7.6 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import { open } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { StringDecoder } from "node:string_decoder";
|
|
import { getHeapStatistics } from "node:v8";
|
|
import { withArtifactFileHandle } from "../artifacts/ArtifactFileOperation.js";
|
|
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
|
|
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
|
|
import { jsonParts } from "../domain/jsonSerialization.js";
|
|
import type { IncludedScript } from "../domain/webBundleAnalyzerState.js";
|
|
import {
|
|
webBundleScriptAnalysisSchema,
|
|
type WebBundleScriptAnalysis,
|
|
} from "../domain/webBundleScriptAnalysis.js";
|
|
import { javaScriptRuntime } from "../domain/runtimeVersion.js";
|
|
import { readStableArtifact } from "../artifacts/readStableArtifact.js";
|
|
import {
|
|
WEB_BUNDLE_WORKER_LIMITS as LIMITS,
|
|
includedWebScriptSchema,
|
|
webBundleWorkerRequestSchema,
|
|
webBundleWorkerMessageSchema,
|
|
type WebBundleWorkerMessage,
|
|
} from "./WebBundleAnalysisWorkerProtocol.js";
|
|
|
|
export interface WebBundleScriptAnalyzer {
|
|
(script: IncludedScript): WebBundleScriptAnalysis;
|
|
}
|
|
|
|
class WebBundleWorkerInputError extends Error {}
|
|
class WebBundleWorkerOutputError extends Error {}
|
|
|
|
const send = async (input: WebBundleWorkerMessage): Promise<void> => {
|
|
const value = webBundleWorkerMessageSchema.parse(input);
|
|
const frame = `${JSON.stringify(value)}\n`;
|
|
if (Buffer.byteLength(frame) > LIMITS.protocolBytes)
|
|
throw new RangeError(
|
|
"Web bundle worker control frame exceeds its byte budget",
|
|
);
|
|
await new Promise<void>((resolve, reject) => {
|
|
process.stdout.write(frame, (error) =>
|
|
error === null || error === undefined ? resolve() : reject(error),
|
|
);
|
|
});
|
|
};
|
|
|
|
async function* boundedControlLines(): AsyncGenerator<string> {
|
|
const decoder = new StringDecoder("utf8");
|
|
let pending = "";
|
|
for await (const chunk of process.stdin) {
|
|
const decoded = decoder.write(chunk);
|
|
let start = 0;
|
|
for (;;) {
|
|
const newline = decoded.indexOf("\n", start);
|
|
if (newline < 0) {
|
|
pending += decoded.slice(start);
|
|
if (Buffer.byteLength(pending) > LIMITS.protocolBytes)
|
|
throw new RangeError(
|
|
"Web bundle worker request exceeds its byte budget",
|
|
);
|
|
break;
|
|
}
|
|
const line = `${pending}${decoded.slice(start, newline)}`.replace(
|
|
/\r$/u,
|
|
"",
|
|
);
|
|
pending = "";
|
|
if (Buffer.byteLength(line) > LIMITS.protocolBytes)
|
|
throw new RangeError(
|
|
"Web bundle worker request exceeds its byte budget",
|
|
);
|
|
yield line;
|
|
start = newline + 1;
|
|
}
|
|
}
|
|
pending += decoder.end();
|
|
if (pending !== "") {
|
|
if (Buffer.byteLength(pending) > LIMITS.protocolBytes)
|
|
throw new RangeError("Web bundle worker request exceeds its byte budget");
|
|
yield pending;
|
|
}
|
|
}
|
|
|
|
/** Serve sequential script jobs from the private root owned by the worker runtime. */
|
|
export const serveWebBundleAnalysisWorker = async (
|
|
analyze: WebBundleScriptAnalyzer,
|
|
): Promise<void> => {
|
|
const root = process.cwd();
|
|
const resources = new ArtifactResourceScope();
|
|
const requests = boundedControlLines();
|
|
await send({
|
|
kind: "ready",
|
|
heap_limit_bytes:
|
|
javaScriptRuntime(process.versions).id === "node"
|
|
? getHeapStatistics().heap_size_limit
|
|
: null,
|
|
});
|
|
try {
|
|
for (;;) {
|
|
const next = await requests.next();
|
|
if (next.done) break;
|
|
if (Buffer.byteLength(next.value) > LIMITS.protocolBytes)
|
|
throw new RangeError(
|
|
"Web bundle worker request exceeds its byte budget",
|
|
);
|
|
const raw: unknown = JSON.parse(next.value);
|
|
const request = webBundleWorkerRequestSchema.parse(raw);
|
|
const continueServing = await analyzeJob(
|
|
root,
|
|
request,
|
|
analyze,
|
|
resources,
|
|
);
|
|
if (!continueServing) break;
|
|
if (
|
|
process.memoryUsage().heapUsed >
|
|
getHeapStatistics().heap_size_limit / 2
|
|
)
|
|
global.gc?.();
|
|
}
|
|
} finally {
|
|
await requests.return(undefined);
|
|
}
|
|
};
|
|
|
|
const analyzeJob = async (
|
|
root: string,
|
|
request: ReturnType<typeof webBundleWorkerRequestSchema.parse>,
|
|
analyze: WebBundleScriptAnalyzer,
|
|
resources: ArtifactResourceScope,
|
|
): Promise<boolean> => {
|
|
try {
|
|
const sourcePath = join(root, `${request.id}.source`);
|
|
const source = await readStableArtifact(
|
|
sourcePath,
|
|
LIMITS.sourceBytes,
|
|
undefined,
|
|
undefined,
|
|
resources,
|
|
);
|
|
if (
|
|
source.bytes.length !== request.source.bytes ||
|
|
source.sha256 !== request.source.sha256
|
|
)
|
|
throw new WebBundleWorkerInputError(
|
|
"Web bundle worker source descriptor does not match its file",
|
|
);
|
|
let script: IncludedScript;
|
|
try {
|
|
const decoded: unknown = JSON.parse(
|
|
new TextDecoder("utf-8", { fatal: true }).decode(source.bytes),
|
|
);
|
|
script = includedWebScriptSchema.parse(decoded);
|
|
} catch (cause) {
|
|
throw new WebBundleWorkerInputError(
|
|
`Web bundle worker source is not a valid included script: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
);
|
|
}
|
|
await send({ kind: "phase", id: request.id, phase: "analyze" });
|
|
let result: WebBundleScriptAnalysis;
|
|
try {
|
|
result = webBundleScriptAnalysisSchema.parse(analyze(script));
|
|
} catch (cause) {
|
|
throw new WebBundleWorkerOutputError(
|
|
`Web bundle analyzer returned an invalid result: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
);
|
|
}
|
|
if (result.status !== "analyzed")
|
|
throw new WebBundleWorkerOutputError(
|
|
"Web bundle analyzer did not produce a completed per-script result",
|
|
);
|
|
const resultPath = join(root, `${request.id}.result`);
|
|
const file = await open(resultPath, "wx", 0o600);
|
|
const hash = createHash("sha256");
|
|
let bytes = 0;
|
|
await withArtifactFileHandle(
|
|
resultPath,
|
|
file,
|
|
resources,
|
|
undefined,
|
|
async (handle) => {
|
|
for (const part of jsonParts(result)) {
|
|
const chunk = Buffer.from(part);
|
|
bytes += chunk.length;
|
|
if (bytes > request.result_budget_bytes)
|
|
throw new RangeError(
|
|
"Web bundle worker result exceeds its admitted byte budget",
|
|
);
|
|
hash.update(chunk);
|
|
let offset = 0;
|
|
while (offset < chunk.length) {
|
|
const written = await handle.write(
|
|
chunk,
|
|
offset,
|
|
chunk.length - offset,
|
|
);
|
|
if (written.bytesWritten === 0)
|
|
throw new Error(
|
|
"Web bundle worker result write made no progress",
|
|
);
|
|
offset += written.bytesWritten;
|
|
}
|
|
}
|
|
},
|
|
);
|
|
await send({
|
|
kind: "complete",
|
|
id: request.id,
|
|
result: { bytes, sha256: hash.digest("hex") },
|
|
});
|
|
return true;
|
|
} catch (cause: unknown) {
|
|
const resource = cause instanceof RangeError;
|
|
const input = cause instanceof WebBundleWorkerInputError;
|
|
const fatalCleanupFailure =
|
|
cause instanceof ArtifactReaderFailure &&
|
|
cause.cleanupLeases !== undefined &&
|
|
cause.cleanupLeases.length > 0;
|
|
const rawMessage = cause instanceof Error ? cause.message : String(cause);
|
|
await send({
|
|
kind: "failure",
|
|
id: request.id,
|
|
reason: resource
|
|
? "resource"
|
|
: input
|
|
? "input"
|
|
: cause instanceof WebBundleWorkerOutputError ||
|
|
cause instanceof TypeError
|
|
? "output"
|
|
: "io",
|
|
message:
|
|
rawMessage.length > 2048 ? `${rawMessage.slice(0, 2048)}…` : rawMessage,
|
|
});
|
|
return !fatalCleanupFailure;
|
|
}
|
|
};
|