fix: root level children update

This commit is contained in:
Eric Maciel 2021-01-20 20:28:52 -05:00
parent a887e26391
commit b5bd7fe750
5 changed files with 100 additions and 22 deletions

View File

@ -5,7 +5,6 @@ interface TestDoc {
status: string status: string
} }
// TODO: delete this?
describe('old state error replication', () => { describe('old state error replication', () => {
const clientDocSet = new Automerge.DocSet() const clientDocSet = new Automerge.DocSet()
const serverDocSet = new Automerge.DocSet() const serverDocSet = new Automerge.DocSet()

View File

@ -23,11 +23,36 @@ const opSet = (op: Automerge.Diff, [map, ops]: any, doc: any) => {
const { link, value, path, obj, key } = op const { link, value, path, obj, key } = op
try { try {
// no slate op needed for root key cursor updates // We can ignore any root level cursor updates since those
// will not correspond to any slate operations
if (obj === rootKey && key === 'cursors') { if (obj === rootKey && key === 'cursors') {
return [map, ops] return [map, ops]
} }
// Handle updates received for the root children array
if (obj === rootKey && key === 'children' && map[value]) {
// First remove all existing child nodes
for (let i = doc.children.length - 1; i >= 0; i--) {
ops.push((map: any) => ({
type: 'remove_node',
path: [i],
node: doc.children[i]
}))
}
// Then add all the newly defined nodes
const newChildren: Node[] = map[value]
newChildren.forEach((child, index) => {
ops.push((map: any) => ({
type: 'insert_node',
path: [index],
node: child
}))
})
return [map, ops]
}
if (path && path[0] !== 'cursors') { if (path && path[0] !== 'cursors') {
ops.push(setDataOp(op, doc)) ops.push(setDataOp(op, doc))
} else if (map[obj]) { } else if (map[obj]) {

View File

@ -137,13 +137,15 @@ export const AutomergeConnector = {
e.handleError(err, { e.handleError(err, {
type: 'applyOperation - toSlateOp', type: 'applyOperation - toSlateOp',
operations, operations,
current: Automerge.save(current) current: Automerge.save(current),
updated: Automerge.save(updated)
}) })
} }
e.isRemote = true e.isRemote = true
Editor.withoutNormalizing(e, () => { Editor.withoutNormalizing(e, () => {
try {
if (HistoryEditor.isHistoryEditor(e) && !preserveExternalHistory) { if (HistoryEditor.isHistoryEditor(e) && !preserveExternalHistory) {
HistoryEditor.withoutSaving(e, () => { HistoryEditor.withoutSaving(e, () => {
slateOps.forEach((o: Operation) => e.apply(o)) slateOps.forEach((o: Operation) => e.apply(o))
@ -151,6 +153,15 @@ export const AutomergeConnector = {
} else { } else {
slateOps.forEach((o: Operation) => e.apply(o)) slateOps.forEach((o: Operation) => e.apply(o))
} }
} catch (err) {
e.handleError(err, {
type: 'applyOperation - slateOps apply',
operations,
slateOps,
current: Automerge.save(current),
updated: Automerge.save(updated)
})
}
e.onCursor && e.onCursor(updated.cursors) e.onCursor && e.onCursor(updated.cursors)
}) })
@ -180,7 +191,7 @@ export const AutomergeConnector = {
if (!doc) return if (!doc) return
const changed = Automerge.change<SyncDoc>(doc, (d: any) => { const changed = Automerge.change<SyncDoc>(doc, (d: any) => {
delete d.cursors d.cursors = {}
}) })
e.docSet.setDoc(docId, changed as any) e.docSet.setDoc(docId, changed as any)

View File

@ -1,4 +1,4 @@
import Automerge, { Frontend } from 'automerge' import Automerge from 'automerge'
import { createServer } from 'http' import { createServer } from 'http'
import fs from 'fs' import fs from 'fs'
import isEqual from 'lodash/isEqual' import isEqual from 'lodash/isEqual'
@ -60,9 +60,17 @@ describe('automerge editor client tests', () => {
}) })
const createCollabEditor = async ( const createCollabEditor = async (
editorOptions: AutomergeOptions & SocketIOPluginOptions = options editorOptions?: Partial<AutomergeOptions> & Partial<SocketIOPluginOptions>
) => { ) => {
const editor = withIOCollaboration(createEditor(), editorOptions) // Given a docId we an generate the collab url
if (editorOptions?.docId) {
editorOptions.url = `http://localhost:5000${editorOptions?.docId}`
}
const editor = withIOCollaboration(createEditor(), {
...options,
...editorOptions
})
const oldReceiveDocument = editor.receiveDocument const oldReceiveDocument = editor.receiveDocument
const promise = new Promise<void>(resolve => { const promise = new Promise<void>(resolve => {
@ -188,8 +196,8 @@ describe('automerge editor client tests', () => {
editor2.destroy() editor2.destroy()
}) })
it('deep nested tree error', () => { it('should not throw deep nested tree error', () => {
// Ready from our test json file for the deep tree error // Read from our test json file for the deep tree error
// This allows us to easily reproduce real production errors // This allows us to easily reproduce real production errors
// and create test cases that resolve those errors // and create test cases that resolve those errors
const rawData = fs.readFileSync( const rawData = fs.readFileSync(
@ -205,6 +213,26 @@ describe('automerge editor client tests', () => {
toSlateOp(operations, currentDoc) toSlateOp(operations, currentDoc)
}) })
it('should update children for a root level children operation', async () => {
const editor = await createCollabEditor()
const oldDoc = collabBackend.backend.documentSetMap[docId].getDoc(docId)
const newDoc = Automerge.change(oldDoc, changed => {
// @ts-ignore
changed.children = [
{ type: 'paragraph', children: [{ text: 'new' }] },
{ type: 'paragraph', children: [{ text: 'nodes' }] }
]
})
collabBackend.backend.documentSetMap[docId].setDoc(docId, newDoc)
await waitForCondition(() => editor.children.length === 2)
expect(editor.children.length).toEqual(2)
expect(Node.string(editor.children[0])).toEqual('new')
expect(Node.string(editor.children[1])).toEqual('nodes')
})
afterAll(() => { afterAll(() => {
collabBackend.destroy() collabBackend.destroy()
server.close() server.close()

View File

@ -16,8 +16,16 @@ const useCursor = (
useEffect(() => { useEffect(() => {
e.onCursor = (data: Cursors) => { e.onCursor = (data: Cursors) => {
if (!mountedRef.current) return if (!mountedRef.current) return
const ranges: Cursor[] = [] const ranges: Cursor[] = []
// If the cursor data is null or undefined, unset all active cursors
if (!data) {
setCursorData(ranges)
return
}
try {
const cursors = toJS(data) const cursors = toJS(data)
for (let cursor in cursors) { for (let cursor in cursors) {
@ -30,6 +38,13 @@ const useCursor = (
if (mountedRef.current) { if (mountedRef.current) {
setCursorData(ranges) setCursorData(ranges)
} }
} catch (err) {
e.handleError(err, {
type: 'onCursor',
data,
ranges
})
}
} }
}, []) }, [])