perf(domain): reuse canonical JSON key encodings
Reuse exact JSON key encodings within the existing 8,192-code-unit digest-buffer quantum and avoid ordinary member value frames. Preserve canonical bytes and cooperative yielding; resume objects when a pending property is omitted. Flameox paired experiments establish a local digest-workload improvement, with complete-result/EvidenceID oracles unchanged. Full CLI acceleration and billed token reductions are not claimed. Refs #1065.
This commit is contained in:
parent
ea06264a56
commit
89ea2cad19
|
|
@ -21,6 +21,12 @@ describe("cooperative canonical JSON digest", () => {
|
|||
),
|
||||
{ payload: `${"x".repeat(8191)}𐀀${"\n\ud800".repeat(20_000)}` },
|
||||
{ [`${"x".repeat(8191)}𐀀${"\n".repeat(20_000)}`]: "value" },
|
||||
Object.fromEntries(
|
||||
Array.from({ length: 512 }, (_, index) => [
|
||||
`escaped\n\"\\𐀀${"x".repeat(32)}${index}`,
|
||||
{ repeated: index },
|
||||
]),
|
||||
),
|
||||
];
|
||||
it.each(values.map((value) => ({ value })))(
|
||||
"preserves complete canonical meaning (%#)",
|
||||
|
|
@ -55,6 +61,18 @@ describe("cooperative canonical JSON digest", () => {
|
|||
expect(steps.next().done).toBe(true);
|
||||
});
|
||||
|
||||
it("continues object hashing when a pending property is omitted", () => {
|
||||
const value: { [key: string]: JsonValue } = {
|
||||
a: "x".repeat(32_768),
|
||||
b: 1,
|
||||
c: { retained: true },
|
||||
};
|
||||
const steps = canonicalJsonDigestSteps(value);
|
||||
expect(steps.next().done).toBe(false);
|
||||
delete value.b;
|
||||
expect(complete(steps)).toBe(digestCanonicalValue(value));
|
||||
});
|
||||
|
||||
it("rejects sparse arrays, nonfinite numbers and cycles", () => {
|
||||
const sparse: JsonValue[] = [];
|
||||
sparse.length = 2;
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ export function* canonicalJsonDigestSteps(
|
|||
const hash = createHash("sha256");
|
||||
const ancestors = new Set<object>();
|
||||
const stack: DigestFrame[] = [{ kind: "value", value }];
|
||||
const encodedKeys = new Map<string, string>();
|
||||
// Keep reusable key encodings within one existing digest-buffer quantum.
|
||||
let remainingKeyCodeUnits = 8192;
|
||||
let buffered = "";
|
||||
let flushed = false;
|
||||
const emit = (part: string): void => {
|
||||
|
|
@ -69,22 +72,22 @@ export function* canonicalJsonDigestSteps(
|
|||
stack.push(frame);
|
||||
continue;
|
||||
}
|
||||
let item: JsonValue;
|
||||
if (frame.kind === "array") {
|
||||
if (frame.index === frame.length) {
|
||||
emit("]");
|
||||
ancestors.delete(frame.value);
|
||||
continue;
|
||||
}
|
||||
const item = frame.value[frame.index];
|
||||
if (item === undefined)
|
||||
const element = frame.value[frame.index];
|
||||
if (element === undefined)
|
||||
throw new TypeError(
|
||||
"Canonical JSON digest requires dense JSON arrays",
|
||||
);
|
||||
if (frame.index++ > 0) emit(",");
|
||||
stack.push(frame, { kind: "value", value: item });
|
||||
continue;
|
||||
}
|
||||
if (frame.kind === "object") {
|
||||
stack.push(frame);
|
||||
item = element;
|
||||
} else if (frame.kind === "object") {
|
||||
if (frame.index === frame.keys.length) {
|
||||
emit("}");
|
||||
ancestors.delete(frame.value);
|
||||
|
|
@ -93,25 +96,42 @@ export function* canonicalJsonDigestSteps(
|
|||
const key = frame.keys[frame.index++];
|
||||
if (key === undefined)
|
||||
throw new TypeError("Canonical JSON object changed during hashing");
|
||||
const item = frame.value[key];
|
||||
if (item === undefined) continue;
|
||||
const property = frame.value[key];
|
||||
if (property === undefined) {
|
||||
stack.push(frame);
|
||||
continue;
|
||||
}
|
||||
if (frame.written++ > 0) emit(",");
|
||||
if (key.length > 8192) {
|
||||
emit('"');
|
||||
stack.push(
|
||||
frame,
|
||||
{ kind: "value", value: item },
|
||||
{ kind: "value", value: property },
|
||||
{ kind: "separator" },
|
||||
{ kind: "string", value: key, index: 0 },
|
||||
);
|
||||
continue;
|
||||
}
|
||||
emit(JSON.stringify(key));
|
||||
let encodedKey = encodedKeys.get(key);
|
||||
if (encodedKey === undefined) {
|
||||
encodedKey = JSON.stringify(key);
|
||||
if (encodedKey.length <= remainingKeyCodeUnits) {
|
||||
encodedKeys.set(key, encodedKey);
|
||||
remainingKeyCodeUnits -= encodedKey.length;
|
||||
}
|
||||
}
|
||||
emit(encodedKey);
|
||||
emit(":");
|
||||
stack.push(frame, { kind: "value", value: item });
|
||||
continue;
|
||||
stack.push(frame);
|
||||
item = property;
|
||||
} else {
|
||||
item = frame.value;
|
||||
}
|
||||
// Resume containers directly; scalar members need no temporary value frame.
|
||||
if (flushed) {
|
||||
flushed = false;
|
||||
yield;
|
||||
}
|
||||
const item = frame.value;
|
||||
if (typeof item === "string" && item.length > 8192) {
|
||||
emit('"');
|
||||
stack.push({ kind: "string", value: item, index: 0 });
|
||||
|
|
@ -151,6 +171,7 @@ export function* canonicalJsonDigestSteps(
|
|||
} finally {
|
||||
stack.length = 0;
|
||||
ancestors.clear();
|
||||
encodedKeys.clear();
|
||||
buffered = "";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue