Reuse verified immutable plans and streamline cooperative hashing
Continue final hosted development reliability qualification.
This commit is contained in:
@@ -26,43 +26,80 @@ export const contentDigest = (value: unknown) =>
|
||||
* 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");
|
||||
// An explicit stack visits each node once; recursive generator delegation
|
||||
// otherwise forwards every token through every ancestor of a deep plan.
|
||||
type Frame = { kind: "value" | "array" | "object"; value: unknown; index?: number; keys?: string[]; wrote?: boolean };
|
||||
const hash = createHash("sha256"),
|
||||
stack: Frame[] = [{ kind: "value", value }],
|
||||
active = new WeakSet<object>();
|
||||
let buffer = "",
|
||||
sliceStarted = performance.now();
|
||||
for (const part of parts(value)) {
|
||||
sliceStarted = performance.now(),
|
||||
steps = 0;
|
||||
const append = (part: string) => {
|
||||
buffer += part;
|
||||
if (buffer.length >= 65536) {
|
||||
hash.update(buffer);
|
||||
buffer = "";
|
||||
if (performance.now() - sliceStarted >= 8) {
|
||||
await setImmediate();
|
||||
sliceStarted = performance.now();
|
||||
}
|
||||
};
|
||||
while (stack.length) {
|
||||
const frame = stack[stack.length - 1]!;
|
||||
if (frame.kind === "value") {
|
||||
const value = frame.value;
|
||||
if (value !== null && typeof value === "object") {
|
||||
if (active.has(value)) throw new Error("Cannot hash cyclic JSON values");
|
||||
active.add(value);
|
||||
frame.index = 0;
|
||||
if (Array.isArray(value)) {
|
||||
frame.kind = "array";
|
||||
append("[");
|
||||
} else {
|
||||
if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)
|
||||
throw new Error("Expected a plain JSON object");
|
||||
frame.kind = "object";
|
||||
frame.keys = Object.keys(value).sort(compareText);
|
||||
frame.wrote = false;
|
||||
append("{");
|
||||
}
|
||||
} else {
|
||||
append(canonicalJson(value));
|
||||
stack.pop();
|
||||
}
|
||||
} else if (frame.kind === "array") {
|
||||
const array = frame.value as unknown[];
|
||||
if (frame.index === array.length) {
|
||||
append("]");
|
||||
active.delete(array);
|
||||
stack.pop();
|
||||
} else {
|
||||
const index = frame.index!;
|
||||
frame.index = index + 1;
|
||||
if (index) append(",");
|
||||
// Match canonicalJson's Array.map behavior for sparse arrays too.
|
||||
if (index in array) stack.push({ kind: "value", value: array[index] });
|
||||
}
|
||||
} else {
|
||||
const object = frame.value as Record<string, unknown>;
|
||||
if (frame.index === frame.keys!.length) {
|
||||
append("}");
|
||||
active.delete(object);
|
||||
stack.pop();
|
||||
} else {
|
||||
const index = frame.index!;
|
||||
frame.index = index + 1;
|
||||
const key = frame.keys![index]!,
|
||||
entry = object[key];
|
||||
if (entry !== undefined) {
|
||||
if (frame.wrote) append(",");
|
||||
frame.wrote = true;
|
||||
append(JSON.stringify(key) + ":");
|
||||
stack.push({ kind: "value", value: entry });
|
||||
}
|
||||
}
|
||||
}
|
||||
if (++steps % 512 === 0 && performance.now() - sliceStarted >= 8) {
|
||||
await setImmediate();
|
||||
sliceStarted = performance.now();
|
||||
}
|
||||
}
|
||||
hash.update(buffer);
|
||||
|
||||
Reference in New Issue
Block a user