fix: persist reviver no longer corrupts ordinary strings into Dates (TODO 9) - #96
fix: persist reviver no longer corrupts ordinary strings into Dates (TODO 9)#96lopugit wants to merge 2 commits into
Conversation
TODO item 9 (data corruption). The localforage reviver revived ANY string
that passed V8's lenient Date.parse into a Date ("Post 1", "1", "2024",
"March 2024", "5 April"), and the replacer then rewrote them as ISO strings
- permanently corrupting user data after one save/reload cycle.
Dates now persist tagged as {ttype:'date', iso}, mirroring the existing
ttype:'function' scheme, so revival never guesses from string shape. Because
JSON.stringify runs Date.prototype.toJSON before the replacer, the original
Date is read from the holder (this[key]) - the old `value instanceof Date`
branch was unreachable and Dates actually persisted as bare ISO strings.
Backward compatibility: bare strings that exactly match the old replacer's
toISOString() output (YYYY-MM-DDTHH:mm:ss.sssZ) still revive as Dates, so
previously persisted state keeps its Dates. Near-ISO user strings
("2024-03-05", "2024-03-05T12:34", offset timestamps) now stay strings.
Verified: 27-case node test against real flatted 3.4.2 (legacy data, new
roundtrip, circular refs, functions, invalid Dates); live browser check on
the worktree dev server - setThingtime('March 2024') + a real Date both
survive a persist -> reload -> hydrate cycle correctly, zero console errors.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Duplicate-work notice (parallel sessions): PR #94 and PR #96 both fix TODO 9 — they raced before the shared claims protocol ( Where they agree — both revive bare strings only when they exactly match Where they differ — the persist format going forward:
Recommendation: merge #96 (tagged format is strictly more future-proof and matches the TODO's suggested scheme), and cherry-pick #94's nice (Posted by session 1; identical comment on both PRs.) |
…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>
Fixes TODO item 9 — 🐛 DATA CORRUPTION: the persist reviver turns ordinary strings into Dates.
Problem
ThingtimeProvider's localforage reviver revived any string that passed V8's lenientDate.parseinto aDate. Everyday values like"Post 1","1","2024","March 2024","5 April"becameDateobjects on reload; the replacer then rewrote them as ISO strings — permanently corrupting user data after one save/reload cycle (andDateobjects could reach React render paths).Fix
{ttype:'date', iso}, mirroring the existingttype:'function'scheme. Subtlety:JSON.stringify(which flatted wraps) runsDate.prototype.toJSONbefore the replacer, so the replacer's oldvalue instanceof Datebranch was unreachable — Dates were actually persisting as bare ISO strings, which is exactly why revival had to guess. The fix reads the originalDateoff the holder (this[key]), the standard escape hatch for this; flatted 3.4.2 forwards the holder correctly (verified in its source).{ttype:'date'}values, plus — for backward compatibility with already-persisted state — bare strings that exactly match the old replacer'stoISOString()output (YYYY-MM-DDTHH:mm:ss.sssZ). Everything else stays a string.isNaN(getTime())) are never tagged; a tagged value with an unparseableisodegrades to the raw string instead of throwing.Known accepted tradeoff: a user string that exactly matches full
toISOString()format still revives as aDate(required to keep legacy persisted Dates working). Near-ISO strings ("2024-03-05","2024-03-05T12:34","...+02:00") now stay strings.Verification
"Post 1"/"2024"are no longer corrupted; double roundtrip is stable; functions still tag; circular refs still work; invalid Dates don't crash.window.flattedroundtrip checks all pass; end-to-endsetThingtime(['settings','x'], 'March 2024')+ a realDate→ debounced autosave → reload → hydrate: the string survives as a string, the Date as a Date (previously the string came back as aDate, then an ISO string).Note: the
eval-basedttype:'function'revival directly below this code is TODO item 10 (security: eval + CSP) and intentionally untouched here — whoever picks it up should rebase on this since we share the reviver/replacer.🤖 Generated with Claude Code