fix(utils): make hash32 total instead of throwing - #12
Merged
Merged
Conversation
hash32 is a bucket key for runtime data, not a serialization, yet it inherited stableStringify's strict JSON domain and threw UnsupportedSerializationTypeError for undefined at any depth, non-finite numbers, bigint, functions, symbols, sparse arrays, accessors, class instances, Map/Set, Blob/File and cycles. That is the wrong contract for this function. Callers key caches, rows and groups by a hash of caller-supplied data, where an undefined field is ordinary and where nothing needs to be recovered from the key. Losing the whole operation to one such field pushed every consumer into writing the same wrapper around this function; a downstream Angular library hit 105 failing specs from the strictness alone before adding one. A value inside the strict domain is still hashed straight through stableStringify, so every key that exists today is unchanged. That matters beyond compatibility: these keys reach persistent storage, and switching the encoding wholesale would silently orphan stored data. Only when the strict attempt throws is the value re-hashed from a copy in which each rejected value is replaced by its own namespaced placeholder, so those inputs hash deterministically and still differ from one another. Two details the substitution has to get right: a sparse hole stays distinct from a real undefined, and Array.prototype.map skips holes while copying them into its result, so the indices are walked explicitly or the array stays sparse and is rejected again. Accessors are never invoked. stableStringify and canonicalStringify are unchanged and still reject, so callers using them to validate that data is serializable keep that signal. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
hash32is a bucket key for runtime data, not a serialization, yet it inheritedstableStringify's strict JSON domain. It threwUnsupportedSerializationTypeErrorforundefinedat any depth, non-finite numbers,bigint, functions, symbols, sparse arrays, accessors, class instances,Map/Set,Blob/Fileand cycles.That is the wrong contract for this function. Callers key caches, rows and groups by a hash of caller-supplied data, where an
undefinedfield is ordinary and where nothing needs to be recovered from the key. Losing the whole operation to one such field pushes every consumer into writing the same wrapper.Measured downstream: bumping
@sd-angular/corefrom1.1.4to1.2.1turned 105 specs red on the strictness alone, across select, autocomplete, the API service, upload and the table group/tree/filter paths. The dominant causes were$.value: undefined(78),$.body: undefined(60),$.filter.externalFilterPerRow: undefined(54),$.checksum: undefined(41),$: File(13) and$.click: function(11). Fixing that in the consumer meant a local adapter plus 43 call-site edits — work every other consumer would repeat.Change
A value inside the strict domain is still hashed straight through
stableStringify, so every key that exists today is unchanged. That matters beyond compatibility: these keys reach persistent storage, and switching the encoding wholesale would silently orphan stored data.Only when the strict attempt throws is the value re-hashed from a copy in which each rejected value is replaced by its own namespaced placeholder (
@sdcorejs/hash/v1:…), so those inputs hash deterministically and still differ from one another.Two details the substitution has to get right:
undefined;Array.prototype.mapskips holes while copying them into its result, so the indices are walked explicitly, otherwise the array stays sparse and is rejected again.Accessors are never invoked while hashing.
Not changed
stableStringifyandcanonicalStringifystill reject. Callers using them to validate that data is serializable keep that signal, and the canonical domain keeps its collision-safety guarantee. Only the explicitly collision-prone bucket hash becomes total.Verification
npm run validate— exit 0 (typecheck, coverage, build, publint, attw, package validation)npm run validate:site— exit 0undefined, determinism, non-invocation of accessors, and that the strict serializers still throw🤖 Generated with Claude Code