diff --git a/README.md b/README.md index 704e899..9aec0b2 100644 --- a/README.md +++ b/README.md @@ -48,9 +48,9 @@ const decorator = useCursor(editor) ## Backend ```ts -const { SocketIOConnection } = require('@hiveteams/collab-backend') +const { AutomergeCollaboration } = require('@hiveteams/collab-backend') -const connection = new SocketIOConnection(options) +const collabBackend = new AutomergeCollaboration(options) ``` ### options: diff --git a/packages/backend/src/AutomergeBackend.ts b/packages/backend/src/AutomergeBackend.ts index 3583d4c..5caa586 100644 --- a/packages/backend/src/AutomergeBackend.ts +++ b/packages/backend/src/AutomergeBackend.ts @@ -53,7 +53,6 @@ class AutomergeBackend { closeConnection(id: string) { this.connectionMap[id]?.close() - delete this.connectionMap[id] } @@ -62,16 +61,12 @@ class AutomergeBackend { */ receiveOperation = (id: string, data: CollabAction) => { - try { - if (!this.connectionMap[id]) { - debugCollabBackend('Could not receive op for closed connection %s', id) - return - } - - this.connectionMap[id].receiveMsg(data.payload) - } catch (e) { - console.error('Unexpected error in receiveOperation', e) + if (!this.connectionMap[id]) { + debugCollabBackend('Could not receive op for closed connection %s', id) + return } + + this.connectionMap[id].receiveMsg(data.payload) } /** @@ -85,19 +80,15 @@ class AutomergeBackend { */ appendDocument = (docId: string, data: Node[]) => { - try { - if (this.getDocument(docId)) { - throw new Error(`Already has document with id: ${docId}`) - } - - const sync = toSync({ cursors: {}, children: data }) - - const doc = Automerge.from(sync) - this.documentSetMap[docId] = new Automerge.DocSet() - this.documentSetMap[docId].setDoc(docId, doc) - } catch (e) { - console.error(e, docId) + if (this.getDocument(docId)) { + throw new Error(`Already has document with id: ${docId}`) } + + const sync = toSync({ cursors: {}, children: data }) + + const doc = Automerge.from(sync) + this.documentSetMap[docId] = new Automerge.DocSet() + this.documentSetMap[docId].setDoc(docId, doc) } /** @@ -105,10 +96,8 @@ class AutomergeBackend { */ removeDocument = (docId: string) => { - if (this.documentSetMap[docId]) { - this.documentSetMap[docId].removeDoc(docId) - delete this.documentSetMap[docId] - } + this.documentSetMap[docId]?.removeDoc(docId) + delete this.documentSetMap[docId] } /** @@ -116,19 +105,16 @@ class AutomergeBackend { */ garbageCursor = (docId: string, id: string) => { - try { - const doc = this.getDocument(docId) + const doc = this.getDocument(docId) - if (!doc || !doc.cursors) return + // no need to delete cursor if the document or cursors have already been deleted + if (!doc || !doc.cursors) return - const change = Automerge.change(doc, (d: any) => { - delete d.cursors[id] - }) + const change = Automerge.change(doc, (d: any) => { + delete d.cursors[id] + }) - this.documentSetMap[docId].setDoc(docId, change) - } catch (e) { - console.error('Unexpected error in garbageCursor', e) - } + this.documentSetMap[docId].setDoc(docId, change) } } diff --git a/packages/backend/src/AutomergeCollaboration.ts b/packages/backend/src/AutomergeCollaboration.ts index 5c84e2f..3d5cd33 100644 --- a/packages/backend/src/AutomergeCollaboration.ts +++ b/packages/backend/src/AutomergeCollaboration.ts @@ -1,4 +1,4 @@ -import io from 'socket.io' +import io, { Socket } from 'socket.io' import * as Automerge from 'automerge' import { Node } from 'slate' import { Server } from 'http' @@ -8,26 +8,35 @@ import { getClients } from './utils/index' import { debugCollabBackend } from './utils/debug' import AutomergeBackend from './AutomergeBackend' +interface ErrorData { + user: any + docId: string + serializedData: string + opData?: string +} + export interface IAutomergeCollaborationOptions { entry: Server connectOpts?: SocketIO.ServerOptions defaultValue: Node[] saveFrequency?: number onAuthRequest?: (query: any, socket?: SocketIO.Socket) => Promise - onDocumentLoad?: (pathname: string, query?: any) => Promise | Node[] + onDocumentLoad?: (docId: string, query?: any) => Promise | Node[] onDocumentSave?: ( - pathname: string, + docId: string, doc: Node[], user: any ) => Promise | void - onDisconnect?: (pathname: string, user: any) => Promise | void + onDisconnect?: (docId: string, user: any) => Promise | void + onError: (error: Error, data: ErrorData) => Promise | void } export default class AutomergeCollaboration { private io: SocketIO.Server private options: IAutomergeCollaborationOptions - private backend: AutomergeBackend + public backend: AutomergeBackend private userMap: { [key: string]: any | undefined } + private autoSaveDoc: (socket: SocketIO.Socket, docId: string) => void /** * Constructor @@ -44,6 +53,15 @@ export default class AutomergeCollaboration { this.userMap = {} + /** + * Save document with throttle + */ + this.autoSaveDoc = throttle( + async (socket: SocketIO.Socket, docId: string) => + this.backend.getDocument(docId) && this.saveDocument(socket, docId), + this.options?.saveFrequency || 2000 + ) + return this } @@ -57,6 +75,24 @@ export default class AutomergeCollaboration { .use(this.authMiddleware) .on('connect', this.onConnect) + /** + * Construct error data and call onError callback + */ + private handleError(socket: SocketIO.Socket, err: Error, opData?: string) { + const { id } = socket + const { name: docId } = socket.nsp + + if (this.options.onError) { + const document = this.backend.getDocument(docId) + this.options.onError(err, { + user: this.userMap[id], + docId: docId, + serializedData: document ? Automerge.save(document) : 'No document', + opData + }) + } + } + /** * Namespace SocketIO middleware. Load document value and append it to CollaborationBackend. */ @@ -73,13 +109,12 @@ export default class AutomergeCollaboration { socket: SocketIO.Socket, next: (e?: any) => void ) => { - const { id } = socket const { query } = socket.handshake const { onAuthRequest } = this.options // we connect before any async logic so that we // never miss a socket disconnection event - socket.on('disconnect', this.onDisconnect(id, socket)) + socket.on('disconnect', this.onDisconnect(socket)) if (onAuthRequest) { const user = await onAuthRequest(query, socket) @@ -98,55 +133,65 @@ export default class AutomergeCollaboration { private onConnect = async (socket: SocketIO.Socket) => { const { id, conn } = socket + // do nothing if the socket connection has already been closed if (conn.readyState === 'closed') { return } - const { name } = socket.nsp + const { name: docId } = socket.nsp const { onDocumentLoad } = this.options - if (!this.backend.getDocument(name)) { - const doc = onDocumentLoad - ? await onDocumentLoad(name) - : this.options.defaultValue + try { + // Load document if no document state is already stored in our automerge backend + if (!this.backend.getDocument(docId)) { + // If the user provided an onDocumentLoad function use that, otherwise use the + // default value that was provided in the options + const doc = onDocumentLoad + ? await onDocumentLoad(docId) + : this.options.defaultValue - // Ensure socket is still opened - // recheck ready state after async operation - if (conn.readyState === 'closed') { - return + // Ensure socket is still opened + // recheck websocket connection state after the previous potentially async document load + if (conn.readyState === 'closed') { + return + } + + // recheck backend getDocument after async operation + // to avoid duplicatively loading a document + if (!this.backend.getDocument(docId)) { + debugCollabBackend('Append document\t\t%s', id) + this.backend.appendDocument(docId, doc) + } } - // recheck backend getDocument after async operation - if (!this.backend.getDocument(name)) { - debugCollabBackend('Append document\t\t%s', id) - this.backend.appendDocument(name, doc) - } - } + // Create a new backend connection for this socketId and docId + debugCollabBackend('Create connection\t%s', id) + this.backend.createConnection( + id, + docId, + ({ type, payload }: CollabAction) => { + if (payload.docId === docId) { + socket.emit('msg', { type, payload: { id: conn.id, ...payload } }) + } + } + ) - debugCollabBackend('Create connection\t%s', id) - this.backend.createConnection( - id, - name, - ({ type, payload }: CollabAction) => { - socket.emit('msg', { type, payload: { id: conn.id, ...payload } }) - } - ) - - socket.on('msg', this.onMessage(id, name)) - - socket.join(id, () => { - const doc = this.backend.getDocument(name) + // Setup the on message callback + socket.on('msg', this.onMessage(socket, docId)) + const doc = this.backend.getDocument(docId) if (!doc) { debugCollabBackend( 'onConnect: No document available at the time of socket.io join docId=%s socketId=%s', - name, + docId, id ) return } + // Emit the socket message needed for receiving the automerge document + // on connect and reconnect socket.emit('msg', { type: 'document', payload: Automerge.save(doc) @@ -154,47 +199,41 @@ export default class AutomergeCollaboration { debugCollabBackend('Open connection\t\t%s', id) this.backend.openConnection(id) - }) - - this.garbageCursors(name) + this.garbageCursors(socket) + } catch (err) { + this.handleError(socket, err) + } } /** * On 'message' handler */ - private onMessage = (id: string, name: string) => (data: any) => { + private onMessage = (socket: SocketIO.Socket, docId: string) => ( + data: any + ) => { + const { id } = socket switch (data.type) { case 'operation': try { this.backend.receiveOperation(id, data) - this.autoSaveDoc(id, name) + this.autoSaveDoc(socket, docId) - this.garbageCursors(name) + this.garbageCursors(socket) } catch (e) { - console.log(e) + this.handleError(socket, e, JSON.stringify(data)) } } } - /** - * Save document with throttle - */ - - private autoSaveDoc = throttle( - async (id: string, docId: string) => - this.backend.getDocument(docId) && this.saveDocument(id, docId), - // @ts-ignore: property used before initialization - this.options?.saveFrequency || 2000 - ) - /** * Save document */ - private saveDocument = async (id: string, docId: string) => { + private saveDocument = async (socket: SocketIO.Socket, docId: string) => { try { + const { id } = socket const { onDocumentSave } = this.options const doc = this.backend.getDocument(docId) @@ -212,7 +251,7 @@ export default class AutomergeCollaboration { await onDocumentSave(docId, toJS(doc.children), user) } } catch (e) { - console.error(e, docId) + this.handleError(socket, e) } } @@ -220,43 +259,56 @@ export default class AutomergeCollaboration { * On 'disconnect' handler */ - private onDisconnect = (id: string, socket: SocketIO.Socket) => async () => { - debugCollabBackend('Connection closed\t%s', id) - this.backend.closeConnection(id) + private onDisconnect = (socket: SocketIO.Socket) => async () => { + try { + const { id } = socket + const { name: docId } = socket.nsp + socket.leave(docId) - await this.saveDocument(id, socket.nsp.name) + debugCollabBackend('Connection closed\t%s', id) + this.backend.closeConnection(id) - // trigger onDisconnect callback if one was provided - // and if a user has been loaded for this socket connection - const user = this.userMap[id] - if (this.options.onDisconnect && user) { - await this.options.onDisconnect(socket.nsp.name, user) + await this.saveDocument(socket, docId) + + // trigger onDisconnect callback if one was provided + // and if a user has been loaded for this socket connection + const user = this.userMap[id] + if (this.options.onDisconnect && user) { + await this.options.onDisconnect(docId, user) + } + + // cleanup automerge cursor and socket connection + this.garbageCursors(socket) + socket.leave(id) + this.garbageNsp(socket) + + // cleanup usermap + delete this.userMap[id] + } catch (err) { + this.handleError(socket, err) } - - // cleanup automerge cursor and socket connection - this.garbageCursors(socket.nsp.name) - socket.leave(id) - this.garbageNsp(id) - - // cleanup usermap - delete this.userMap[id] } /** * Clean up unused SocketIO namespaces. */ - garbageNsp = (id: string) => { + garbageNsp = (socket: SocketIO.Socket) => { + const { name: docId } = socket.nsp Object.keys(this.io.nsps) .filter(n => n !== '/') .forEach(nsp => { - getClients(this.io, nsp).then((clientsList: any) => { - if (!clientsList.length) { - debugCollabBackend('Removing document\t%s', id) - this.backend.removeDocument(nsp) - delete this.io.nsps[nsp] - } - }) + getClients(this.io, nsp) + .then((clientsList: any) => { + if (!clientsList.length) { + debugCollabBackend('Removing document\t%s', docId) + this.backend.removeDocument(nsp) + delete this.io.nsps[nsp] + } + }) + .catch(err => { + this.handleError(socket, err) + }) }) } @@ -264,19 +316,24 @@ export default class AutomergeCollaboration { * Clean up unused cursor data. */ - garbageCursors = (nsp: string) => { - const doc = this.backend.getDocument(nsp) - // if document has already been cleaned up, it is safe to return early - if (!doc || !doc.cursors) return + garbageCursors = (socket: SocketIO.Socket) => { + const { name: docId } = socket.nsp + try { + const doc = this.backend.getDocument(docId) + // if document has already been cleaned up, it is safe to return early + if (!doc || !doc.cursors) return - const namespace = this.io.of(nsp) + const namespace = this.io.of(docId) - Object.keys(doc?.cursors)?.forEach(key => { - if (!namespace.sockets[key]) { - debugCollabBackend('Garbage cursor\t\t%s', key) - this.backend.garbageCursor(nsp, key) - } - }) + Object.keys(doc?.cursors)?.forEach(key => { + if (!namespace.sockets[key]) { + debugCollabBackend('Garbage cursor\t\t%s', key) + this.backend.garbageCursor(docId, key) + } + }) + } catch (err) { + this.handleError(socket, err) + } } /** diff --git a/packages/backend/src/SocketIOConnection.ts b/packages/backend/src/SocketIOConnection.ts deleted file mode 100644 index afdd17b..0000000 --- a/packages/backend/src/SocketIOConnection.ts +++ /dev/null @@ -1,282 +0,0 @@ -import io from 'socket.io' -import * as Automerge from 'automerge' -import { Node } from 'slate' -import { Server } from 'http' - -import throttle from 'lodash/throttle' - -import { SyncDoc, CollabAction, toJS } from '@hiveteams/collab-bridge' - -import { getClients } from './utils' - -import AutomergeBackend from './AutomergeBackend' -import { debugCollabBackend } from './utils/debug' - -export interface SocketIOCollaborationOptions { - entry: Server - connectOpts?: SocketIO.ServerOptions - defaultValue: Node[] - saveFrequency?: number - onAuthRequest?: ( - query: Object, - socket?: SocketIO.Socket - ) => Promise | boolean - onDocumentLoad?: ( - pathname: string, - query?: Object - ) => Promise | Node[] - onDocumentSave?: (pathname: string, doc: Node[]) => Promise | void -} - -export default class SocketIOCollaboration { - private io: SocketIO.Server - private options: SocketIOCollaborationOptions - private backend: AutomergeBackend - private autoSaveDoc: (id: string, docId: string) => void - - /** - * Constructor - */ - - constructor(options: SocketIOCollaborationOptions) { - this.io = io(options.entry, options.connectOpts) - - this.backend = new AutomergeBackend() - - this.options = options - - /** - * Save document with throttle - */ - this.autoSaveDoc = throttle( - async (id: string, docId: string) => - this.backend.getDocument(docId) && this.saveDocument(id, docId), - this.options?.saveFrequency || 2000 - ) - - this.configure() - - return this - } - - /** - * Initial IO configuration - */ - - private configure = () => - this.io - .of(this.nspMiddleware) - .use(this.authMiddleware) - .on('connect', this.onConnect) - - /** - * Namespace SocketIO middleware. Load document value and append it to CollaborationBackend. - */ - - private nspMiddleware = async (path: string, query: any, next: any) => { - return next(null, true) - } - - /** - * SocketIO auth middleware. Used for user authentification. - */ - - private authMiddleware = async ( - socket: SocketIO.Socket, - next: (e?: any) => void - ) => { - const { id } = socket - const { query } = socket.handshake - const { onAuthRequest } = this.options - - // we connect before any async logic so that we - // never miss a socket disconnection event - socket.on('disconnect', this.onDisconnect(id, socket)) - - if (onAuthRequest) { - const permit = await onAuthRequest(query, socket) - - if (!permit) - return next(new Error(`Authentification error: ${socket.id}`)) - } - - return next() - } - - /** - * On 'connect' handler. - */ - - private onConnect = async (socket: SocketIO.Socket) => { - const { id, conn } = socket - // do nothing if the socket connection has already been closed - if (conn.readyState === 'closed') { - return - } - - const { name } = socket.nsp - const { onDocumentLoad } = this.options - - if (!this.backend.getDocument(name)) { - const doc = onDocumentLoad - ? await onDocumentLoad(name) - : this.options.defaultValue - - // Ensure socket is still opened - // recheck ready state after async operation - if (conn.readyState === 'closed') { - return - } - - // recheck backend getDocument after async operation - if (!this.backend.getDocument(name)) { - debugCollabBackend('Append document\t\t%s', id) - this.backend.appendDocument(name, doc) - } - } - - debugCollabBackend('Create connection\t%s', id) - this.backend.createConnection( - id, - name, - ({ type, payload }: CollabAction) => { - socket.emit('msg', { type, payload: { id: conn.id, ...payload } }) - } - ) - - socket.on('msg', this.onMessage(id, name)) - - socket.join(id, () => { - const doc = this.backend.getDocument(name) - - if (!doc) { - debugCollabBackend( - 'onConnect: No document available at the time of socket.io join docId=%s socketId=%s', - name, - id - ) - return - } - - socket.emit('msg', { - type: 'document', - payload: Automerge.save(doc) - }) - - debugCollabBackend('Open connection\t\t%s', id) - this.backend.openConnection(id) - }) - - this.garbageCursors(name) - } - - /** - * On 'message' handler - */ - - private onMessage = (id: string, name: string) => (data: any) => { - switch (data.type) { - case 'operation': - try { - this.backend.receiveOperation(id, data) - - this.autoSaveDoc(id, name) - - this.garbageCursors(name) - } catch (e) { - console.log(e) - } - } - } - - /** - * Save document - */ - - private saveDocument = async (id: string, docId: string) => { - try { - const { onDocumentSave } = this.options - - const doc = this.backend.getDocument(docId) - - // Return early if there is no valid document in our crdt backend - // Note: this will happen when user disconnects from the collab server - // before document load has completed - if (!doc) { - return - } - - onDocumentSave && (await onDocumentSave(docId, toJS(doc.children))) - } catch (e) { - console.error(e, docId) - } - } - - /** - * On 'disconnect' handler - */ - - private onDisconnect = (id: string, socket: SocketIO.Socket) => async () => { - debugCollabBackend('Connection closed\t%s', id) - this.backend.closeConnection(id) - - await this.saveDocument(id, socket.nsp.name) - - // cleanup automerge cursor and socket connection - this.garbageCursors(socket.nsp.name) - - socket.leave(id) - this.garbageNsp(id) - } - - /** - * Clean up unused SocketIO namespaces. - */ - - garbageNsp = (id: string) => { - Object.keys(this.io.nsps) - .filter(n => n !== '/') - .forEach(nsp => { - getClients(this.io, nsp).then((clientsList: any) => { - debugCollabBackend( - 'Garbage namespace\t%s clientsList=%o %s', - id, - clientsList, - nsp - ) - if (!clientsList.length) { - debugCollabBackend('Removing document\t%s', id) - this.backend.removeDocument(nsp) - delete this.io.nsps[nsp] - } - }) - }) - } - - /** - * Clean up unused cursor data. - */ - - garbageCursors = (nsp: string) => { - const doc = this.backend.getDocument(nsp) - // if document has already been cleaned up, it is safe to return early - if (!doc || !doc.cursors) return - - const namespace = this.io.of(nsp) - - Object.keys(doc?.cursors)?.forEach(key => { - if (!namespace.sockets[key]) { - debugCollabBackend('Garbage cursor\t\t%s', key) - this.backend.garbageCursor(nsp, key) - } - }) - } - - /** - * Destroy SocketIO connection - */ - - destroy = async () => { - this.io.close() - } -} diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 9794ea9..f25625d 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -1,3 +1,3 @@ -import SocketIOConnection from './SocketIOConnection' +import AutomergeCollaboration from './AutomergeCollaboration' -export { SocketIOConnection } +export { AutomergeCollaboration } diff --git a/packages/backend/src/utils/debug.ts b/packages/backend/src/utils/debug.ts index 455d4aa..5b8bf9f 100644 --- a/packages/backend/src/utils/debug.ts +++ b/packages/backend/src/utils/debug.ts @@ -1,3 +1,3 @@ import debug from 'debug' -export const debugCollabBackend = debug('collab-backend') +export const debugCollabBackend = debug('app-collab') diff --git a/packages/client/package.json b/packages/client/package.json index 13d5ef6..00d4667 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -29,6 +29,7 @@ "@babel/preset-react": "^7.0.0", "@hiveteams/collab-bridge": "^0.7.19", "automerge": "0.14.0", + "lodash": "^4.17.20", "slate": "0.58.3", "slate-history": "0.58.3", "socket.io-client": "^2.3.0", diff --git a/packages/client/src/automerge-editor.ts b/packages/client/src/automerge-connector.ts similarity index 82% rename from packages/client/src/automerge-editor.ts rename to packages/client/src/automerge-connector.ts index 31d4260..773d4b9 100644 --- a/packages/client/src/automerge-editor.ts +++ b/packages/client/src/automerge-connector.ts @@ -13,35 +13,13 @@ import { toSlateOp, CursorData } from '@hiveteams/collab-bridge' - -export interface AutomergeEditor extends Editor { - clientId: string - - isRemote: boolean - - docSet: Automerge.DocSet - connection: Automerge.Connection - - onConnectionMsg: (msg: Automerge.Message) => void - - openConnection: () => void - closeConnection: () => void - - receiveDocument: (data: string) => void - receiveOperation: (data: Automerge.Message) => void - - gabageCursor: () => void - - onCursor: (data: any) => void - - automergeCleanup: () => void -} +import { AutomergeEditor } from './interfaces' /** * `AutomergeEditor` contains methods for collaboration-enabled editors. */ -export const AutomergeEditor = { +export const AutomergeConnector = { /** * Create Automerge connection */ @@ -62,7 +40,7 @@ export const AutomergeEditor = { const doc = e.docSet.getDoc(docId) if (!doc) { - throw new TypeError(`Unknown docId: ${docId}!`) + throw new TypeError('Cannot apply slate ops for missing docId') } let changed: any @@ -114,7 +92,7 @@ export const AutomergeEditor = { preserveExternalHistory?: boolean ) => { try { - const current: any = e.docSet.getDoc(docId) + const current = e.docSet.getDoc(docId) const updated = e.connection.receiveMsg(data) @@ -160,7 +138,7 @@ export const AutomergeEditor = { delete d.cursors }) - e.docSet.setDoc(docId, changed) + e.docSet.setDoc(docId, changed as any) e.onCursor && e.onCursor(null) diff --git a/packages/client/src/client-copy.spec.ts b/packages/client/src/client-copy.spec.ts deleted file mode 100644 index 5320745..0000000 --- a/packages/client/src/client-copy.spec.ts +++ /dev/null @@ -1,185 +0,0 @@ -// 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 AutomergeCollaboration from '@hiveteams/collab-backend/lib/AutomergeCollaboration' -// 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 backendSend = (msg: any) => { -// serverMessages.push(msg) -// } -// const clientId = 'test-client' -// editor.clientId = clientId - -// /** -// * 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, backendSend) - -// // define an editor send function for the clientside automerge editor -// let clientMessages: any[] = [] -// editor.send = (msg: any) => { -// clientMessages.push(msg) -// } - -// automergeBackend.openConnection(clientId) -// // open the editor connection -// editor.openConnection() - -// /** -// * Helper function to flush client messages and send them to the server -// */ -// const sendClientMessagesToServer = () => { -// if (!clientMessages.length) return - -// 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 = () => { -// if (!serverMessages.length) return - -// console.log('serverMessages', JSON.stringify(serverMessages)) -// serverMessages.forEach(msg => { -// editor.receiveOperation(msg.payload) -// }) -// 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 -// if (Node.string(secondParagraph) === 'ab') { -// clearInterval(handle) -// done() -// } -// }, 10) -// }) - -// it('should reapply server state client side when server restarts', done => { -// automergeBackend.closeConnection(clientId) -// automergeBackend.removeDocument(docId) -// automergeBackend.appendDocument(docId, [ -// { type: 'paragraph', children: [{ text: 'Hi' }] } -// ]) -// automergeBackend.createConnection(clientId, docId, backendSend) -// automergeBackend.openConnection(clientId) - -// const docData = Automerge.save(automergeBackend.getDocument(docId)) -// editor.receiveDocument(docData) - -// const handle = setInterval(() => { -// sendClientMessagesToServer() -// receiveMessagesFromServer() -// console.log('server doc', toJS(automergeBackend.getDocument(docId))) -// if (editor.children.length === 1) { -// done() -// clearInterval(handle) -// } -// }, 1000) -// }) - -// // it('should ? on client restart', done => { -// // editor.closeConnection() - -// // Transforms.insertNodes( -// // editor, -// // { -// // type: 'paragraph', -// // children: [{ text: 'a' }] -// // }, -// // { at: [1] } -// // ) - -// // editor.openConnection() -// // const docData = Automerge.save(automergeBackend.getDocument(docId)) -// // editor.receiveDocument(docData) -// // // ensure that we eventually send a message for the insert_node operation -// // const handle = setInterval(() => { -// // sendClientMessagesToServer() -// // receiveMessagesFromServer() - -// // const serverDoc = toJS(automergeBackend.getDocument(docId)) -// // console.log(JSON.stringify(serverDoc)) -// // console.log(editor.children) -// // if (serverDoc.children.length === 2) { -// // const paragraphNode = serverDoc.children[1] -// // expect(Node.string(paragraphNode)).toEqual('a') -// // clearInterval(handle) -// // done() -// // } -// // }, 1000) -// // }) -// }) diff --git a/packages/client/src/client.spec.ts b/packages/client/src/client.spec.ts index 0074703..f7f1975 100644 --- a/packages/client/src/client.spec.ts +++ b/packages/client/src/client.spec.ts @@ -1,185 +1,193 @@ -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' +import { createServer } from 'http' +import isEqual from 'lodash/isEqual' +import { createEditor, Node, Transforms } from 'slate' +import { toJS } from '@hiveteams/collab-bridge' +import AutomergeCollaboration from '@hiveteams/collab-backend/lib/AutomergeCollaboration' +import withIOCollaboration from './withIOCollaboration' +import { AutomergeOptions, SocketIOPluginOptions } from './interfaces' + +const connectionSlug = 'test' +const docId = `/${connectionSlug}` +const options: AutomergeOptions & SocketIOPluginOptions = { + docId, + onError: msg => console.log('Encountered test error', msg), + url: `http://localhost:5000/${connectionSlug}`, + connectOpts: { + query: { + name: 'test-user', + slug: connectionSlug + }, + forceNew: true + } +} + +const waitForCondition = (condition: () => boolean, ms = 10) => + new Promise(resolve => { + const handle = setInterval(() => { + if (condition()) { + clearInterval(handle) + resolve() + } + }, ms) + }) + +const server = createServer(function(req, res) { + res.writeHead(200, { 'Content-Type': 'text/plain' }) + res.write('Hello World!') + res.end() +}) + +const defaultSlateJson = [{ type: 'paragraph', children: [{ text: '' }] }] +const collabBackend = new AutomergeCollaboration({ + entry: server, + defaultValue: defaultSlateJson, + saveFrequency: 1000, + async onAuthRequest(query) { + return { _id: 'test-id', name: 'Eric' } + }, + async onDocumentLoad(pathname) { + return defaultSlateJson + } +}) 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 backendSend = (msg: any) => { - serverMessages.push(msg) - } - const clientId = 'test-client' - editor.clientId = clientId - - /** - * 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, backendSend) - - // define an editor send function for the clientside automerge editor - let clientMessages: any[] = [] - editor.send = (msg: any) => { - clientMessages.push(msg) - } - - automergeBackend.openConnection(clientId) - // open the editor connection - editor.openConnection() - - /** - * Helper function to flush client messages and send them to the server - */ - const sendClientMessagesToServer = () => { - if (!clientMessages.length) return - - 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 = () => { - if (!serverMessages.length) return - - console.log('serverMessages', JSON.stringify(serverMessages)) - serverMessages.forEach(msg => { - editor.receiveOperation(msg.payload) - }) - serverMessages = [] - } - - afterEach(() => { - sendClientMessagesToServer() - receiveMessagesFromServer() + beforeAll(done => { + //pass a callback to tell jest it is async + //start the server before any test + server.listen(5000, () => done()) }) - it('should properly receiveDocument', () => { - const initialDocData = Automerge.save(automergeBackend.getDocument(docId)) - editor.receiveDocument(initialDocData) + const createCollabEditor = async ( + editorOptions: AutomergeOptions & SocketIOPluginOptions = options + ) => { + const editor = withIOCollaboration(createEditor(), editorOptions) + const oldReceiveDocument = editor.receiveDocument + const promise = new Promise(resolve => { + editor.receiveDocument = data => { + oldReceiveDocument(data) + resolve() + } + }) + editor.connect() + + await promise + return editor + } + + it('should receiveDocument', async () => { + const editor = await createCollabEditor() 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') + editor.destroy() }) - it('should sync insert node operation with server', done => { - Transforms.insertNodes(editor, { - type: 'paragraph', - children: [{ text: 'a' }] + it('should send client update to server', async () => { + const editor = await createCollabEditor() + + editor.insertNode({ type: 'paragraph', children: [{ text: 'hi' }] }) + + await waitForCondition(() => { + const serverDoc = toJS(collabBackend.backend.getDocument(docId)) + return serverDoc.children.length === 2 }) - // 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) + editor.destroy() }) - it('should sync insert text operation with client', done => { - const serverDoc = automergeBackend.getDocument(docId) + it('should sync updates across two clients', async () => { + const editor1 = await createCollabEditor() + const editor2 = await createCollabEditor() - const updatedServerDoc = Automerge.change(serverDoc, newServerDoc => { - insertText(newServerDoc as any, { - type: 'insert_text', - path: [1, 0], - offset: 1, - text: 'b' - }) + editor1.insertNode({ type: 'paragraph', children: [{ text: 'hi' }] }) + + await waitForCondition(() => { + const serverDoc = toJS(collabBackend.backend.getDocument(docId)) + return serverDoc.children.length === 2 && editor2.children.length === 2 }) - 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 - if (Node.string(secondParagraph) === 'ab') { - clearInterval(handle) - done() - } - }, 10) + editor1.destroy() + editor2.destroy() }) - it('should reapply server state client side when server restarts', done => { - automergeBackend.closeConnection(clientId) - automergeBackend.removeDocument(docId) - automergeBackend.appendDocument(docId, [ - { type: 'paragraph', children: [{ text: 'Hi' }] } - ]) - automergeBackend.createConnection(clientId, docId, backendSend) - automergeBackend.openConnection(clientId) + it('should sync offline changes on reconnect', async () => { + const editor1 = await createCollabEditor() + const editor2 = await createCollabEditor() - const docData = Automerge.save(automergeBackend.getDocument(docId)) - editor.receiveDocument(docData) + editor1.insertNode({ type: 'paragraph', children: [{ text: 'hi' }] }) - const handle = setInterval(() => { - sendClientMessagesToServer() - receiveMessagesFromServer() - console.log('server doc', toJS(automergeBackend.getDocument(docId))) - if (editor.children.length === 1) { - done() - clearInterval(handle) - } - }, 1000) + await waitForCondition(() => { + const serverDoc = toJS(collabBackend.backend.getDocument(docId)) + return serverDoc.children.length === 2 && editor2.children.length === 2 + }) + + editor1.destroy() + + editor1.insertNode({ type: 'paragraph', children: [{ text: 'offline' }] }) + + editor1.connect() + + await waitForCondition(() => { + const serverDoc = toJS(collabBackend.backend.getDocument(docId)) + return serverDoc.children.length === 3 && editor2.children.length === 3 + }) + + expect(Node.string(editor2.children[2])).toEqual('offline') + + editor1.destroy() + editor2.destroy() }) - // it('should ? on client restart', done => { - // editor.closeConnection() + it('should work with concurrent edits', async () => { + const editor1 = await createCollabEditor() + const editor2 = await createCollabEditor() - // Transforms.insertNodes( - // editor, - // { - // type: 'paragraph', - // children: [{ text: 'a' }] - // }, - // { at: [1] } - // ) + const numEdits = 10 + for (let i = 0; i < numEdits; i++) { + editor1.insertNode({ type: 'paragraph', children: [{ text: '' }] }) + editor2.insertNode({ type: 'paragraph', children: [{ text: '' }] }) + } - // editor.openConnection() - // const docData = Automerge.save(automergeBackend.getDocument(docId)) - // editor.receiveDocument(docData) - // // ensure that we eventually send a message for the insert_node operation - // const handle = setInterval(() => { - // sendClientMessagesToServer() - // receiveMessagesFromServer() + await waitForCondition(() => { + return ( + editor1.children.length === numEdits * 2 + 1 && + editor2.children.length === numEdits * 2 + 1 + ) + }) - // const serverDoc = toJS(automergeBackend.getDocument(docId)) - // console.log(JSON.stringify(serverDoc)) - // console.log(editor.children) - // if (serverDoc.children.length === 2) { - // const paragraphNode = serverDoc.children[1] - // expect(Node.string(paragraphNode)).toEqual('a') - // clearInterval(handle) - // done() - // } - // }, 1000) - // }) + expect(isEqual(editor1.children, editor2.children)).toBeTruthy() + + editor1.destroy() + editor2.destroy() + }) + + it('should work with concurrent insert text operations', async () => { + const editor1 = await createCollabEditor() + const editor2 = await createCollabEditor() + + Transforms.select(editor1, [0, 0]) + Transforms.select(editor2, [0, 0]) + + const numEdits = 10 + for (let i = 0; i < numEdits; i++) { + editor1.insertText('a') + editor2.insertText('b') + } + + await waitForCondition(() => { + return ( + Node.string(editor1.children[0]).length === numEdits * 2 && + Node.string(editor2.children[0]).length === numEdits * 2 + ) + }) + + expect(isEqual(editor1.children, editor2.children)).toBeTruthy() + + editor1.destroy() + editor2.destroy() + }) + + afterAll(() => { + collabBackend.destroy() + server.close() + }) }) diff --git a/packages/client/src/interfaces.ts b/packages/client/src/interfaces.ts new file mode 100644 index 0000000..5fd3104 --- /dev/null +++ b/packages/client/src/interfaces.ts @@ -0,0 +1,58 @@ +import Automerge from 'automerge' +import { Editor } from 'slate' +import { CollabAction, CursorData, SyncDoc } from '@hiveteams/collab-bridge' + +interface ErrorData { + docId: string + serializedData: string + opData?: string + slateOperations?: string +} + +export interface AutomergeOptions { + docId: string + cursorData?: CursorData + preserveExternalHistory?: boolean + onError?: (msg: string | Error, data: ErrorData) => void +} + +export interface AutomergeEditor extends Editor { + clientId: string + + isRemote: boolean + + docSet: Automerge.DocSet + connection: Automerge.Connection + + onConnectionMsg: (msg: Automerge.Message) => void + + openConnection: () => void + closeConnection: () => void + + receiveDocument: (data: string) => void + receiveOperation: (data: Automerge.Message) => void + + gabageCursor: () => void + + onCursor: (data: any) => void + + handleError: (err: Error | string, opData?: string) => void +} + +export interface SocketIOPluginOptions { + url: string + connectOpts: SocketIOClient.ConnectOpts + onConnect?: () => void + onDisconnect?: () => void + onError?: (msg: string | Error, data: ErrorData) => void +} + +export interface WithSocketIOEditor { + clientId: string + socket: SocketIOClient.Socket + connect: () => void + disconnect: () => void + send: (op: CollabAction) => void + receive: (op: CollabAction) => void + destroy: () => void +} diff --git a/packages/client/src/useCursor.ts b/packages/client/src/useCursor.ts index 3907661..b29e073 100644 --- a/packages/client/src/useCursor.ts +++ b/packages/client/src/useCursor.ts @@ -4,8 +4,8 @@ import { Text, Range, Path, NodeEntry } from 'slate' import { toJS, Cursor, Cursors } from '@hiveteams/collab-bridge' -import { AutomergeEditor } from './automerge-editor' import useMounted from './useMounted' +import { AutomergeEditor } from './interfaces' const useCursor = ( e: AutomergeEditor diff --git a/packages/client/src/withAutomerge.ts b/packages/client/src/withAutomerge.ts index 4dea830..0ab1914 100644 --- a/packages/client/src/withAutomerge.ts +++ b/packages/client/src/withAutomerge.ts @@ -2,59 +2,69 @@ import Automerge from 'automerge' import { Editor } from 'slate' -import { AutomergeEditor } from './automerge-editor' +import { AutomergeConnector } from './automerge-connector' -import { CursorData, CollabAction } from '@hiveteams/collab-bridge' - -export interface AutomergeOptions { - docId: string - cursorData?: CursorData - preserveExternalHistory?: boolean - onError?: (msg: string | Error) => void -} +import { CollabAction } from '@hiveteams/collab-bridge' +import { + AutomergeEditor, + AutomergeOptions, + WithSocketIOEditor +} from './interfaces' /** * The `withAutomerge` plugin contains core collaboration logic. */ const withAutomerge = ( - editor: T, + slateEditor: T, options: AutomergeOptions ) => { - const e = editor as T & AutomergeEditor + const { docId, cursorData, preserveExternalHistory } = options || {} - const { onChange } = e + const editor = slateEditor as T & AutomergeEditor & WithSocketIOEditor - const { - docId, - cursorData, - preserveExternalHistory, - onError = (err: string | Error) => console.log('AutomergeEditor error', err) - } = options || {} + const { onChange } = editor - e.docSet = new Automerge.DocSet() + editor.docSet = new Automerge.DocSet() + + /** + * Helper function for handling errors + */ + + editor.handleError = (err: Error | string, opData?: string) => { + const { docId, cursorData, onError } = options + if (onError && cursorData) { + const document = editor.docSet.getDoc(docId) + onError(err, { + docId: docId, + serializedData: document ? Automerge.save(document) : 'No document', + opData, + slateOperations: JSON.stringify(editor.operations) + }) + } + } /** * Open Automerge Connection */ - e.openConnection = () => { - e.connection = AutomergeEditor.createConnection(e, (data: CollabAction) => - //@ts-ignore - e.send(data) + editor.openConnection = () => { + editor.connection = AutomergeConnector.createConnection( + editor, + (data: CollabAction) => editor.send(data) ) - e.connection.open() + editor.connection.open() } /** * Close Automerge Connection */ - e.closeConnection = () => { + editor.closeConnection = () => { // close any actively open connections - if (e.connection) { - e.connection.close() + if (editor.connection) { + editor.connection.close() } } @@ -62,30 +72,25 @@ const withAutomerge = ( * Clear cursor data */ - e.gabageCursor = () => { + editor.gabageCursor = () => { try { - AutomergeEditor.garbageCursor(e, docId) + AutomergeConnector.garbageCursor(editor, docId) } catch (err) { - console.log('garbageCursor error', err) + editor.handleError(err) } } - e.automergeCleanup = () => { - e.docSet = new Automerge.DocSet() - } - /** * Editor onChange */ + editor.onChange = () => { + const operations = editor.operations - e.onChange = () => { - const operations: any = e.operations - - if (!e.isRemote) { + if (!editor.isRemote) { try { - AutomergeEditor.applySlateOps(e, docId, operations, cursorData) + AutomergeConnector.applySlateOps(editor, docId, operations, cursorData) } catch (err) { - onError(err) + editor.handleError(err) } onChange() @@ -96,11 +101,11 @@ const withAutomerge = ( * Receive document value */ - e.receiveDocument = data => { + editor.receiveDocument = data => { try { - AutomergeEditor.receiveDocument(e, docId, data) + AutomergeConnector.receiveDocument(editor, docId, data) } catch (err) { - onError(err) + editor.handleError(err, JSON.stringify(data)) } } @@ -108,18 +113,23 @@ const withAutomerge = ( * Receive Automerge sync operations */ - e.receiveOperation = data => { + editor.receiveOperation = data => { + // ignore document updates for differnt docIds if (docId !== data.docId) return try { - AutomergeEditor.applyOperation(e, docId, data, preserveExternalHistory) + AutomergeConnector.applyOperation( + editor, + docId, + data, + preserveExternalHistory + ) } catch (err) { - // report any errors during apply operation - onError(err) + editor.handleError(err, JSON.stringify(data)) } } - return e + return editor } export default withAutomerge diff --git a/packages/client/src/withIOCollaboration.ts b/packages/client/src/withIOCollaboration.ts index cd18012..3f5fab6 100644 --- a/packages/client/src/withIOCollaboration.ts +++ b/packages/client/src/withIOCollaboration.ts @@ -1,11 +1,13 @@ import { Editor } from 'slate' -import { AutomergeEditor } from './automerge-editor' -import withAutomerge, { AutomergeOptions } from './withAutomerge' -import withSocketIO, { - WithSocketIOEditor, - SocketIOPluginOptions -} from './withSocketIO' +import withAutomerge from './withAutomerge' +import { + AutomergeEditor, + AutomergeOptions, + SocketIOPluginOptions, + WithSocketIOEditor +} from './interfaces' +import withSocketIO from './withSocketIO' /** * The `withIOCollaboration` plugin contains collaboration with SocketIO. diff --git a/packages/client/src/withSocketIO.ts b/packages/client/src/withSocketIO.ts index 621f6a2..777507a 100644 --- a/packages/client/src/withSocketIO.ts +++ b/packages/client/src/withSocketIO.ts @@ -1,102 +1,85 @@ import io from 'socket.io-client' -import { AutomergeEditor } from './automerge-editor' - +import Automerge from 'automerge' import { CollabAction } from '@hiveteams/collab-bridge' - -export interface SocketIOPluginOptions { - url: string - connectOpts: SocketIOClient.ConnectOpts - - onConnect?: () => void - onDisconnect?: () => void - - onError?: (msg: string | Error) => void -} - -export interface WithSocketIOEditor { - socket: SocketIOClient.Socket - - connect: () => void - disconnect: () => void - - send: (op: CollabAction) => void - receive: (op: CollabAction) => void - - destroy: () => void -} +import { + AutomergeEditor, + AutomergeOptions, + SocketIOPluginOptions, + WithSocketIOEditor +} from './interfaces' /** * The `withSocketIO` plugin contains SocketIO layer logic. */ const withSocketIO = ( - editor: T, - options: SocketIOPluginOptions + slateEditor: T, + options: SocketIOPluginOptions & AutomergeOptions ) => { - const e = editor as T & WithSocketIOEditor + const { onConnect, onDisconnect, connectOpts, url } = options + const editor = slateEditor as T & WithSocketIOEditor & AutomergeEditor let socket: SocketIOClient.Socket - const { onConnect, onDisconnect, onError, connectOpts, url } = options - /** * Connect to Socket. */ - e.connect = () => { + editor.connect = () => { socket = io(url, { ...connectOpts }) + // On socket io connect, open a new automerge connection socket.on('connect', () => { - e.clientId = socket.id - - e.openConnection() - + editor.clientId = socket.id + editor.openConnection() onConnect && onConnect() }) + // On socket io error socket.on('error', (msg: string) => { - onError && onError(msg) + editor.handleError(msg) }) + // On socket io msg, process the collab operation socket.on('msg', (data: CollabAction) => { - e.receive(data) + editor.receive(data) }) + // On socket io disconnect, cleanup cursor and call the provided onDisconnect callback socket.on('disconnect', () => { - e.gabageCursor() - + editor.gabageCursor() onDisconnect && onDisconnect() }) socket.connect() - return e + return editor } /** * Disconnect from Socket. */ - e.disconnect = () => { + editor.disconnect = () => { socket.removeListener('msg') socket.close() - e.closeConnection() + editor.closeConnection() - return e + return editor } /** * Receive transport msg. */ - e.receive = (msg: CollabAction) => { + editor.receive = (msg: CollabAction) => { switch (msg.type) { case 'operation': - return e.receiveOperation(msg.payload) + return editor.receiveOperation(msg.payload) case 'document': - return e.receiveDocument(msg.payload) + return editor.receiveDocument(msg.payload) } } @@ -104,7 +87,7 @@ const withSocketIO = ( * Send message to socket. */ - e.send = (msg: CollabAction) => { + editor.send = (msg: CollabAction) => { socket.emit('msg', msg) } @@ -112,13 +95,12 @@ const withSocketIO = ( * Close socket and connection. */ - e.destroy = () => { + editor.destroy = () => { socket.close() - e.closeConnection() - e.automergeCleanup() + editor.closeConnection() } - return e + return editor } export default withSocketIO diff --git a/packages/client/tsconfig.json b/packages/client/tsconfig.json index 3f968f9..9b88382 100644 --- a/packages/client/tsconfig.json +++ b/packages/client/tsconfig.json @@ -7,8 +7,9 @@ "outDir": "./lib", "composite": true, "paths": { - "@hiveteams/collab-bridge": ["../../bridge"] + "@hiveteams/collab-bridge": ["../../bridge"], + "@hiveteams/collab-backend": ["../../backend"] } }, - "references": [{ "path": "../bridge" }] + "references": [{ "path": "../bridge" }, { "path": "../backend" }] } diff --git a/packages/example/server.js b/packages/example/server.js index 898dafb..a6606b7 100644 --- a/packages/example/server.js +++ b/packages/example/server.js @@ -1,4 +1,4 @@ -const { SocketIOConnection } = require('@hiveteams/collab-backend') +const { AutomergeCollaboration } = require('@hiveteams/collab-backend') const express = require('express') const defaultValue = [ @@ -36,4 +36,4 @@ const config = { } } -const connection = new SocketIOConnection(config) +const connection = new AutomergeCollaboration(config)