Summary
When a record is deleted, Legend-State marks it internally with symbolDelete (Symbol('delete')). If the observable is persisted with the IndexedDB plugin, this Symbol reaches IDBObjectStore.put(), which uses the Structured Clone Algorithm and cannot serialise Symbol values. The result is an unrecoverable runtime crash on every persisted deletion:
DataCloneError: Failed to execute 'put' on 'IDBObjectStore': Symbol(delete) could not be cloned.
This makes IndexedDB persistence unusable for any app that deletes records.
Root cause
_setItem in src/persist-plugins/indexeddb.ts guards deletion with a falsy check only:
_setItem(table, key, value, store, config) {
if (!value) {
// ...delete from store
return store.delete(key);
} else {
// ...
return store.put(value); // <- symbolDelete lands here
}
}
symbolDelete is truthy (!symbolDelete === false), so it falls through to store.put(symbolDelete) and throws.
Two call paths reach this with a Symbol value:
- Item-level deletion —
path: ['itemId'], valueAtPath: symbolDelete.
- Full-table replacement (
mode: 'set') — path: [], valueAtPath: { id1: {…}, id2: symbolDelete }. _setTable delegates each entry to _setItem, so id2 crashes.
Reproduction
Any observable persisted with observablePersistIndexedDB where an item is deleted:
- Persist an object map to IndexedDB and let it load.
- Delete one of the entries (which internally sets
symbolDelete on it).
- On the next persist flush,
store.put() throws DataCloneError.
Deterministic minimal repro via the plugin directly:
const plugin = observablePersistIndexedDB({ databaseName, version, tableNames });
await plugin.initialize({ indexedDB: { databaseName, version, tableNames } });
// seed
await plugin.set('items', [{ path: ['id-1'], pathTypes: ['object'], valueAtPath: { id: 'id-1', name: 'Alice' } }], config);
// delete -> throws DataCloneError today
await plugin.set('items', [{ path: ['id-1'], pathTypes: ['object'], valueAtPath: symbolDelete }], config);
Proposed fix
Recognise Symbol values alongside the existing falsy check in _setItem:
- if (!value) {
+ if (!value || typeof value === 'symbol') {
if (this.tableData[table]) {
delete this.tableData[table][key];
}
return store.delete(key);
}
Because _setTable delegates to _setItem, this covers both the item-level and full-table paths.
Note on other persist plugins
The MMKV and AsyncStorage plugins don't crash, but have a related latent gap: their serializers (safeStringify / JSON.stringify) silently drop Symbol values, so a deleted item is written back as an empty object instead of being removed. Worth a follow-up, but out of scope for this issue.
I already have a local branch with the one-line fix plus two regression tests (item-level deletion and full-table replacement — both fail with DataCloneError before the fix, pass after). Happy to open a PR once this is triaged.
Summary
When a record is deleted, Legend-State marks it internally with
symbolDelete(Symbol('delete')). If the observable is persisted with the IndexedDB plugin, this Symbol reachesIDBObjectStore.put(), which uses the Structured Clone Algorithm and cannot serialise Symbol values. The result is an unrecoverable runtime crash on every persisted deletion:This makes IndexedDB persistence unusable for any app that deletes records.
Root cause
_setIteminsrc/persist-plugins/indexeddb.tsguards deletion with a falsy check only:symbolDeleteis truthy (!symbolDelete === false), so it falls through tostore.put(symbolDelete)and throws.Two call paths reach this with a Symbol value:
path: ['itemId'],valueAtPath: symbolDelete.mode: 'set') —path: [],valueAtPath: { id1: {…}, id2: symbolDelete }._setTabledelegates each entry to_setItem, soid2crashes.Reproduction
Any observable persisted with
observablePersistIndexedDBwhere an item is deleted:symbolDeleteon it).store.put()throwsDataCloneError.Deterministic minimal repro via the plugin directly:
Proposed fix
Recognise Symbol values alongside the existing falsy check in
_setItem:Because
_setTabledelegates to_setItem, this covers both the item-level and full-table paths.Note on other persist plugins
The MMKV and AsyncStorage plugins don't crash, but have a related latent gap: their serializers (
safeStringify/JSON.stringify) silently drop Symbol values, so a deleted item is written back as an empty object instead of being removed. Worth a follow-up, but out of scope for this issue.I already have a local branch with the one-line fix plus two regression tests (item-level deletion and full-table replacement — both fail with
DataCloneErrorbefore the fix, pass after). Happy to open a PR once this is triaged.