You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Since #80, a change with an unknown $type is stored and synced onward as an OpaqueChange, but SnapshotWorker skips applying it. Snapshots and projected tables are then silently missing its effect, and:
When the app later registers the type, nothing notices. The stored JSON would now deserialize concretely, but snapshots are never re-folded, so the stale state persists forever.
Some types will never become known (a different app writing to the same project). That's a legitimate steady state, but a client currently can't even answer "is my materialized state incomplete?", which matters for consumers that write materialized state into other systems (FwHeadless → FieldWorks).
If the first change to an entity is opaque, no snapshot exists, so later known changes to that entity are skipped too.
Recovery itself is the easy part: the commit log is complete everywhere (opaque JSON round-trips verbatim), so RegenerateSnapshots() already repairs state once the type is registered. What's missing is detection, and ideally a cheaper replay scope than "everything".
One idea: an UnappliedChangeTypes table
One aggregate row per unknown discriminator, written at commit ingest (CrdtRepository.AddCommits, same transaction): TypeName (PK), EarliestCommitId, ChangeCount.
Detection = intersect TypeNames with registered discriminators at project open. Non-empty: replay from the predecessor of the earliest affected commit (the existing DeleteStaleSnapshots + UpdateSnapshots path), then delete the recovered rows.
Never-known types never intersect; their rows sit inert and double as a cheap "this project has changes I can't apply" signal for UI or sync guards.
Existing DBs backfill with one json_extract(Change, '$."$type"') scan over ChangeEntities.
Ingest is deliberate: the converter also runs on read paths, and the SnapshotWorker skip sites also fire for ordinary out-of-order arrivals, so ingest is the only place that sees each commit exactly once.
Alternatives
Persist nothing; fingerprint the registered type set. On growth, scan history for the new types; regenerate if found.
Pro: zero bookkeeping, fully retroactive.
Con: a scan every release that adds a type; "am I incomplete right now?" also costs a scan; the registered set can differ per host (e.g. conditional AddRemoteResourceEntity), so set comparison needs care.
Con: unbounded in the never-known steady state; no benefit over the aggregate, since replay is scoped by earliest commit, not per change; only records skips after it ships, so it needs the backfill anyway.
Regenerate whenever the registered type set changes.
Pro: no detection code.
Con: full replay after nearly every release; RegenerateSnapshots is currently unlocked, non-transactional, and slow on big projects, so everyone pays for a rare event.
Do nothing; rely on the manual regenerate troubleshooting action.
Pro: zero code.
Con: staleness is silent, so nobody knows to press the button; FwHeadless would sync stale state indefinitely.
Notes
Whichever way this goes, it should land in the same release as Tolerate unknown IChange $type via PeekThenConcreteChangeConverter #80 or close behind: builds that skip changes without recording anything leave stale snapshots that later detection can't tell from a clean DB without the backfill scan.
Auto-regeneration should wait until RegenerateSnapshots is locked, transactional, and fast enough. Detection plus a visible warning is already useful on its own.
[Claude, autonomous]
Problem
Since #80, a change with an unknown
$typeis stored and synced onward as anOpaqueChange, butSnapshotWorkerskips applying it. Snapshots and projected tables are then silently missing its effect, and:Recovery itself is the easy part: the commit log is complete everywhere (opaque JSON round-trips verbatim), so
RegenerateSnapshots()already repairs state once the type is registered. What's missing is detection, and ideally a cheaper replay scope than "everything".One idea: an
UnappliedChangeTypestableOne aggregate row per unknown discriminator, written at commit ingest (
CrdtRepository.AddCommits, same transaction):TypeName(PK),EarliestCommitId,ChangeCount.TypeNames with registered discriminators at project open. Non-empty: replay from the predecessor of the earliest affected commit (the existingDeleteStaleSnapshots+UpdateSnapshotspath), then delete the recovered rows.json_extract(Change, '$."$type"')scan overChangeEntities.Ingest is deliberate: the converter also runs on read paths, and the
SnapshotWorkerskip sites also fire for ordinary out-of-order arrivals, so ingest is the only place that sees each commit exactly once.Alternatives
Persist nothing; fingerprint the registered type set. On growth, scan history for the new types; regenerate if found.
AddRemoteResourceEntity), so set comparison needs care.Per-skip rows (CommitId, ChangeIndex, TypeName, EntityId).
Regenerate whenever the registered type set changes.
RegenerateSnapshotsis currently unlocked, non-transactional, and slow on big projects, so everyone pays for a rare event.Do nothing; rely on the manual regenerate troubleshooting action.
Notes
RegenerateSnapshotsis locked, transactional, and fast enough. Detection plus a visible warning is already useful on its own.Context: sillsdev/languageforge-lexbox#2482 (review discussion that prompted this).