610dba78b7
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
38 lines
2.0 KiB
TypeScript
38 lines
2.0 KiB
TypeScript
// Session slimming — the single definition of "shed a Session's bulky,
|
|
// reconstructible fields before it crosses a size-sensitive boundary".
|
|
//
|
|
// Two boundaries consume this, so the rule lives in one place (depends only on
|
|
// @infiplot/types, no engine/client imports, so both the storage layer and the
|
|
// engine transport layer can import it without pulling in each other's deps):
|
|
// - network transport (lib/engineClient.ts) drops voice before POSTing the
|
|
// session to scene/vision/insert-beat — voice is only used by /api/beat-audio.
|
|
// - local persistence (lib/persistence/localStore.ts) drops voice AND the
|
|
// style reference image before writing to IndexedDB.
|
|
|
|
import type { Session } from "@infiplot/types";
|
|
|
|
/** Drop each character's `voice` (the ~160-220KB referenceAudioBase64 + provider
|
|
* fields). The field is destructured out so it's ABSENT from the result rather
|
|
* than serialized as `undefined`. Tolerates a missing `characters` array. */
|
|
export function stripSessionVoices(session: Session): Session {
|
|
return {
|
|
...session,
|
|
characters: (session.characters ?? []).map(({ voice: _voice, ...rest }) => rest),
|
|
};
|
|
}
|
|
|
|
/** The persistence-grade slim: voices stripped (via stripSessionVoices) AND the
|
|
* bulky `styleReferenceImage` removed. Both are reconstructible — voices
|
|
* re-provision on the next /api/scene call, and styleReferenceImage is cosmetic
|
|
* (the engine paints fine without it). Keeps each stored record small regardless
|
|
* of IndexedDB quota headroom. */
|
|
export function slimSession(session: Session): Session {
|
|
// Destructure styleReferenceImage OUT (rather than set it to `undefined`) so
|
|
// it's ABSENT from the result — the same absent-not-undefined invariant as
|
|
// stripSessionVoices. structured-clone (IndexedDB) preserves an own key whose
|
|
// value is `undefined`, which a next-phase sync reconciler probing
|
|
// `'styleReferenceImage' in session` or Object.keys() would misread as present.
|
|
const { styleReferenceImage: _styleRef, ...rest } = stripSessionVoices(session);
|
|
return rest;
|
|
}
|