Skip to content

fix(data): exclude nonPersistent resources from store serialization#141

Merged
krisnye merged 6 commits into
mainfrom
krisnye/fix-ephemeral-resources
Jul 13, 2026
Merged

fix(data): exclude nonPersistent resources from store serialization#141
krisnye merged 6 commits into
mainfrom
krisnye/fix-ephemeral-resources

Conversation

@krisnye

@krisnye krisnye commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Store.toData() serialized every archetype unconditionally, including archetypes that hold nonPersistent resources/entities (the negative-ID entity space).
  • entityLocationTableData only ever restores the persistent location table, so serialized nonPersistent archetype rows had no entity to attach to on load — but the raw resource values were still leaking into every snapshot.
  • Filtered nonPersistent archetypes out of archetypesData in createCore's toData, so nonPersistent: true (and deprecated ephemeral: true) resources are correctly excluded from persisted/synced state, matching the documented contract.

Test plan

  • Added a regression test in create-store.test.ts asserting a nonPersistent resource's archetype is absent from toData() output, and that loading that snapshot into a fresh store leaves the nonPersistent resource at its default while the persistent resource restores correctly.
  • pnpm run lint, pnpm run typecheck, pnpm run build, pnpm run test all pass across the monorepo.

🤖 Generated with Claude Code

krisnye and others added 6 commits July 10, 2026 10:28
Store.toData() serialized every archetype unconditionally, including
archetypes holding nonPersistent resources/entities (negative-ID space).
Since entityLocationTableData only ever covers the persistent location
table, this leaked nonPersistent resource values into snapshots even
though the corresponding entities could never be restored to point at
them. Filter nonPersistent archetypes out of archetypesData so
nonPersistent resources correctly stay out of serialized state.

Bump version to 0.9.77.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…rom serialization

The previous commit dropped nonPersistent archetypes from Store.toData()
entirely. Archetype ids are dense array indices stored by index in the
persistent location table, so omitting a nonPersistent archetype that
precedes a persistent one shifted every later id on reload, leaving
persistent entities pointing at the wrong (or a missing) archetype.

Instead keep every archetype's slot to preserve ids, but serialize
nonPersistent (and deprecated ephemeral) archetypes as a data-free stub
of component names. fromData recreates the empty archetype at the same
id and restores rows only for persistent archetypes. Adds a regression
test covering the id-aliasing case.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ral alias

Replaces the stub-based fix with a cleaner, uniform serialization model and
removes the dead `ephemeral` naming entirely (breaking change).

Serialization: every archetype serializes as `{ componentNames, data? }`.
`data` is present only for persistent archetypes; nonPersistent (negative-ID
space) archetypes serialize identity only. This preserves the dense archetype
id (referenced by value in the persistent location table) without persisting
session-only rows, and collapses the read/write decisions to O(1):
`components.has("nonPersistent")` on write and `if (data)` on read — no
component scanning. Verified end-to-end through the serialize/deserialize
codec (the real bug: a nonPersistent resource previously decoded to
`undefined`).

Drop `ephemeral`: removed the `ephemeral` schema flag, the `ephemeral`
component alias, `Entity.isEphemeral`, and every `?? schema.ephemeral`
fallback. Only `nonPersistent` remains. Renamed is-ephemeral.ts →
is-non-persistent.ts and updated ECS/sync docs. Existing snapshots that
relied on the old format or the ephemeral flag are not compatible.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…oads

Adds ECS_SNAPSHOT_VERSION, stamped into every toData() snapshot and checked
by fromData(), which now throws on a version mismatch instead of silently
mis-reconstructing. This makes the nonPersistent/ephemeral format break
explicit: legacy snapshots (no version field) and any future-version
snapshot are rejected with a clear error.

The version guard covers the whole db/store/core fromData chain; the one
internal caller that builds a payload directly (data-persistence's
finalizeEntityLocationTable) stamps the current version. The incremental
journal format is otherwise unaffected. Documents the break in the README.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fromData now logs a console.warn and returns without loading when the
snapshot version does not match (legacy/unversioned or future), keeping the
freshly-constructed state. Callers such as createStoragePersistenceService
treat an unloadable snapshot as "no saved data" rather than surfacing an
error. Updates the regression test and README accordingly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… the delete sentinel

Undoing an update that added a column (pre-image undefined) recorded the undo
op's value as the internal DELETE sentinel. On replay the sentinel string was
written into the column verbatim instead of removing it — the mirror of the
redo-of-column-removal bug fixed in #140 (FFP-98612), and blocking FFP-98613.

Root cause: recorded ops carry DELETE to mean "remove this column", but the
apply path handed op values straight to store.update, which wrote the string.
Fix resolves DELETE -> undefined at the single point recorded ops re-enter a
store (applyOperations), on a *copy* — the core store deletes undefined keys
from the object it is given, so mutating the shared op would empty it and make
a second undo/redo a no-op. Rollback now reuses applyOperations (removing the
duplicate applyWriteOperations), so a failed transaction that added a column
also rolls back to the no-column state.

Tests: undo-of-column-ADD round-trips to undefined (incl. undo -> redo -> undo),
and rollback of a failed column-ADD removes the column.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@krisnye krisnye merged commit b002aa1 into main Jul 13, 2026
4 checks passed
@krisnye krisnye deleted the krisnye/fix-ephemeral-resources branch July 13, 2026 18:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant