From cf6e08aec4a738a87402e847cc139d43532a1f16 Mon Sep 17 00:00:00 2001 From: yuanzonghao Date: Sat, 27 Jun 2026 18:36:26 +0800 Subject: [PATCH] fix(persistence): harden coerceEpoch null guard + autosave catch rollback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - coerceEpoch: early-return fallback on null/undefined input (was falling through to new Date(null) → epoch 0, surfacing as 1970-01-01 in the stories list); also unify the tail branch to Number.isFinite for consistency with the number-type fast path - autosave .catch: roll back lastSavedFingerprintRef on unexpected throw so the next session change retries instead of silently marking the content as saved Co-Authored-By: Claude Opus 4.6 --- app/[locale]/play/page.tsx | 9 +++++---- lib/persistence/types.ts | 3 ++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/[locale]/play/page.tsx b/app/[locale]/play/page.tsx index 9f047ab..0604cd8 100644 --- a/app/[locale]/play/page.tsx +++ b/app/[locale]/play/page.tsx @@ -902,10 +902,11 @@ function PlayInner() { lastSavedFingerprintRef.current = ""; } }) - // Defensive: saveStory is contracted never to throw, but if a future edit - // to this callback ever does, an unhandled rejection here would poison the - // chain and freeze ALL subsequent saves. Swallow to keep the chain alive. - .catch(() => {}); + .catch(() => { + if (lastSavedFingerprintRef.current === fingerprint) { + lastSavedFingerprintRef.current = ""; + } + }); }, [session]); useEffect(() => { currentSceneRef.current = currentScene; diff --git a/lib/persistence/types.ts b/lib/persistence/types.ts index 9ea1f56..3f567a0 100644 --- a/lib/persistence/types.ts +++ b/lib/persistence/types.ts @@ -19,12 +19,13 @@ export const STORY_SCHEMA_VERSION = 1; * crosses a storage/serialization boundary and could arrive as a non-number, * guarding against the historical `t.getTime is not a function` white-screen. */ export function coerceEpoch(value: unknown, fallback: number): number { + if (value == null) return fallback; // Number.isFinite (not just !isNaN) so ±Infinity also falls through to the // fallback — new Date(Infinity).getTime() is NaN, not a usable epoch. if (typeof value === "number" && Number.isFinite(value)) return value; const d = value instanceof Date ? value : new Date(value as string | number); const t = d.getTime(); - return Number.isNaN(t) ? fallback : t; + return Number.isFinite(t) ? t : fallback; } /** local-first sync state of a record.