fix: old state passed to connection error

This commit is contained in:
Eric Maciel 2021-01-05 18:30:40 -05:00
parent 511f0f925d
commit 2b8206d1c5
9 changed files with 265 additions and 45 deletions

View File

@ -93,10 +93,7 @@ class AutomergeBackend {
const sync = toSync({ cursors: {}, children: data })
const doc = Automerge.from<SyncDoc>(sync)
if (!this.documentSetMap[docId]) {
this.documentSetMap[docId] = new Automerge.DocSet<SyncDoc>()
}
this.documentSetMap[docId] = new Automerge.DocSet<SyncDoc>()
this.documentSetMap[docId].setDoc(docId, doc)
} catch (e) {
console.error(e, docId)

View File

@ -10,7 +10,7 @@ import { SyncDoc, CollabAction, toJS } from '@hiveteams/collab-bridge'
import { getClients } from './utils'
import AutomergeBackend from './AutomergeBackend'
import { debugCollabBackend } from 'utils/debug'
import { debugCollabBackend } from './utils/debug'
export interface SocketIOCollaborationOptions {
entry: Server

View File

@ -0,0 +1,51 @@
import * as Automerge from 'automerge'
interface TestDoc {
_id: string
status: string
}
// TODO: delete this?
describe('old state error replication', () => {
const clientDocSet = new Automerge.DocSet()
const serverDocSet = new Automerge.DocSet()
const docId = 'test'
let clientDoc = Automerge.from<TestDoc>({
_id: docId,
status: 'Unstarted'
})
let serverDoc = Automerge.from<TestDoc>({
_id: docId,
status: 'Unstarted'
})
it('replicate old state error', () => {
clientDocSet.setDoc(docId, clientDoc)
serverDocSet.setDoc(docId, serverDoc)
let clientMessages: string[] = []
const clientConnection = new Automerge.Connection(clientDocSet, msg => {
clientMessages.push(JSON.stringify(msg))
})
clientConnection.open()
let serverMessages: string[] = []
const serverConnection = new Automerge.Connection(serverDocSet, msg => {
serverMessages.push(JSON.stringify(msg))
})
serverConnection.open()
let oldClientDoc = clientDoc
clientDoc = Automerge.change(clientDoc, newClientDoc => {
newClientDoc.status = 'In progress'
})
clientDocSet.setDoc(docId, clientDoc)
expect(clientMessages.length).toEqual(2)
expect(serverMessages.length).toEqual(1)
expect(() => {
clientDocSet.setDoc(docId, oldClientDoc)
}).toThrow()
})
})

View File

@ -1,6 +1,7 @@
import { Operation, Range } from 'slate'
import { CursorData } from '../model'
import { toJS } from '../utils'
export const setCursor = (
id: string,
@ -9,26 +10,30 @@ export const setCursor = (
operations: Operation[],
cursorData: CursorData
) => {
const cursorOps = operations.filter(op => op.type === 'set_selection')
try {
const cursorOps = operations.filter(op => op.type === 'set_selection')
if (!doc.cursors) doc.cursors = {}
if (!doc.cursors) doc.cursors = {}
const newCursor = cursorOps[cursorOps.length - 1]?.newProperties || {}
const newCursor = cursorOps[cursorOps.length - 1]?.newProperties || {}
if (selection) {
const newCursorData = Object.assign(
(doc.cursors[id] && JSON.parse(doc.cursors[id])) || {},
newCursor,
selection,
{
...cursorData,
isForward: Range.isForward(selection)
}
)
if (selection) {
const newCursorData = Object.assign(
(doc.cursors[id] && JSON.parse(doc.cursors[id])) || {},
newCursor,
selection,
{
...cursorData,
isForward: Range.isForward(selection)
}
)
doc.cursors[id] = JSON.stringify(newCursorData)
} else {
delete doc.cursors[id]
doc.cursors[id] = JSON.stringify(newCursorData)
} else {
delete doc.cursors[id]
}
} catch (e) {
console.error(e, toJS(doc))
}
return doc

View File

@ -21,7 +21,8 @@
"build:module": "npm run build:types && npm run build:js",
"build:types": "tsc --emitDeclarationOnly",
"build:js": "babel src --out-dir lib --extensions \".ts,.tsx\" --source-maps inline",
"watch": "yarn build:js -w"
"watch": "yarn build:js -w",
"test": "jest"
},
"dependencies": {
"@babel/plugin-proposal-optional-chaining": "^7.9.0",
@ -41,10 +42,29 @@
"@babel/preset-env": "^7.6.0",
"@babel/preset-typescript": "^7.6.0",
"@types/react": "^16.9.34",
"@types/socket.io-client": "^1.4.32"
"@types/socket.io-client": "^1.4.32",
"@hiveteams/collab-backend": "^0.7.16",
"@types/jest": "^24.9.0",
"jest": "^26.6.3",
"ts-jest": "^26.4.4"
},
"directories": {
"lib": "lib"
},
"gitHead": "89dd1657ba1b39db298e00a380f45089b8b52a91"
"gitHead": "89dd1657ba1b39db298e00a380f45089b8b52a91",
"jest": {
"preset": "ts-jest",
"globals": {
"ts-jest": {
"babelConfig": ".babelrc"
}
},
"roots": [
"<rootDir>/src"
],
"transform": {
"^.+\\.ts?$": "ts-jest"
},
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$"
}
}

View File

@ -33,6 +33,8 @@ export interface AutomergeEditor extends Editor {
gabageCursor: () => void
onCursor: (data: any) => void
automergeCleanup: () => void
}
/**
@ -51,7 +53,7 @@ export const AutomergeEditor = {
* Apply Slate operations to Automerge
*/
applySlateOps: async (
applySlateOps: (
e: AutomergeEditor,
docId: string,
operations: Operation[],
@ -63,19 +65,19 @@ export const AutomergeEditor = {
throw new TypeError(`Unknown docId: ${docId}!`)
}
let changed
let changed: any
for await (let op of operations) {
operations.forEach(op => {
changed = Automerge.change<SyncDoc>(changed || doc, d =>
applyOperation(d.children, op)
)
}
})
changed = Automerge.change(changed || doc, d => {
setCursor(e.clientId, e.selection, d, operations, cursorData || {})
})
e.docSet.setDoc(docId, changed as any)
e.docSet.setDoc(docId, changed)
},
/**
@ -150,14 +152,18 @@ export const AutomergeEditor = {
garbageCursor: (e: AutomergeEditor, docId: string) => {
const doc = e.docSet.getDoc(docId)
// if the document has already been cleaned up
// return early and do nothing
if (!doc) return
const changed = Automerge.change<SyncDoc>(doc, (d: any) => {
delete d.cursors
})
e.onCursor && e.onCursor(null)
e.docSet.setDoc(docId, changed)
e.onCursor && e.onCursor(null)
e.onChange()
}
}

View File

@ -0,0 +1,136 @@
import { createEditor, Element, Node, Transforms } from 'slate'
import * as Automerge from 'automerge'
import withAutomerge, { AutomergeOptions } from './withAutomerge'
import { SyncDoc, toJS } from '@hiveteams/collab-bridge'
import AutomergeBackend from '@hiveteams/collab-backend/lib/AutomergeBackend'
import { insertText } from '../../bridge/src/apply/text'
describe('automerge editor client tests', () => {
const docId = 'test'
const automergeOptions: AutomergeOptions = {
docId,
onError: msg => console.log('Encountered test error', msg)
}
const editor = withAutomerge(createEditor(), automergeOptions)
const automergeBackend = new AutomergeBackend()
const clientId = 'test-client'
/**
* Initialize a basic automerge backend
*/
// Create a new server automerge connection with a basic send function
let serverMessages: any[] = []
automergeBackend.appendDocument(docId, [
{ type: 'paragraph', children: [{ text: 'Hi' }] }
])
automergeBackend.createConnection(clientId, docId, (msg: any) => {
serverMessages.push(msg)
})
automergeBackend.openConnection(clientId)
// define an editor send function for the clientside automerge editor
let clientMessages: any[] = []
editor.send = (msg: any) => {
clientMessages.push(msg)
}
// open the editor connection
editor.openConnection()
/**
* Helper function to flush client messages and send them to the server
*/
const sendClientMessagesToServer = () => {
// console.log('clientMessages', JSON.stringify(clientMessages))
clientMessages.forEach(msg => {
automergeBackend.receiveOperation(clientId, msg)
})
clientMessages = []
}
/**
* Helper function to flush server messages and send them to the client
*/
const receiveMessagesFromServer = () => {
console.log('serverMessages', JSON.stringify(serverMessages))
serverMessages.forEach(msg => {
editor.receiveOperation(msg)
})
serverMessages = []
}
afterEach(() => {
sendClientMessagesToServer()
receiveMessagesFromServer()
})
it('should properly receiveDocument', () => {
const initialDocData = Automerge.save(automergeBackend.getDocument(docId))
editor.receiveDocument(initialDocData)
expect(editor.children.length).toEqual(1)
const paragraphNode = editor.children[0] as Element
expect(paragraphNode.type).toEqual('paragraph')
expect(paragraphNode.children.length).toEqual(1)
expect(Node.string(paragraphNode)).toEqual('Hi')
})
it('should sync insert node operation with server', done => {
Transforms.insertNodes(editor, {
type: 'paragraph',
children: [{ text: 'a' }]
})
// ensure that we eventually send a message for the insert_node oepration
const handle = setInterval(() => {
sendClientMessagesToServer()
receiveMessagesFromServer()
const serverDoc = toJS(automergeBackend.getDocument(docId))
if (serverDoc.children.length === 2) {
const paragraphNode = serverDoc.children[1]
expect(Node.string(paragraphNode)).toEqual('a')
clearInterval(handle)
done()
}
}, 10)
})
it('should sync insert text operation with client', done => {
const serverDoc = automergeBackend.getDocument(docId)
const updatedServerDoc = Automerge.change(serverDoc, newServerDoc => {
insertText(newServerDoc as any, {
type: 'insert_text',
path: [1, 0],
offset: 1,
text: 'b'
})
})
automergeBackend.documentSetMap[docId].setDoc(docId, updatedServerDoc)
// ensure that we eventually send a message for the insert_node oepration
const handle = setInterval(() => {
sendClientMessagesToServer()
receiveMessagesFromServer()
const [, secondParagraph] = editor.children
console.log(secondParagraph)
if (Node.string(secondParagraph) === 'ab') {
clearInterval(handle)
done()
}
}, 10)
})
// it('replicate old state error', done => {
// serverConnection.close()
// serverConnection = new Automerge.Connection(serverDocSet, msg => {
// serverMessages.push(msg)
// })
// serverConnection.open()
// sendClientMessagesToServer()
// receiveMessagesFromServer()
// })
})

View File

@ -34,21 +34,15 @@ const withAutomerge = <T extends Editor>(
e.docSet = new Automerge.DocSet()
const createConnection = () => {
e.connection = AutomergeEditor.createConnection(e, (data: CollabAction) =>
//@ts-ignore
e.send(data)
)
e.connection.open()
}
/**
* Open Automerge Connection
*/
e.openConnection = () => {
createConnection()
e.connection = AutomergeEditor.createConnection(e, (data: CollabAction) =>
//@ts-ignore
e.send(data)
)
e.connection.open()
}
@ -76,6 +70,10 @@ const withAutomerge = <T extends Editor>(
}
}
e.automergeCleanup = () => {
e.docSet = new Automerge.DocSet()
}
/**
* Editor onChange
*/
@ -84,9 +82,11 @@ const withAutomerge = <T extends Editor>(
const operations: any = e.operations
if (!e.isRemote) {
AutomergeEditor.applySlateOps(e, docId, operations, cursorData).catch(
onError
)
try {
AutomergeEditor.applySlateOps(e, docId, operations, cursorData)
} catch (err) {
onError(err)
}
onChange()
}
@ -97,7 +97,11 @@ const withAutomerge = <T extends Editor>(
*/
e.receiveDocument = data => {
AutomergeEditor.receiveDocument(e, docId, data)
try {
AutomergeEditor.receiveDocument(e, docId, data)
} catch (err) {
onError(err)
}
}
/**

View File

@ -115,6 +115,7 @@ const withSocketIO = <T extends AutomergeEditor>(
e.destroy = () => {
socket.close()
e.closeConnection()
e.automergeCleanup()
}
return e