diff --git a/src/domain/canonicalDigest.test.ts b/src/domain/canonicalDigest.test.ts index 02a641905..51df94619 100644 --- a/src/domain/canonicalDigest.test.ts +++ b/src/domain/canonicalDigest.test.ts @@ -62,6 +62,13 @@ describe("incremental canonical digest", () => { expect(digestCanonicalValue(value)).toBe(legacyDigest(value)); }); + it("keeps digests identical when buffered parts cross flush boundaries", () => { + const manyKeys: Record = {}; + for (let i = 0; i < 2000; i += 1) manyKeys[`k${i}`] = i; + const value = { keys: manyKeys, tail: ["€", "😀", "x".repeat(9000)] }; + expect(digestCanonicalValue(value)).toBe(legacyDigest(value)); + }); + it("preserves canonicalize property access and toJSON cycle behavior", () => { const createAccessor = () => { let reads = 0; diff --git a/src/domain/canonicalDigest.ts b/src/domain/canonicalDigest.ts index c9f1c6260..3c4f9e3c6 100644 --- a/src/domain/canonicalDigest.ts +++ b/src/domain/canonicalDigest.ts @@ -6,8 +6,19 @@ export const digestCanonicalValue = ( context = "Comparison", ): string => { const hash = createHash("sha256"); - const encoded = emitCanonical(value, (part) => hash.update(part), new Set()); + // Batch small emitted parts to cut hash.update crossings. + // Flush cuts only at part boundaries, so the hashed byte stream is unchanged. + let buffered = ""; + const emit = (part: string): void => { + buffered += part; + if (buffered.length >= 8192) { + hash.update(buffered); + buffered = ""; + } + }; + const encoded = emitCanonical(value, emit, new Set()); if (!encoded) throw new TypeError(`${context} could not canonicalize data`); + if (buffered) hash.update(buffered); return hash.digest("hex"); };