I have two Y.Doc I am trying to keep in sync. In the example below, written as a test, session1 will initiate all changes and session2 will be syncing to it.
If session1 updates an element and then deletes it within the span of UndoManager.captureTimeout
, this becomes one undo step as expected. However, calling undo will cause session2 to lose data while session1’s data is restored correctly.
import * as Y from 'yjs'
/** Utility class to quickly access a doc, its top level data, associated undo, and grab its JSON representation. */
class Session {
yDoc = new Y.Doc()
yArray = this.yDoc.getArray('top') as Y.Array<Y.Map<number>>
undoManager = new Y.UndoManager(this.yArray, { trackedOrigins: new Set(['local']) })
getJSON = () => this.yArray.toJSON()
}
/** Model to start with */
const originalModel = [{ X: 1, Y: 1, Z: 0 }]
/** Returns true if vertices exist and some are missing a key. */
const hasCorruptVertices = (json: Array<any>) => {
return json.some((v: any) => v.X === undefined || v.Y === undefined || v.Z === undefined)
}
/** Expect that the JSON matches, we have no missing changes, and that there are no corrupt vertices. */
const expectMatch = (sessions: Session[], expected: any) => {
for (const session of sessions) {
expect(session.yDoc.store.pendingStructs?.missing).toBe(undefined)
const json = session.getJSON()
const isCorrupted = hasCorruptVertices(json)
expect(isCorrupted).toBe(false)
expect(json).toEqual(expected)
}
}
describe('Y.UndoManager Example', () => {
it('will ❌ the synced doc if delete the element in the same undo step as an update', async () => {
// Create two sessions that stay in sync.
const session1 = new Session()
const session2 = new Session()
session1.yDoc.on('update', (update) => {
session2.yDoc.transact(() => {
Y.applyUpdate(session2.yDoc, update)
}, 'remote')
})
session2.yDoc.on('update', (update) => {
session1.yDoc.transact(() => {
Y.applyUpdate(session1.yDoc, update)
}, 'remote')
})
// Load model into session1
originalModel.forEach((v: { X: number, Y: number, Z: number }) => {
const vertex = new Y.Map<number>()
vertex.set('X', v.X)
vertex.set('Y', v.Y)
vertex.set('Z', v.Z)
session1.yArray.push([vertex])
})
// Expect that the model is correct in both sessions
expectMatch([session1, session2], originalModel)
// Update the vertex in session1
session1.yDoc.transact(() => {
const vertex = session1.yArray.get(0)
vertex.set('X', 10)
}, 'local')
// Delete the parent of the vertex in session1
session1.yDoc.transact(() => {
session1.yArray.delete(0)
}, 'local')
// We expect that the two transactions above collapsed into one undo
expect(session1.undoManager.undoStack.length).toBe(1)
// Expect that the model in both sessions are updated to reflect the result of modification + deletion, which means deletion
expectMatch([session1, session2], [])
// Undo the change
session1.undoManager.undo()
// Expect that the model in session 1 is back to its original state
expectMatch([session1], originalModel)
// We would want the second model to also match the original model, but instead it looks like this:
// [{ Y: 1, Z: 0 }]
expect(hasCorruptVertices(session2.getJSON())).toBe(true)
})
})