In the app, the first step below is what freeThoughts does to a thought's child once more than 500 thoughts are in memory, and the second is any New Subthought under that thought before the pull queue has restored the child. That timing is not reliably reachable by hand, so the steps are given at the reducer level, where they reproduce every time.
Steps to Reproduce
- Deallocate
b from memory without deleting it, as freeThoughts does: deleteThought({ pathParent: [a], thoughtId: b, local: false, remote: false }). This leaves a marked pending with b and c still in its childrenMap, and b out of the thoughtIndex.
- New Subthought (CmdEnter, or swipe
→↓→) under a with the value d: newThought({ at: [a], insertNewSubthought: true, value: 'd' }).
Current Behavior
- The console warns
Sibling <id of b> with missing thought found while creating new thought d (<id of d>).
a.childrenMap holds only c and d, and a.pending is false.
b is not rendered, and it stays hidden until the app is reloaded.
Expected Behavior
In this case
After step 2, a.childrenMap should hold b, c, and d, a should still be pending, and no warning should be logged. b should reappear once the pull queue pulls a again.
In general
Creating a thought under a pending parent should leave the parent's existing childrenMap entries and its pending flag intact, whether the missing children were deallocated by freeThoughts or left unfetched at the buffer depth by fetchDescendants, so that the pull queue can restore them.
Notes
b is intact in storage. The TreeCRDT write path stores only the thought payload and derives childrenMap on read, so this is a visibility defect rather than data loss.
- The sweep in
createThought that drops the missing children dates from 63873b4, when YJS persisted thoughts non-atomically and an interrupted import could leave a parent referencing an unsaved child. Under TreeCRDT a create is a single atomic insert, so that failure can no longer occur.
- The same lines evaluate
childrenMapKey against the map before the sweep, so when the sweep drops a stale =note entry the new =note child is keyed by id and findDescendant(state, parent, '=note') cannot find it.
In the app, the first step below is what
freeThoughtsdoes to a thought's child once more than 500 thoughts are in memory, and the second is any New Subthought under that thought before the pull queue has restored the child. That timing is not reliably reachable by hand, so the steps are given at the reducer level, where they reproduce every time.Steps to Reproduce
bfrom memory without deleting it, asfreeThoughtsdoes:deleteThought({ pathParent: [a], thoughtId: b, local: false, remote: false }). This leavesamarkedpendingwithbandcstill in itschildrenMap, andbout of thethoughtIndex.→↓→) underawith the valued:newThought({ at: [a], insertNewSubthought: true, value: 'd' }).Current Behavior
Sibling <id of b> with missing thought found while creating new thought d (<id of d>).a.childrenMapholds onlycandd, anda.pendingisfalse.bis not rendered, and it stays hidden until the app is reloaded.Expected Behavior
In this case
After step 2,
a.childrenMapshould holdb,c, andd,ashould still bepending, and no warning should be logged.bshould reappear once the pull queue pullsaagain.In general
Creating a thought under a pending parent should leave the parent's existing
childrenMapentries and itspendingflag intact, whether the missing children were deallocated byfreeThoughtsor left unfetched at the buffer depth byfetchDescendants, so that the pull queue can restore them.Notes
bis intact in storage. The TreeCRDT write path stores only the thought payload and deriveschildrenMapon read, so this is a visibility defect rather than data loss.createThoughtthat drops the missing children dates from 63873b4, when YJS persisted thoughts non-atomically and an interrupted import could leave a parent referencing an unsaved child. Under TreeCRDT a create is a single atomic insert, so that failure can no longer occur.childrenMapKeyagainst the map before the sweep, so when the sweep drops a stale=noteentry the new=notechild is keyed by id andfindDescendant(state, parent, '=note')cannot find it.