2019-10-05 08:44:49 +00:00
|
|
|
import Automerge from 'automerge'
|
|
|
|
import Immutable from 'immutable'
|
|
|
|
import io from 'socket.io-client'
|
|
|
|
|
|
|
|
import { Value, Operation } from 'slate'
|
|
|
|
import { ConnectionModel } from './model'
|
|
|
|
|
2019-10-10 19:45:31 +00:00
|
|
|
import {
|
|
|
|
applySlateOps,
|
|
|
|
toSlateOp,
|
|
|
|
hexGen,
|
|
|
|
toJS
|
|
|
|
} from '@slate-collaborative/bridge'
|
2019-10-05 08:44:49 +00:00
|
|
|
|
|
|
|
class Connection {
|
|
|
|
url: string
|
|
|
|
docId: string
|
|
|
|
docSet: Automerge.DocSet<any>
|
|
|
|
connection: Automerge.Connection<any>
|
|
|
|
socket: SocketIOClient.Socket
|
|
|
|
editor: any
|
|
|
|
connectOpts: any
|
2019-10-10 19:45:31 +00:00
|
|
|
selection: any
|
2019-10-05 08:44:49 +00:00
|
|
|
onConnect?: () => void
|
|
|
|
onDisconnect?: () => void
|
|
|
|
|
|
|
|
constructor({
|
|
|
|
editor,
|
|
|
|
url,
|
|
|
|
connectOpts,
|
|
|
|
onConnect,
|
|
|
|
onDisconnect
|
|
|
|
}: ConnectionModel) {
|
|
|
|
this.url = url
|
|
|
|
this.editor = editor
|
|
|
|
this.connectOpts = connectOpts
|
|
|
|
this.onConnect = onConnect
|
|
|
|
this.onDisconnect = onDisconnect
|
|
|
|
|
|
|
|
this.docId = connectOpts.path || new URL(url).pathname
|
|
|
|
|
|
|
|
this.docSet = new Automerge.DocSet()
|
|
|
|
|
|
|
|
this.connect()
|
|
|
|
}
|
|
|
|
|
|
|
|
sendData = (data: any) => {
|
|
|
|
this.socket.emit('operation', data)
|
|
|
|
}
|
|
|
|
|
|
|
|
recieveData = async (data: any) => {
|
|
|
|
if (this.docId !== data.docId || !this.connection) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
const currentDoc = this.docSet.getDoc(this.docId)
|
|
|
|
const docNew = this.connection.receiveMsg(data)
|
|
|
|
|
|
|
|
if (!docNew) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
const operations = Automerge.diff(currentDoc, docNew)
|
|
|
|
|
|
|
|
if (operations.length !== 0) {
|
2019-10-05 21:24:52 +00:00
|
|
|
const slateOps = toSlateOp(operations, currentDoc)
|
|
|
|
|
2019-10-05 08:44:49 +00:00
|
|
|
this.editor.remote = true
|
|
|
|
|
|
|
|
this.editor.withoutSaving(() => {
|
2019-10-05 21:24:52 +00:00
|
|
|
slateOps.forEach(o => {
|
|
|
|
this.editor.applyOperation(o)
|
|
|
|
})
|
2019-10-05 08:44:49 +00:00
|
|
|
})
|
|
|
|
|
2019-10-10 19:45:31 +00:00
|
|
|
await Promise.resolve()
|
|
|
|
|
|
|
|
this.editor.remote = false
|
|
|
|
|
|
|
|
this.setCursors(docNew.cursors)
|
2019-10-05 08:44:49 +00:00
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
console.error(e)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-10 19:45:31 +00:00
|
|
|
setCursors = cursors => {
|
|
|
|
if (!cursors) return
|
2019-10-12 05:02:37 +00:00
|
|
|
|
2019-10-10 19:45:31 +00:00
|
|
|
const {
|
|
|
|
value: { annotations }
|
|
|
|
} = this.editor
|
|
|
|
|
|
|
|
const keyMap = {}
|
|
|
|
|
2019-10-12 05:02:37 +00:00
|
|
|
console.log('cursors', cursors)
|
|
|
|
|
2019-10-10 19:45:31 +00:00
|
|
|
this.editor.withoutSaving(() => {
|
|
|
|
annotations.forEach(anno => {
|
|
|
|
this.editor.removeAnnotation(anno)
|
|
|
|
})
|
|
|
|
|
|
|
|
Object.keys(cursors).forEach(key => {
|
|
|
|
if (key !== this.socket.id && !keyMap[key]) {
|
2019-10-12 05:02:37 +00:00
|
|
|
this.editor.addAnnotation(toJS(cursors[key]))
|
2019-10-10 19:45:31 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2019-10-05 08:44:49 +00:00
|
|
|
receiveSlateOps = (operations: Immutable.List<Operation>) => {
|
|
|
|
const doc = this.docSet.getDoc(this.docId)
|
|
|
|
const message = `change from ${this.socket.id}`
|
|
|
|
|
|
|
|
if (!doc) return
|
|
|
|
|
2019-10-12 05:02:37 +00:00
|
|
|
const selectionOps = operations.filter(op => op.type === 'set_selection')
|
|
|
|
|
|
|
|
console.log('hasSelectionOps', selectionOps.size)
|
|
|
|
|
|
|
|
const { value } = this.editor
|
|
|
|
|
|
|
|
const { selection } = value
|
|
|
|
|
|
|
|
const meta = {
|
|
|
|
id: this.socket.id,
|
|
|
|
selection,
|
|
|
|
annotationType: 'collaborative_selection'
|
|
|
|
}
|
|
|
|
|
|
|
|
const cursor = doc.cursors[meta.id]
|
|
|
|
const cursorOffset = cursor && cursor.anchor && cursor.anchor.offset
|
|
|
|
|
|
|
|
if (!selectionOps.size && selection.start.offset !== cursorOffset) {
|
|
|
|
const opData = {
|
|
|
|
type: 'set_selection',
|
|
|
|
properties: {},
|
|
|
|
newProperties: {
|
|
|
|
anchor: selection.start,
|
|
|
|
focus: selection.end
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const op = Operation.fromJSON(opData)
|
|
|
|
|
|
|
|
operations = operations.push(op)
|
|
|
|
}
|
|
|
|
|
|
|
|
console.log('operations', operations.toJSON())
|
|
|
|
|
2019-10-05 08:44:49 +00:00
|
|
|
const changed = Automerge.change(doc, message, (d: any) =>
|
2019-10-12 05:02:37 +00:00
|
|
|
applySlateOps(d, operations, meta)
|
2019-10-05 08:44:49 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
this.docSet.setDoc(this.docId, changed)
|
|
|
|
}
|
|
|
|
|
|
|
|
recieveDocument = data => {
|
|
|
|
const currentDoc = this.docSet.getDoc(this.docId)
|
|
|
|
|
|
|
|
if (!currentDoc) {
|
|
|
|
const doc = Automerge.load(data)
|
|
|
|
|
|
|
|
this.docSet.removeDoc(this.docId)
|
|
|
|
|
|
|
|
this.docSet.setDoc(this.docId, doc)
|
|
|
|
|
|
|
|
this.editor.controller.setValue(Value.fromJSON(toJS(doc)))
|
|
|
|
}
|
|
|
|
|
|
|
|
this.editor.setFocus()
|
|
|
|
|
|
|
|
this.connection.open()
|
|
|
|
|
|
|
|
this.onConnect && setTimeout(this.onConnect, 0)
|
|
|
|
}
|
|
|
|
|
|
|
|
connect = () => {
|
|
|
|
this.socket = io(this.url, this.connectOpts)
|
|
|
|
|
|
|
|
this.socket.on('connect', () => {
|
|
|
|
this.connection = new Automerge.Connection(this.docSet, this.sendData)
|
|
|
|
|
|
|
|
this.socket.on('document', this.recieveDocument)
|
|
|
|
|
|
|
|
this.socket.on('operation', this.recieveData)
|
|
|
|
|
|
|
|
this.socket.on('disconnect', this.disconnect)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
disconnect = () => {
|
|
|
|
this.onDisconnect()
|
|
|
|
|
|
|
|
this.connection && this.connection.close()
|
|
|
|
|
|
|
|
delete this.connection
|
|
|
|
|
|
|
|
this.socket.removeListener('document')
|
|
|
|
this.socket.removeListener('operation')
|
|
|
|
}
|
|
|
|
|
|
|
|
close = () => {
|
|
|
|
this.onDisconnect()
|
|
|
|
|
|
|
|
this.socket.close()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export default Connection
|