Keep cold Candidate verification cooperative with controller renewals

This commit is contained in:
Timothy J. Aveni
2026-10-12 18:24:38 -07:00
parent 755c008a88
commit a48fccc328
5 changed files with 93 additions and 11 deletions
+47
View File
@@ -1,4 +1,5 @@
import { createHash } from "node:crypto";
import { setImmediate } from "node:timers/promises";
import type { Binding, Conformance, DependencyBinding, PersistentAttachment, WorkspaceRevision } from "./types.js";
import { validateWorkspaceRevision } from "./validation.js";
@@ -21,6 +22,52 @@ export const canonicalJson = (value: unknown): string => {
};
export const contentDigest = (value: unknown) =>
`sha256:${createHash("sha256").update(canonicalJson(value)).digest("hex")}`;
/** Same canonical bytes as contentDigest, without monopolizing a live host's
* event loop while verifying large immutable metadata. No validation is skipped.
* This is for asynchronous loading, not a different build/wire identity. */
export async function contentDigestAsync(value: unknown): Promise<string> {
function* parts(value: unknown): Generator<string> {
if (Array.isArray(value)) {
yield "[";
for (let index = 0; index < value.length; index++) {
if (index) yield ",";
// Match canonicalJson's Array.map behavior for sparse arrays too.
if (index in value) yield* parts(value[index]);
}
yield "]";
} else if (value !== null && typeof value === "object") {
if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)
throw new Error("Expected a plain JSON object");
yield "{";
let first = true;
for (const key of Object.keys(value).sort(compareText)) {
const entry = (value as Record<string, unknown>)[key];
if (entry === undefined) continue;
if (!first) yield ",";
first = false;
yield JSON.stringify(key) + ":";
yield* parts(entry);
}
yield "}";
} else yield canonicalJson(value);
}
const hash = createHash("sha256");
let buffer = "",
sliceStarted = performance.now();
for (const part of parts(value)) {
buffer += part;
if (buffer.length >= 65536) {
hash.update(buffer);
buffer = "";
if (performance.now() - sliceStarted >= 8) {
await setImmediate();
sliceStarted = performance.now();
}
}
}
hash.update(buffer);
return `sha256:${hash.digest("hex")}`;
}
const semantic = (value: unknown): unknown => {
if (Array.isArray(value)) return value.map(semantic);
if (value && typeof value === "object")