fix(persist): stop lenient Date.parse reviver corrupting ordinary strings (TODO 9) - #98
Closed
lopugit wants to merge 3 commits into
Closed
fix(persist): stop lenient Date.parse reviver corrupting ordinary strings (TODO 9)#98lopugit wants to merge 3 commits into
lopugit wants to merge 3 commits into
Conversation
…ings
Persist Dates as tagged {ttype:'date', iso} objects (read off the replacer
holder, since flatted applies Date.prototype.toJSON before the replacer) and
only revive tagged values or exact toISOString()-shaped strings. Strings like
'Post 1', '2024', 'March 2024' no longer become Dates after a save/reload
cycle. TODO/TODO.md item 9.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
… in autosave) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Owner
Author
lopugit
added a commit
that referenced
this pull request
Jul 30, 2026
…98/#106) Upgrade the persist codec from strict-ISO string revival to the tagged scheme the duplicate TODO-9 PRs used, closing the remaining false-positive: - Real Dates persist as {ttype:'date', iso}. The replacer reads the original off the holder (this[key]) because Date.toJSON converts Dates to strings before the replacer sees the value. - A USER string that merely looks like a full ISO timestamp is escaped as {ttype:'iso-string', s} so the legacy fallback can never capture it — previously such a string became a Date on the next load. - The legacy bare-ISO fallback remains only to migrate pre-tagging persists. - Both replacer and reviver skip their own wrappers' inner keys (without the guard the escape rule re-wraps its own output unboundedly, and the legacy fallback hands the wrapper branches a Date instead of the original string — caught by the new tests). Tests: 9/9 in test:persist, including Date + identical-looking string coexisting, two-cycle string stability, and hostile-payload drops. TESTING.md gains the register rate-limit checklist (from #106) and a persisted-codec checklist. Live-verified: app hydrates under the CSP, persists and reloads cleanly, zero console errors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Owner
Author
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.
Fixes TODO/TODO.md item 9 (data corruption): the localforage persist reviver in
ThingtimeProviderrevived any string passing V8's lenientDate.parseinto aDate— so ordinary values like "Post 1", "1", "2024", "March 2024", "5 April" becameDateobjects on reload, and the replacer then rewrote them as ISO strings, permanently corrupting user data after one save/reload cycle.Fix
{ttype: 'date', iso}objects, mirroring the existingttype: 'function'scheme. It reads the raw value off the holder (this[key]) because flatted — likeJSON.stringify— appliesDate.prototype.toJSONbefore the replacer runs, so the previousvalue instanceof Datebranch was dead code and Dates leaked through as bare ISO strings.{ttype: 'date', iso}values, andtoISOString()shape (YYYY-MM-DDTHH:mm:ss.sssZ) for backward compatibility with previously persisted state.Date.parseare left untouched.Old persisted payloads (bare ISO strings) still revive to Dates; after one save cycle they migrate to the tagged form.
Verification
flattedpackage: buggy behaviour reproduced ("Post 1" →Date(2000-12-31…)), new code round-trips strings/Dates/functions correctly across generations, legacy payloads included.window.flatted(the sameparse/stringifyused by the autosave and load paths); a marker ("Post 1","2024","March 2024", one real Date) planted in the IndexedDBthingtimepayload survived a load → state change → autosave → re-read cycle with strings intact, Date preserved, and the new payload in tagged form. No console errors.flattedcall sites (useThingtimeMachineundo/redo snapshots,UserSettingsModalexport) use raw flatted without the reviver/replacer and are unaffected.🤖 Generated with Claude Code