mirror of
https://github.com/cudr/slate-collaborative.git
synced 2024-10-27 20:34:06 +00:00
fix: old state passed to connection error
This commit is contained in:
parent
511f0f925d
commit
2b8206d1c5
@ -93,10 +93,7 @@ class AutomergeBackend {
|
|||||||
const sync = toSync({ cursors: {}, children: data })
|
const sync = toSync({ cursors: {}, children: data })
|
||||||
|
|
||||||
const doc = Automerge.from<SyncDoc>(sync)
|
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)
|
this.documentSetMap[docId].setDoc(docId, doc)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e, docId)
|
console.error(e, docId)
|
||||||
|
@ -10,7 +10,7 @@ import { SyncDoc, CollabAction, toJS } from '@hiveteams/collab-bridge'
|
|||||||
import { getClients } from './utils'
|
import { getClients } from './utils'
|
||||||
|
|
||||||
import AutomergeBackend from './AutomergeBackend'
|
import AutomergeBackend from './AutomergeBackend'
|
||||||
import { debugCollabBackend } from 'utils/debug'
|
import { debugCollabBackend } from './utils/debug'
|
||||||
|
|
||||||
export interface SocketIOCollaborationOptions {
|
export interface SocketIOCollaborationOptions {
|
||||||
entry: Server
|
entry: Server
|
||||||
|
51
packages/bridge/src/connection/connection.spec.ts
Normal file
51
packages/bridge/src/connection/connection.spec.ts
Normal 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()
|
||||||
|
})
|
||||||
|
})
|
@ -1,6 +1,7 @@
|
|||||||
import { Operation, Range } from 'slate'
|
import { Operation, Range } from 'slate'
|
||||||
|
|
||||||
import { CursorData } from '../model'
|
import { CursorData } from '../model'
|
||||||
|
import { toJS } from '../utils'
|
||||||
|
|
||||||
export const setCursor = (
|
export const setCursor = (
|
||||||
id: string,
|
id: string,
|
||||||
@ -9,6 +10,7 @@ export const setCursor = (
|
|||||||
operations: Operation[],
|
operations: Operation[],
|
||||||
cursorData: CursorData
|
cursorData: CursorData
|
||||||
) => {
|
) => {
|
||||||
|
try {
|
||||||
const cursorOps = operations.filter(op => op.type === 'set_selection')
|
const cursorOps = operations.filter(op => op.type === 'set_selection')
|
||||||
|
|
||||||
if (!doc.cursors) doc.cursors = {}
|
if (!doc.cursors) doc.cursors = {}
|
||||||
@ -30,6 +32,9 @@ export const setCursor = (
|
|||||||
} else {
|
} else {
|
||||||
delete doc.cursors[id]
|
delete doc.cursors[id]
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e, toJS(doc))
|
||||||
|
}
|
||||||
|
|
||||||
return doc
|
return doc
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,8 @@
|
|||||||
"build:module": "npm run build:types && npm run build:js",
|
"build:module": "npm run build:types && npm run build:js",
|
||||||
"build:types": "tsc --emitDeclarationOnly",
|
"build:types": "tsc --emitDeclarationOnly",
|
||||||
"build:js": "babel src --out-dir lib --extensions \".ts,.tsx\" --source-maps inline",
|
"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": {
|
"dependencies": {
|
||||||
"@babel/plugin-proposal-optional-chaining": "^7.9.0",
|
"@babel/plugin-proposal-optional-chaining": "^7.9.0",
|
||||||
@ -41,10 +42,29 @@
|
|||||||
"@babel/preset-env": "^7.6.0",
|
"@babel/preset-env": "^7.6.0",
|
||||||
"@babel/preset-typescript": "^7.6.0",
|
"@babel/preset-typescript": "^7.6.0",
|
||||||
"@types/react": "^16.9.34",
|
"@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": {
|
"directories": {
|
||||||
"lib": "lib"
|
"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?$"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,6 +33,8 @@ export interface AutomergeEditor extends Editor {
|
|||||||
gabageCursor: () => void
|
gabageCursor: () => void
|
||||||
|
|
||||||
onCursor: (data: any) => void
|
onCursor: (data: any) => void
|
||||||
|
|
||||||
|
automergeCleanup: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -51,7 +53,7 @@ export const AutomergeEditor = {
|
|||||||
* Apply Slate operations to Automerge
|
* Apply Slate operations to Automerge
|
||||||
*/
|
*/
|
||||||
|
|
||||||
applySlateOps: async (
|
applySlateOps: (
|
||||||
e: AutomergeEditor,
|
e: AutomergeEditor,
|
||||||
docId: string,
|
docId: string,
|
||||||
operations: Operation[],
|
operations: Operation[],
|
||||||
@ -63,19 +65,19 @@ export const AutomergeEditor = {
|
|||||||
throw new TypeError(`Unknown docId: ${docId}!`)
|
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 =>
|
changed = Automerge.change<SyncDoc>(changed || doc, d =>
|
||||||
applyOperation(d.children, op)
|
applyOperation(d.children, op)
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
changed = Automerge.change(changed || doc, d => {
|
changed = Automerge.change(changed || doc, d => {
|
||||||
setCursor(e.clientId, e.selection, d, operations, cursorData || {})
|
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) => {
|
garbageCursor: (e: AutomergeEditor, docId: string) => {
|
||||||
const doc = e.docSet.getDoc(docId)
|
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) => {
|
const changed = Automerge.change<SyncDoc>(doc, (d: any) => {
|
||||||
delete d.cursors
|
delete d.cursors
|
||||||
})
|
})
|
||||||
|
|
||||||
e.onCursor && e.onCursor(null)
|
|
||||||
|
|
||||||
e.docSet.setDoc(docId, changed)
|
e.docSet.setDoc(docId, changed)
|
||||||
|
|
||||||
|
e.onCursor && e.onCursor(null)
|
||||||
|
|
||||||
e.onChange()
|
e.onChange()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
136
packages/client/src/client.spec.ts
Normal file
136
packages/client/src/client.spec.ts
Normal 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()
|
||||||
|
// })
|
||||||
|
})
|
@ -34,21 +34,15 @@ const withAutomerge = <T extends Editor>(
|
|||||||
|
|
||||||
e.docSet = new Automerge.DocSet()
|
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
|
* Open Automerge Connection
|
||||||
*/
|
*/
|
||||||
|
|
||||||
e.openConnection = () => {
|
e.openConnection = () => {
|
||||||
createConnection()
|
e.connection = AutomergeEditor.createConnection(e, (data: CollabAction) =>
|
||||||
|
//@ts-ignore
|
||||||
|
e.send(data)
|
||||||
|
)
|
||||||
|
|
||||||
e.connection.open()
|
e.connection.open()
|
||||||
}
|
}
|
||||||
@ -76,6 +70,10 @@ const withAutomerge = <T extends Editor>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
e.automergeCleanup = () => {
|
||||||
|
e.docSet = new Automerge.DocSet()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Editor onChange
|
* Editor onChange
|
||||||
*/
|
*/
|
||||||
@ -84,9 +82,11 @@ const withAutomerge = <T extends Editor>(
|
|||||||
const operations: any = e.operations
|
const operations: any = e.operations
|
||||||
|
|
||||||
if (!e.isRemote) {
|
if (!e.isRemote) {
|
||||||
AutomergeEditor.applySlateOps(e, docId, operations, cursorData).catch(
|
try {
|
||||||
onError
|
AutomergeEditor.applySlateOps(e, docId, operations, cursorData)
|
||||||
)
|
} catch (err) {
|
||||||
|
onError(err)
|
||||||
|
}
|
||||||
|
|
||||||
onChange()
|
onChange()
|
||||||
}
|
}
|
||||||
@ -97,7 +97,11 @@ const withAutomerge = <T extends Editor>(
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
e.receiveDocument = data => {
|
e.receiveDocument = data => {
|
||||||
|
try {
|
||||||
AutomergeEditor.receiveDocument(e, docId, data)
|
AutomergeEditor.receiveDocument(e, docId, data)
|
||||||
|
} catch (err) {
|
||||||
|
onError(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -115,6 +115,7 @@ const withSocketIO = <T extends AutomergeEditor>(
|
|||||||
e.destroy = () => {
|
e.destroy = () => {
|
||||||
socket.close()
|
socket.close()
|
||||||
e.closeConnection()
|
e.closeConnection()
|
||||||
|
e.automergeCleanup()
|
||||||
}
|
}
|
||||||
|
|
||||||
return e
|
return e
|
||||||
|
Loading…
Reference in New Issue
Block a user