Kai ki
ff12b2759f
feat(persistence): bidirectional local/cloud story sync (Supabase)
...
Connect the previously-skeleton cloudStore to the client with a full
bidirectional reconcile engine. Commercial build (AUTH_ENABLED) only; the
open-source build is byte-for-byte unchanged — all cloud paths short-circuit
when AUTH_ENABLED is false.
- cloudSync.ts: reconcile engine — decideAction (pure, LWW rev->updatedAt with
tombstone priority) + syncOnLogin/pushOnSave/pushDeletion (best-effort,
serialized, isAuthed-gated)
- cloudSyncClient.ts: browser fetch bridge (short-circuit + fault-tolerant)
- /api/stories/{manifest,pull,push,delete}: RLS-guarded sync endpoints
- upsert_story_if_newer RPC: optimistic concurrency (SECURITY INVOKER,
auth.uid() injection, rev->updated_at guard, revoked from public)
- cloudStore: +manifest/pullBlobs, save->RPC {stored,won}, softDelete w/ rev
- localStore: +listAllRecordsForSync/putSyncedRecord/markRecordSynced
(concurrency-guarded sync writes); types: +StorySyncMeta/StorySyncEnvelope
- facade + UserChip: inject pushOnSave/pushDeletion + login-triggered reconcile
Sync model: full reconcile on login + background push on save (no Realtime;
eventual consistency). Conflict resolution: last-write-wins.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com >
2026-06-28 11:20:47 +08:00
Kai ki
da74e3e763
fix(persistence): address PR review feedback (4 low-cost improvements)
...
From PR #114 external review agent — adopted the real, low-cost findings;
remaining items (false positives / design trade-offs) explained in PR replies:
- coerceEpoch: !Number.isNaN → Number.isFinite — reject ±Infinity, which
previously slipped through and produced Invalid Date via new Date(Infinity)
- enforceRetentionCap pass2/pass3: decrement overflow only when idbDelete
actually succeeds — a failed best-effort delete no longer under-evicts
- cloudListStories: explicit column list instead of select() — avoid pulling
the bulky session_jsonb when only metadata is needed
- Supabase stories: composite primary key (user_id, id) + onConflict user_id,id
— avoid a cross-user Session.id collision rejecting the second user's save
(skeleton not yet deployed, so the migration is edited in place)
typecheck + build:cf green.
2026-06-25 19:20:55 +08:00
Kai ki
610dba78b7
feat(persistence): local-first story persistence (IndexedDB + Supabase skeleton)
...
Remove Cloudflare D1 entirely (4 API routes, lib/db/, Drizzle config/migrations,
drizzle-orm/drizzle-kit deps, wrangler D1/R2/KV bindings) and replace with
browser-local-first architecture:
Open-source build (IndexedDB, no auth):
- lib/persistence/ 5-file module: types, idb adapter (zero-dep, fault-tolerant,
post-open invalidation retry), localStore (CRUD + sync-reserved metadata +
slim/rebuild + retention-cap eviction with tombstone reap + sync-state
protection + last-resort bounded fallback), sessionSlim (voice strip +
styleRef absent-delete), cloudStore (Supabase skeleton, server-only)
- Autosave: persistence fingerprint (history.length:lastBeatCount:playerName),
serial saveChain, failure rollback retry, replaySourceRef guard (prevents
replayed shared stories from clobbering user saves)
- clientStoryPersistence.ts: thin facade (SaveResult discriminated union)
- Stories page: /[locale]/stories with 3-language i18n (zh-CN/en/ja)
- Homepage: book icon entry point in header
Commercial build (Supabase, skeleton only):
- Single table public.stories (JSONB + RLS 4 policies on auth.uid()=user_id)
- supabase/migrations/ DDL (idempotent)
- cloudStore.ts server-only repository, AUTH_ENABLED short-circuit
- Not wired to client this phase
Featured stories: pure fallback (buildFallbackCards + localizeCards), no D1
Includes fixes from 3 rounds of subagent code-review (tasks 16-30):
- CR1: autosave restructure, coerceOrientation, D1 comment cleanup
- CR2: fingerprint+serial+rollback+replay guard, idb post-open retry,
enforceRetentionCap latent defense, sessionSlim absent invariant
- CR3: single-scene share guard (replaySourceRef), insert-beat fingerprint
(beats.length), pass3 overflow double-count fix, detach gate unification
2026-06-25 18:19:08 +08:00