{"record":{"id":"ae87d6392abac919","repo":"Hmbown/CodeWhale","slug":"saved-habitat-is-invalid-save-a-replay-file-before-replacing","errorCode":null,"errorMessage":"Saved habitat is invalid. Save a replay file before replacing it.","messagePattern":"Saved habitat is invalid\\. Save a replay file before replacing it\\.","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"pet/src/ui/pet.ts","lineNumber":286,"sourceCode":"    accumulator += last ? Math.min(.1, (now - last) / 1000) : 0;\n    while (accumulator >= 1 / 30 && (mode.value === 'live' || !tape.length || seconds < duration())) {\n      world.step(1 / 30, { motion: !motion.checked, sensitivity: 1 }); seconds = world.frame.timeMs / 1000; play(world.voices); accumulator -= 1 / 30;\n    }\n    if ((mode.value === 'live' || !tape.length) && seconds >= duration()) seek.max = String(Math.ceil(seconds / 30) * 30 + 30);\n    draw();\n  }\n  last = now; requestAnimationFrame(animate);\n}\ntry {\n  const response = await fetch('./whale-points.tsv'); if (!response.ok) throw new Error('Whale point asset is unavailable.');\n  points = (await response.text()).trim().split('\\n').map(row => row.trim().split(/\\s+/).map(Number) as [number, number]);\n  if (points.length !== 980 || points.some(p => p.length !== 2 || !p.every(Number.isFinite))) throw new Error('Invalid whale point asset.');\n  try {\n    const saved = await library.getHabitat();\n    if (saved) {\n      savedRevision = saved.revision; const h = saved.habitat;\n      if (h.petPersistenceVersion !== 1 || !Number.isFinite(h.seconds) || h.seconds < 0 || h.seconds > PET_MAX_SECONDS\n        || !['wild', 'demo', 'replay'].includes(h.source) || typeof h.still !== 'boolean' || typeof h.sourceName !== 'string') throw new Error('Saved habitat is invalid. Save a replay file before replacing it.');\n      expressionVersion = h.expressionVersion === undefined ? 1 : h.expressionVersion;\n      if (![1, 2].includes(expressionVersion) || h.checkpoint && (h.checkpoint.sim?.expressionVersion ?? 1) !== expressionVersion) throw new Error('Saved expression version does not match its checkpoint.');\n      const savedWorld = PetWorld.fromRecording(points, { ...h, petReplayVersion: h.petReplayVersion === undefined ? 1 : h.petReplayVersion });\n      if (h.checkpoint && savedWorld.frame.timeMs / 1000 !== h.seconds) throw new Error('Saved pet clock does not match its checkpoint.');\n      tape = h.tape; interactions = [...h.interactions]; mode.value = h.source; motion.checked = h.still || media.matches;\n      source.textContent = h.sourceName;\n      seek.max = String(Math.max(h.source === 'demo' ? 80 : h.source === 'replay' ? 90 : 120, savedWorld.endTimeMs / 1000));\n      if (h.source === 'replay') {\n        mode.querySelector<HTMLOptionElement>('[value=\"replay\"]')!.disabled = false;\n      }\n      message.textContent = 'Restoring the saved habitat…';\n      if (h.checkpoint && h.still === motion.checked) adoptWorld(savedWorld);\n      else await rebuild(h.seconds, undefined, savedWorld.recording(false).start);\n      message.textContent = 'Habitat restored locally. Sound starts only when you enable it.';\n    } else await rebuild();\n    persistenceReady = true; void refreshArchives().catch(() => {});\n  } catch (error) { persistenceFailed = true; get('persistence').textContent = error instanceof Error ? error.message : 'Local persistence is unavailable.'; await rebuild(); }\n  void refreshArchives().catch(() => {});","sourceCodeStart":268,"sourceCodeEnd":304,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/pet/src/ui/pet.ts#L268-L304","documentation":"Thrown while restoring a persisted habitat from IndexedDB when the saved habitat object fails its structural validation: wrong `petPersistenceVersion`, non-finite/negative/over-limit `seconds`, a `source` outside wild/demo/replay, or non-boolean `still` / non-string `sourceName`. The message suggests saving a replay file before overwriting the broken save.","triggerScenarios":"`library.getHabitat()` returns a record written by a different app version, hand-edited, partially migrated (e.g. missing `petPersistenceVersion: 1`), or with `seconds` beyond `PET_MAX_SECONDS`.","commonSituations":"Upgrading from an older persistence schema, corrupted IndexedDB entry, manually constructed or copied habitat objects, cross-browser storage imports.","solutions":["Export a replay file first, then clear the stale IndexedDB habitat so a fresh one is written.","Migrate the saved object to `petPersistenceVersion: 1` with valid `seconds`/`source`/`still`/`sourceName`.","Bump the persistence version and add a migration path if the schema intentionally changed."],"exampleFix":"// before\nconst h = saved.habitat; // legacy object without petPersistenceVersion\n// after\nconst h = { ...saved.habitat, petPersistenceVersion: 1, source: 'wild', still: false, sourceName: 'Wild' };","handlingStrategy":"type-guard","validationCode":"const valid = h && h.petPersistenceVersion === 1 && Number.isFinite(h.seconds) && h.seconds >= 0 && h.seconds <= PET_MAX_SECONDS && ['wild','demo','replay'].includes(h.source) && typeof h.still === 'boolean' && typeof h.sourceName === 'string';","typeGuard":"function isValidHabitat(h: any): h is SavedHabitat {\n  return h?.petPersistenceVersion === 1 && Number.isFinite(h?.seconds) && h.seconds >= 0 && h.seconds <= PET_MAX_SECONDS\n    && ['wild','demo','replay'].includes(h.source) && typeof h.still === 'boolean' && typeof h.sourceName === 'string';\n}","tryCatchPattern":"try { restoreHabitat(saved); } catch (e) { if (e.message.startsWith('Saved habitat is invalid')) { await library.clearHabitat(); startWild(); } }","preventionTips":["Always write the full habitat schema including petPersistenceVersion.","Add a migration step when bumping the persistence version.","Export a replay before clearing or upgrading storage."],"tags":["indexeddb","persistence","validation"],"backgroundTag":"schema-validation-failed","analyzedSha":"433685b2024e7bc4c99e1e2e326bcad39b4d9d65","analyzedAt":"2026-09-15T12:24:24.634Z","contentChangedAt":"2026-09-15T12:24:24.634Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}