diff --git a/collab/ClientConnection.js b/collab/ClientConnection.js index 39e517b8c..ef212276e 100644 --- a/collab/ClientConnection.js +++ b/collab/ClientConnection.js @@ -1,105 +1,102 @@ -"use strict"; - import EventEmitter from '../util/EventEmitter' import Err from '../util/SubstanceError' -var __id__ = 0; + +let __id__ = 0 /** ClientConnection abstraction. Uses websockets internally */ -function ClientConnection(config) { - ClientConnection.super.apply(this); +class ClientConnection extends EventEmitter { + constructor(config) { + super() - this.__id__ = __id__++; - this.config = config; - this._onMessage = this._onMessage.bind(this); - this._onConnectionOpen = this._onConnectionOpen.bind(this); - this._onConnectionClose = this._onConnectionClose.bind(this); + this.__id__ = __id__++ + this.config = config + this._onMessage = this._onMessage.bind(this) + this._onConnectionOpen = this._onConnectionOpen.bind(this) + this._onConnectionClose = this._onConnectionClose.bind(this) - // Establish websocket connection - this._connect(); -} + // Establish websocket connection + this._connect() + } -ClientConnection.Prototype = function() { - - this._createWebSocket = function() { - throw Err('AbstractMethodError'); - }; + _createWebSocket() { + throw Err('AbstractMethodError') + } /* Initializes a new websocket connection */ - this._connect = function() { - this.ws = this._createWebSocket(); - this.ws.addEventListener('open', this._onConnectionOpen); - this.ws.addEventListener('close', this._onConnectionClose); - this.ws.addEventListener('message', this._onMessage); - }; + _connect() { + this.ws = this._createWebSocket() + this.ws.addEventListener('open', this._onConnectionOpen) + this.ws.addEventListener('close', this._onConnectionClose) + this.ws.addEventListener('message', this._onMessage) + } /* Disposes the current websocket connection */ - this._disconnect = function() { - this.ws.removeEventListener('message', this._onMessage); - this.ws.removeEventListener('open', this._onConnectionOpen); - this.ws.removeEventListener('close', this._onConnectionClose); - this.ws = null; - }; + _disconnect() { + this.ws.removeEventListener('message', this._onMessage) + this.ws.removeEventListener('open', this._onConnectionOpen) + this.ws.removeEventListener('close', this._onConnectionClose) + this.ws = null + } /* Emits open event when connection has been established */ - this._onConnectionOpen = function() { - this.emit('open'); - }; + _onConnectionOpen() { + this.emit('open') + } /* Trigger reconnect on connection close */ - this._onConnectionClose = function() { - this._disconnect(); - this.emit('close'); - console.info('websocket connection closed. Attempting to reconnect in 5s.'); + _onConnectionClose() { + this._disconnect() + this.emit('close') + console.info('websocket connection closed. Attempting to reconnect in 5s.') setTimeout(function() { - this._connect(); - }.bind(this), 5000); - }; + this._connect() + }.bind(this), 5000) + } /* Delegate incoming websocket messages */ - this._onMessage = function(msg) { - msg = this.deserializeMessage(msg.data); - this.emit('message', msg); - }; + _onMessage(msg) { + msg = this.deserializeMessage(msg.data) + this.emit('message', msg) + } /* Send message via websocket channel */ - this.send = function(msg) { + send(msg) { if (!this.isOpen()) { - console.warn('Message could not be sent. Connection is not open.', msg); - return; + console.warn('Message could not be sent. Connection is not open.', msg) + return } - this.ws.send(this.serializeMessage(msg)); - }; + this.ws.send(this.serializeMessage(msg)) + } /* Returns true if websocket connection is open */ - this.isOpen = function() { - return this.ws && this.ws.readyState === 1; - }; + isOpen() { + return this.ws && this.ws.readyState === 1 + } - this.serializeMessage = function(msg) { - return JSON.stringify(msg); - }; + serializeMessage(msg) { + return JSON.stringify(msg) + } - this.deserializeMessage = function(msg) { - return JSON.parse(msg); - }; + deserializeMessage(msg) { + return JSON.parse(msg) + } -}; +} -EventEmitter.extend(ClientConnection); -export default ClientConnection; +export default ClientConnection diff --git a/collab/CollabClient.js b/collab/CollabClient.js index dc6c382cd..cb2deb7e1 100644 --- a/collab/CollabClient.js +++ b/collab/CollabClient.js @@ -1,83 +1,78 @@ -"use strict"; - import EventEmitter from '../util/EventEmitter' -var __id__ = 0; +let __id__ = 0 /** Client for CollabServer API Communicates via websocket for real-time operations */ -function CollabClient(config) { - CollabClient.super.apply(this); - - this.__id__ = __id__++; - this.config = config; - this.connection = config.connection; +class CollabClient extends EventEmitter { + constructor(config) { + super() - // Hard-coded for now - this.scope = 'substance/collab'; + this.__id__ = __id__++ + this.config = config + this.connection = config.connection - // Bind handlers - this._onMessage = this._onMessage.bind(this); - this._onConnectionOpen = this._onConnectionOpen.bind(this); - this._onConnectionClose = this._onConnectionClose.bind(this); + // Hard-coded for now + this.scope = 'substance/collab' - // Connect handlers - this.connection.on('open', this._onConnectionOpen); - this.connection.on('close', this._onConnectionClose); - this.connection.on('message', this._onMessage); -} + // Bind handlers + this._onMessage = this._onMessage.bind(this) + this._onConnectionOpen = this._onConnectionOpen.bind(this) + this._onConnectionClose = this._onConnectionClose.bind(this) -CollabClient.Prototype = function() { + // Connect handlers + this.connection.on('open', this._onConnectionOpen) + this.connection.on('close', this._onConnectionClose) + this.connection.on('message', this._onMessage) + } - this._onConnectionClose = function() { - this.emit('disconnected'); - }; + _onConnectionClose() { + this.emit('disconnected') + } - this._onConnectionOpen = function() { - this.emit('connected'); - }; + _onConnectionOpen() { + this.emit('connected') + } /* Delegate incoming messages from the connection */ - this._onMessage = function(msg) { + _onMessage(msg) { if (msg.scope === this.scope) { - this.emit('message', msg); + this.emit('message', msg) } else { - console.info('Message ignored. Not sent in hub scope', msg); + console.info('Message ignored. Not sent in hub scope', msg) } - }; + } /* Send message via websocket channel */ - this.send = function(msg) { + send(msg) { if (!this.connection.isOpen()) { - console.warn('Message could not be sent. Connection not open.', msg); - return; + console.warn('Message could not be sent. Connection not open.', msg) + return } msg.scope = this.scope; if (this.config.enhanceMessage) { - msg = this.config.enhanceMessage(msg); + msg = this.config.enhanceMessage(msg) } - this.connection.send(msg); - }; + this.connection.send(msg) + } /* Returns true if websocket connection is open */ - this.isConnected = function() { - return this.connection.isOpen(); - }; + isConnected() { + return this.connection.isOpen() + } - this.dispose = function() { - this.connection.off(this); - }; -}; - -EventEmitter.extend(CollabClient); + dispose() { + this.connection.off(this) + } +} -export default CollabClient; +export default CollabClient diff --git a/collab/CollabEngine.js b/collab/CollabEngine.js index 3b45381db..8206c58e5 100644 --- a/collab/CollabEngine.js +++ b/collab/CollabEngine.js @@ -1,5 +1,3 @@ -"use strict"; - import EventEmitter from '../util/EventEmitter' import forEach from 'lodash/forEach' import map from 'lodash/map' @@ -12,97 +10,96 @@ import Err from '../util/SubstanceError' Engine for realizing collaborative editing. Implements the server-methods of the real time editing as a reusable library. */ -function CollabEngine(documentEngine) { - CollabEngine.super.apply(this); - - this.documentEngine = documentEngine; +class CollabEngine extends EventEmitter { + constructor(documentEngine) { + super() - // Active collaborators - this._collaborators = {}; -} + this.documentEngine = documentEngine -CollabEngine.Prototype = function() { + // Active collaborators + this._collaborators = {} + } /* Register collaborator for a given documentId */ - this._register = function(collaboratorId, documentId, selection, collaboratorInfo) { - var collaborator = this._collaborators[collaboratorId]; + _register(collaboratorId, documentId, selection, collaboratorInfo) { + let collaborator = this._collaborators[collaboratorId] if (!collaborator) { collaborator = this._collaborators[collaboratorId] = { collaboratorId: collaboratorId, documents: {} - }; + } } // Extend with collaboratorInfo if available - collaborator.info = collaboratorInfo; + collaborator.info = collaboratorInfo // Register document collaborator.documents[documentId] = { selection: selection - }; - }; + } + } /* Unregister collaborator id from document */ - this._unregister = function(collaboratorId, documentId) { - var collaborator = this._collaborators[collaboratorId]; - delete collaborator.documents[documentId]; - var docCount = Object.keys(collaborator.documents).length; + _unregister(collaboratorId, documentId) { + let collaborator = this._collaborators[collaboratorId] + delete collaborator.documents[documentId] + let docCount = Object.keys(collaborator.documents).length // If there is no doc left, we can remove the entire collaborator entry if (docCount === 0) { - delete this._collaborators[collaboratorId]; + delete this._collaborators[collaboratorId] } - }; + } - this._updateSelection = function(collaboratorId, documentId, sel) { - var docEntry = this._collaborators[collaboratorId].documents[documentId]; - docEntry.selection = sel; - }; + _updateSelection(collaboratorId, documentId, sel) { + let docEntry = this._collaborators[collaboratorId].documents[documentId] + docEntry.selection = sel + } /* Get list of active documents for a given collaboratorId */ - this.getDocumentIds = function(collaboratorId) { - var collaborator = this._collaborators[collaboratorId]; + getDocumentIds(collaboratorId) { + let collaborator = this._collaborators[collaboratorId] if (!collaborator) { // console.log('CollabEngine.getDocumentIds', collaboratorId, 'not found'); // console.log('CollabEngine._collaborators', this._collaborators); - return []; + return [] } - return Object.keys(collaborator.documents); - }; + return Object.keys(collaborator.documents) + } /* Get collaborators for a specific document */ - this.getCollaborators = function(documentId, collaboratorId) { - var collaborators = {}; + getCollaborators(documentId, collaboratorId) { + let collaborators = {} forEach(this._collaborators, function(collab) { - var doc = collab.documents[documentId]; + let doc = collab.documents[documentId] if (doc && collab.collaboratorId !== collaboratorId) { - var entry = { + let entry = { selection: doc.selection, collaboratorId: collab.collaboratorId - }; - entry = extend({}, collab.info, entry); - collaborators[collab.collaboratorId] = entry; + } + entry = extend({}, collab.info, entry) + collaborators[collab.collaboratorId] = entry } - }); - return collaborators; - }; + }) + return collaborators + } /* Get only collaborator ids for a specific document */ - this.getCollaboratorIds = function(documentId, collaboratorId) { - var collaborators = this.getCollaborators(documentId, collaboratorId); + getCollaboratorIds(documentId, collaboratorId) { + let collaborators = this.getCollaborators(documentId, collaboratorId) return map(collaborators, function(c) { - return c.collaboratorId; - }); + return c.collaboratorId + }) }; /* @@ -115,15 +112,15 @@ CollabEngine.Prototype = function() { Note: a client can reconnect having a pending change which is similar to the commit case */ - this.sync = function(args, cb) { + sync(args, cb) { // We now always get a change since the selection should be considered this._sync(args, function(err, result) { - if (err) return cb(err); + if (err) return cb(err) // Registers the collaborator If not already registered for that document - this._register(args.collaboratorId, args.documentId, result.change.after.selection, args.collaboratorInfo); - cb(null, result); - }.bind(this)); - }; + this._register(args.collaboratorId, args.documentId, result.change.after.selection, args.collaboratorInfo) + cb(null, result) + }.bind(this)) + } /* Internal implementation of sync @@ -135,47 +132,47 @@ CollabEngine.Prototype = function() { OUT: version, changes, version */ - this._sync = function(args, cb) { + _sync(args, cb) { // Get latest doc version this.documentEngine.getVersion(args.documentId, function(err, serverVersion) { if (serverVersion === args.version) { // Fast forward update - this._syncFF(args, cb); + this._syncFF(args, cb) } else if (serverVersion > args.version) { // Client changes need to be rebased to latest serverVersion - this._syncRB(args, cb); + this._syncRB(args, cb) } else { cb(new Err('InvalidVersionError', { message: 'Client version greater than server version' - })); + })) } - }.bind(this)); - }; + }.bind(this)) + } /* Update all collaborators selections of a document according to a given change WARNING: This has not been tested quite well */ - this._updateCollaboratorSelections = function(documentId, change) { + _updateCollaboratorSelections(documentId, change) { // By not providing the 2nd argument to getCollaborators the change // creator is also included. - var collaborators = this.getCollaborators(documentId); + let collaborators = this.getCollaborators(documentId) forEach(collaborators, function(collaborator) { if (collaborator.selection) { - var sel = Selection.fromJSON(collaborator.selection); - change = this.deserializeChange(change); - sel = DocumentChange.transformSelection(sel, change); + let sel = Selection.fromJSON(collaborator.selection) + change = this.deserializeChange(change) + sel = DocumentChange.transformSelection(sel, change) // Write back the transformed selection to the server state - this._updateSelection(collaborator.collaboratorId, documentId, sel.toJSON()); + this._updateSelection(collaborator.collaboratorId, documentId, sel.toJSON()) } - }.bind(this)); - }; + }.bind(this)) + } /* Fast forward sync (client version = server version) */ - this._syncFF = function(args, cb) { - this._updateCollaboratorSelections(args.documentId, args.change); + _syncFF(args, cb) { + this._updateCollaboratorSelections(args.documentId, args.change) // HACK: On connect we may receive a nop that only has selection data. // We don't want to store such changes. @@ -187,7 +184,7 @@ CollabEngine.Prototype = function() { // changes: [], serverChange: null, version: args.version - }); + }) } // Store the commit @@ -202,23 +199,23 @@ CollabEngine.Prototype = function() { serverChange: null, // changes: [], // no changes missed in fast-forward scenario version: serverVersion - }); - }); - }; + }) + }) + } /* Rebased sync (client version < server version) */ - this._syncRB = function(args, cb) { + _syncRB(args, cb) { this._rebaseChange({ documentId: args.documentId, change: args.change, version: args.version }, function(err, rebased) { // result has change, changes, version (serverversion) - if (err) return cb(err); + if (err) return cb(err) - this._updateCollaboratorSelections(args.documentId, rebased.change); + this._updateCollaboratorSelections(args.documentId, rebased.change) // HACK: On connect we may receive a nop that only has selection data. // We don't want to store such changes. @@ -229,7 +226,7 @@ CollabEngine.Prototype = function() { change: rebased.change, serverChange: rebased.serverChange, version: rebased.version - }); + }) } // Store the rebased commit @@ -238,15 +235,15 @@ CollabEngine.Prototype = function() { change: rebased.change, // rebased change documentInfo: args.documentInfo }, function(err, serverVersion) { - if (err) return cb(err); + if (err) return cb(err) cb(null, { change: rebased.change, serverChange: rebased.serverChange, // collaborators must be notified version: serverVersion - }); - }); - }.bind(this)); - }; + }) + }) + }.bind(this)) + } /* Rebase change @@ -254,54 +251,52 @@ CollabEngine.Prototype = function() { IN: documentId, change, version (change version) OUT: change, changes (server changes), version (server version) */ - this._rebaseChange = function(args, cb) { + _rebaseChange(args, cb) { this.documentEngine.getChanges({ documentId: args.documentId, sinceVersion: args.version }, function(err, result) { - var B = result.changes.map(this.deserializeChange); - var a = this.deserializeChange(args.change); + let B = result.changes.map(this.deserializeChange) + let a = this.deserializeChange(args.change) // transform changes - DocumentChange.transformInplace(a, B); - var ops = B.reduce(function(ops, change) { - return ops.concat(change.ops); - }, []); - var serverChange = new DocumentChange(ops, {}, {}); + DocumentChange.transformInplace(a, B) + let ops = B.reduce(function(ops, change) { + return ops.concat(change.ops) + }, []) + let serverChange = new DocumentChange(ops, {}, {}) cb(null, { change: this.serializeChange(a), serverChange: this.serializeChange(serverChange), version: result.version - }); - }.bind(this)); - }; + }) + }.bind(this)) + } /* Collaborator leaves a document editing session NOTE: This method is synchronous */ - this.disconnect = function(args) { - this._unregister(args.collaboratorId, args.documentId); - }; + disconnect(args) { + this._unregister(args.collaboratorId, args.documentId) + } /* To JSON */ - this.serializeChange = function(change) { - return change.toJSON(); - }; + serializeChange(change) { + return change.toJSON() + } /* From JSON */ - this.deserializeChange = function(serializedChange) { - var ch = DocumentChange.fromJSON(serializedChange); - return ch; - }; + deserializeChange(serializedChange) { + let ch = DocumentChange.fromJSON(serializedChange) + return ch + } -}; - -EventEmitter.extend(CollabEngine); +} -export default CollabEngine; +export default CollabEngine diff --git a/collab/CollabServer.js b/collab/CollabServer.js index 4250da165..60e1b65b8 100644 --- a/collab/CollabServer.js +++ b/collab/CollabServer.js @@ -1,5 +1,3 @@ -'use strict'; - import Server from './Server' import CollabEngine from './CollabEngine' import Err from '../util/SubstanceError' @@ -8,21 +6,19 @@ import forEach from 'lodash/forEach' /* Implements Substance CollabServer API. */ -function CollabServer(config) { - CollabServer.super.apply(this, arguments); - - this.scope = 'substance/collab'; - this.documentEngine = config.documentEngine; - this.collabEngine = new CollabEngine(this.documentEngine); -} +class CollabServer extends Server { + constructor(config) { + super(config) -CollabServer.Prototype = function() { - var _super = CollabServer.super.prototype; + this.scope = 'substance/collab' + this.documentEngine = config.documentEngine + this.collabEngine = new CollabEngine(this.documentEngine) + } /* Send an error */ - this._error = function(req, res, err) { + _error(req, res, err) { res.error({ type: 'error', error: { @@ -33,80 +29,80 @@ CollabServer.Prototype = function() { }, // errorName: err.name, documentId: req.message.documentId - }); - this.next(req, res); - }; + }) + this.next(req, res) + } /* Configurable authenticate method */ - this.authenticate = function(req, res) { + authenticate(req, res) { if (this.config.authenticate) { this.config.authenticate(req, function(err, session) { if (err) { - console.error(err); + console.error(err) // Send the response with some delay - this._error(req, res, new Err('AuthenticationError', {cause: err})); - return; + this._error(req, res, new Err('AuthenticationError', {cause: err})) + return } - req.setAuthenticated(session); - this.next(req, res); - }.bind(this)); + req.setAuthenticated(session) + this.next(req, res) + }.bind(this)) } else { - _super.authenticate.apply(this, arguments); + super.authenticate.apply(this, arguments); } - }; + } /* Configureable enhanceRequest method */ - this.enhanceRequest = function(req, res) { + enhanceRequest(req, res) { if (this.config.enhanceRequest) { this.config.enhanceRequest(req, function(err) { if (err) { - console.error('enhanceRequest returned an error', err); - this._error(req, res, err); - return; + console.error('enhanceRequest returned an error', err) + this._error(req, res, err) + return } - req.setEnhanced(); - this.next(req, res); - }.bind(this)); + req.setEnhanced() + this.next(req, res) + }.bind(this)) } else { - _super.enhanceRequest.apply(this, arguments); + super.enhanceRequest.apply(this, arguments) } - }; + } /* Called when a collaborator disconnects */ - this.onDisconnect = function(collaboratorId) { + onDisconnect(collaboratorId) { // console.log('CollabServer.onDisconnect ', collaboratorId); // All documents collaborator is currently collaborating to - var documentIds = this.collabEngine.getDocumentIds(collaboratorId); + let documentIds = this.collabEngine.getDocumentIds(collaboratorId) documentIds.forEach(function(documentId) { - this._disconnectDocument(collaboratorId, documentId); - }.bind(this)); - }; + this._disconnectDocument(collaboratorId, documentId) + }.bind(this)) + } /* Execute CollabServer API method based on msg.type */ - this.execute = function(req, res) { - var msg = req.message; - var method = this[msg.type]; + execute(req, res) { + let msg = req.message + let method = this[msg.type] if (method) { - method.call(this, req, res); + method.call(this, req, res) } else { - console.error('Method', msg.type, 'not implemented for CollabServer'); + console.error('Method', msg.type, 'not implemented for CollabServer') } - }; + } /* Client initiates a sync */ - this.sync = function(req, res) { - var args = req.message; + sync(req, res) { + let args = req.message // console.log('CollabServer.connect', args.collaboratorId); @@ -114,12 +110,12 @@ CollabServer.Prototype = function() { this.collabEngine.sync(args, function(err, result) { // result: changes, version, change if (err) { - this._error(req, res, err); - return; + this._error(req, res, err) + return } // Get enhance collaborators (e.g. including some app-specific user-info) - var collaborators = this.collabEngine.getCollaborators(args.documentId, args.collaboratorId); + let collaborators = this.collabEngine.getCollaborators(args.documentId, args.collaboratorId) // Send the response res.send({ @@ -129,7 +125,7 @@ CollabServer.Prototype = function() { version: result.version, serverChange: result.serverChange, collaborators: collaborators - }); + }) // We need to broadcast a new change if there is one // console.log('CollabServer.connect: update is broadcasted to collaborators', Object.keys(collaborators)); @@ -143,34 +139,34 @@ CollabServer.Prototype = function() { // collaboratorId: args.collaboratorId, // All except of receiver record collaborators: this.collabEngine.getCollaborators(args.documentId, collaborator.collaboratorId) - }); - }.bind(this)); - this.next(req, res); - }.bind(this)); - }; + }) + }.bind(this)) + this.next(req, res) + }.bind(this)) + } /* Expcicit disconnect. User wants to exit a collab session. */ - this.disconnect = function(req, res) { - var args = req.message; - var collaboratorId = args.collaboratorId; - var documentId = args.documentId; - this._disconnectDocument(collaboratorId, documentId); + disconnect(req, res) { + let args = req.message + let collaboratorId = args.collaboratorId + let documentId = args.documentId + this._disconnectDocument(collaboratorId, documentId) // Notify client that disconnect has completed successfully res.send({ scope: this.scope, type: 'disconnectDone', documentId: args.documentId - }); - this.next(req, res); - }; + }) + this.next(req, res) + } - this._disconnectDocument = function(collaboratorId, documentId) { - var collaboratorIds = this.collabEngine.getCollaboratorIds(documentId, collaboratorId); + _disconnectDocument(collaboratorId, documentId) { + let collaboratorIds = this.collabEngine.getCollaboratorIds(documentId, collaboratorId) - var collaborators = {}; - collaborators[collaboratorId] = null; + let collaborators = {} + collaborators[collaboratorId] = null this.broadCast(collaboratorIds, { scope: this.scope, @@ -178,15 +174,14 @@ CollabServer.Prototype = function() { documentId: documentId, // Removes the entry collaborators: collaborators - }); + }) // Exit from each document session this.collabEngine.disconnect({ documentId: documentId, collaboratorId: collaboratorId - }); - }; + }) + } -}; +} -Server.extend(CollabServer); -export default CollabServer; +export default CollabServer diff --git a/collab/CollabSession.js b/collab/CollabSession.js index 4d6198209..1b3fc00e5 100644 --- a/collab/CollabSession.js +++ b/collab/CollabSession.js @@ -1,5 +1,3 @@ -'use strict'; - import debounce from 'lodash/debounce' import forEach from 'lodash/forEach' import clone from 'lodash/clone' @@ -15,152 +13,148 @@ import Selection from '../model/Selection' Requires a connected and authenticated collabClient. */ -function CollabSession(doc, config) { - CollabSession.super.call(this, doc, config); +class CollabSession extends DocumentSession { + constructor(doc, config) { + super(doc, config) - config = config || {}; - this.config = config; - this.collabClient = config.collabClient; + config = config || {} + this.config = config + this.collabClient = config.collabClient - if (config.docVersion) { - console.warn('config.docVersion is deprecated: Use config.version instead'); - } + if (config.docVersion) { + console.warn('config.docVersion is deprecated: Use config.version instead') + } - if (config.docVersion) { - console.warn('config.docId is deprecated: Use config.documentId instead'); - } + if (config.docVersion) { + console.warn('config.docId is deprecated: Use config.documentId instead') + } - this.version = config.version; - this.documentId = config.documentId || config.docId; + this.version = config.version + this.documentId = config.documentId || config.docId - if (config.autoSync !== undefined) { - this.autoSync = config.autoSync; - } else { - this.autoSync = true; - } + if (config.autoSync !== undefined) { + this.autoSync = config.autoSync + } else { + this.autoSync = true + } - if (!this.documentId) { - throw new Err('InvalidArgumentsError', {message: 'documentId is mandatory'}); - } + if (!this.documentId) { + throw new Err('InvalidArgumentsError', {message: 'documentId is mandatory'}) + } - if (typeof this.version === undefined) { - throw new Err('InvalidArgumentsError', {message: 'version is mandatory'}); - } + if (typeof this.version === undefined) { + throw new Err('InvalidArgumentsError', {message: 'version is mandatory'}) + } - // Internal state - this._connected = false; // gets flipped to true in syncDone - this._nextChange = null; // next change to be sent over the wire - this._pendingChange = null; // change that is currently being synced - this._error = null; + // Internal state + this._connected = false // gets flipped to true in syncDone + this._nextChange = null // next change to be sent over the wire + this._pendingChange = null // change that is currently being synced + this._error = null - // Note: registering a second document:changed handler where we trigger sync requests - this.doc.on('document:changed', this.afterDocumentChange, this, {priority: -10}); + // Note: registering a second document:changed handler where we trigger sync requests + this.doc.on('document:changed', this.afterDocumentChange, this, {priority: -10}) - // Bind handlers - this._broadCastSelectionUpdateDebounced = debounce(this._broadCastSelectionUpdate, 250); + // Bind handlers + this._broadCastSelectionUpdateDebounced = debounce(this._broadCastSelectionUpdate, 250) - // Keep track of collaborators in a session - this.collaborators = {}; + // Keep track of collaborators in a session + this.collaborators = {} - // This happens on a reconnect - this.collabClient.on('connected', this.onCollabClientConnected, this); - this.collabClient.on('disconnected', this.onCollabClientDisconnected, this); + // This happens on a reconnect + this.collabClient.on('connected', this.onCollabClientConnected, this) + this.collabClient.on('disconnected', this.onCollabClientDisconnected, this) - // Constraints used for computing color indexes - this.__maxColors = 5; - this.__nextColorIndex = 0; - this.collabClient.on('message', this._onMessage.bind(this)); + // Constraints used for computing color indexes + this.__maxColors = 5 + this.__nextColorIndex = 0 + this.collabClient.on('message', this._onMessage.bind(this)) - // Attempt to open a document immediately, but only if the collabClient is - // already connected. If not the _onConnected handler will take care of it - // once websocket connection is ready. - if (this.collabClient.isConnected() && this.autoSync) { - this.sync(); + // Attempt to open a document immediately, but only if the collabClient is + // already connected. If not the _onConnected handler will take care of it + // once websocket connection is ready. + if (this.collabClient.isConnected() && this.autoSync) { + this.sync() + } } -} - -CollabSession.Prototype = function() { - - var _super = CollabSession.super.prototype; /* Unregister event handlers. Call this before throw away a CollabSession reference. Otherwise you will leak memory */ - this.dispose = function() { - this.disconnect(); - this.collabClient.off(this); - }; + dispose() { + this.disconnect() + this.collabClient.off(this) + } /* Explicit disconnect initiated by user */ - this.disconnect = function() { + disconnect() { // Let the server know we no longer want to edit this document - var msg = { + let msg = { type: 'disconnect', documentId: this.documentId - }; + } // We abort pening syncs - this._abortSync(); - this._send(msg); - }; + this._abortSync() + this._send(msg) + } /* Synchronize with collab server */ - this.sync = function() { + sync() { // If there is something to sync and there is no running sync if (this.__canSync()) { - var nextChange = this._getNextChange(); - var msg = { + let nextChange = this._getNextChange() + let msg = { type: 'sync', documentId: this.documentId, version: this.version, change: this.serializeChange(nextChange) - }; + } - this._send(msg); - this._pendingChange = nextChange; + this._send(msg) + this._pendingChange = nextChange // Can be used to reset errors that arised from previous syncs. // When a new sync is started, users can use this event to unset the error - this.emit('sync'); - this._nextChange = null; - this._error = null; + this.emit('sync') + this._nextChange = null + this._error = null } else { - console.error('Can not sync. Either collabClient is not connected or we are already syncing'); + console.error('Can not sync. Either collabClient is not connected or we are already syncing') } - }; + } /* When selection is changed explicitly by the user we broadcast that update to other collaborators */ - this.setSelection = function(sel) { + setSelection(sel) { // We just remember beforeSel on the CollabSession (need for connect use-case) - var beforeSel = this.selection; - _super.setSelection.call(this, sel); - this._broadCastSelectionUpdateDebounced(beforeSel, sel); - }; - - this.getCollaborators = function() { - return this.collaborators; - }; + let beforeSel = this.selection + super.setSelection.call(this, sel) + this._broadCastSelectionUpdateDebounced(beforeSel, sel) + } - this.isConnected = function() { - return this._connected; - }; + getCollaborators() { + return this.collaborators + } + isConnected() { + return this._connected + } - this.serializeChange = function(change) { - return change.toJSON(); - }; + serializeChange(change) { + return change.toJSON() + } - this.deserializeChange = function(serializedChange) { - return DocumentChange.fromJSON(serializedChange); - }; + deserializeChange(serializedChange) { + return DocumentChange.fromJSON(serializedChange) + } /* Message handlers ================ */ @@ -168,50 +162,50 @@ CollabSession.Prototype = function() { /* Dispatching of remote messages. */ - this._onMessage = function(msg) { + _onMessage(msg) { // Skip if message is not addressing this document if (msg.documentId !== this.documentId) { - return false; + return false } // clone the msg to make sure that the original does not get altered - msg = cloneDeep(msg); + msg = cloneDeep(msg) switch (msg.type) { case 'syncDone': - this.syncDone(msg); - break; + this.syncDone(msg) + break case 'syncError': - this.syncError(msg); - break; + this.syncError(msg) + break case 'update': - this.update(msg); - break; + this.update(msg) + break case 'disconnectDone': - this.disconnectDone(msg); - break; + this.disconnectDone(msg) + break case 'error': - this.error(msg); - break; + this.error(msg) + break default: - console.error('CollabSession: unsupported message', msg.type, msg); - return false; + console.error('CollabSession: unsupported message', msg.type, msg) + return false } - return true; - }; + return true + } /* Send message Returns true if sent, false if not sent (e.g. when not connected) */ - this._send = function(msg) { + _send(msg) { if (this.collabClient.isConnected()) { - this.collabClient.send(msg); - return true; + this.collabClient.send(msg) + return true } else { - console.warn('Try not to call _send when disconnected. Skipping message', msg); - return false; + console.warn('Try not to call _send when disconnected. Skipping message', msg) + return false } - }; + } /* Apply remote update @@ -223,40 +217,40 @@ CollabSession.Prototype = function() { If we are currently in the middle of a sync or have local changes we just ignore the update. We will receive all server updates on the next syncDone. */ - this.update = function(args) { + update(args) { // console.log('CollabSession.update(): received remote update', args); - var serverChange = args.change; - var collaborators = args.collaborators; - var serverVersion = args.version; + let serverChange = args.change + let collaborators = args.collaborators + let serverVersion = args.version if (!this._nextChange && !this._pendingChange) { - var oldSelection = this.selection; + let oldSelection = this.selection if (serverChange) { - serverChange = this.deserializeChange(serverChange); - this._applyRemoteChange(serverChange); + serverChange = this.deserializeChange(serverChange) + this._applyRemoteChange(serverChange) } - var newSelection = this.selection; + let newSelection = this.selection if (serverVersion) { - this.version = serverVersion; + this.version = serverVersion } - var update = { + let update = { change: serverChange - }; + } if (newSelection !== oldSelection) { - update.selection = newSelection; + update.selection = newSelection } // collaboratorsChange only contains information about // changed collaborators - var collaboratorsChange = this._updateCollaborators(collaborators); + let collaboratorsChange = this._updateCollaborators(collaborators) if (collaboratorsChange) { - update.collaborators = collaboratorsChange; - this.emit('collaborators:changed'); + update.collaborators = collaboratorsChange + this.emit('collaborators:changed') } - this._triggerUpdateEvent(update, { remote: true }); + this._triggerUpdateEvent(update, { remote: true }) } else { // console.log('skipped remote update. Pending sync or local changes.'); } - }; + } /* Sync has completed @@ -264,175 +258,175 @@ CollabSession.Prototype = function() { We apply server changes that happened in the meanwhile and we update the collaborators (=selections etc.) */ - this.syncDone = function(args) { - var serverChange = args.serverChange; - var collaborators = args.collaborators; - var serverVersion = args.version; + syncDone(args) { + let serverChange = args.serverChange + let collaborators = args.collaborators + let serverVersion = args.version if (serverChange) { - serverChange = this.deserializeChange(serverChange); - this._applyRemoteChange(serverChange); + serverChange = this.deserializeChange(serverChange) + this._applyRemoteChange(serverChange) } - this.version = serverVersion; + this.version = serverVersion // Only apply updated collaborators if there are no local changes // Otherwise they will not be accurate. We can safely skip this // here as we know the next sync will be triggered soon. And if // followed by an idle phase (_nextChange = null) will give us // the latest collaborator records - var collaboratorsChange = this._updateCollaborators(collaborators); + let collaboratorsChange = this._updateCollaborators(collaborators) if (this._nextChange) { - this._transformCollaboratorSelections(this._nextChange); + this._transformCollaboratorSelections(this._nextChange) } // Important: after sync is done we need to reset _pendingChange and _error // In this state we can safely listen to - this._pendingChange = null; - this._error = null; + this._pendingChange = null + this._error = null // Each time the sync worked we consider the system connected - this._connected = true; + this._connected = true - var update = { + let update = { change: serverChange - }; + } if (collaboratorsChange) { - update.collaborators = collaboratorsChange; + update.collaborators = collaboratorsChange } - this._triggerUpdateEvent(update, { remote: true }); + this._triggerUpdateEvent(update, { remote: true }) - this.emit('connected'); + this.emit('connected') // Attempt to sync again (maybe we have new local changes) - this._requestSync(); - }; + this._requestSync() + } /* Handle sync error */ - this.syncError = function(error) { - error('Sync error:', error); - this._abortSync(); - }; + syncError(error) { + error('Sync error:', error) + this._abortSync() + } - this.disconnectDone = function() { + disconnectDone() { // console.log('disconnect done'); // Let the server know we no longer want to edit this document - this._afterDisconnected(); - }; + this._afterDisconnected() + } /* Handle errors. This gets called if any request produced an error on the server. */ - this.error = function(message) { - var error = message.error; - var errorFn = this[error.name]; - var err = Err.fromJSON(error); + error(message) { + let error = message.error + let errorFn = this[error.name] + let err = Err.fromJSON(error) if (!errorFn) { - error('CollabSession: unsupported error', error.name); - return false; + error('CollabSession: unsupported error', error.name) + return false } - this.emit('error', err); - errorFn = errorFn.bind(this); - errorFn(err); - }; + this.emit('error', err) + errorFn = errorFn.bind(this) + errorFn(err) + } /* Event handlers ============== */ - this.afterDocumentChange = function(change, info) { + afterDocumentChange(change, info) { // Record local changes into nextCommit if (!info.remote) { - this._recordChange(change); + this._recordChange(change) } - }; + } /* A new authenticated collabClient connection is available. This happens in a reconnect scenario. */ - this.onCollabClientConnected = function() { + onCollabClientConnected() { // console.log('CollabClient connected'); if (this.autoSync) { - this.sync(); + this.sync() } - }; + } /* Implicit disconnect (server connection drop out) */ - this.onCollabClientDisconnected = function() { + onCollabClientDisconnected() { // console.log('CollabClient disconnected'); - this._abortSync(); + this._abortSync() if (this._connected) { - this._afterDisconnected(); + this._afterDisconnected() } - }; + } /* Internal methods ================ */ - this._commit = function(change, info) { - var selectionHasChanged = this._commitChange(change); + _commit(change, info) { + let selectionHasChanged = this._commitChange(change) - var collaboratorsChange = null; + let collaboratorsChange = null forEach(this.getCollaborators(), function(collaborator) { // transform local version of collaborator selection - var id = collaborator.collaboratorId; - var oldSelection = collaborator.selection; - var newSelection = DocumentChange.transformSelection(oldSelection, change); + let id = collaborator.collaboratorId + let oldSelection = collaborator.selection + let newSelection = DocumentChange.transformSelection(oldSelection, change) if (oldSelection !== newSelection) { - collaboratorsChange = collaboratorsChange || {}; - collaborator = clone(collaborator); - collaborator.selection = newSelection; - collaboratorsChange[id] = collaborator; + collaboratorsChange = collaboratorsChange || {} + collaborator = clone(collaborator) + collaborator.selection = newSelection + collaboratorsChange[id] = collaborator } - }); + }) - var update = { + let update = { change: change - }; + } if (selectionHasChanged) { - update.selection = this.getSelection(); + update.selection = this.getSelection() } if (collaboratorsChange) { - update.collaborators = collaboratorsChange; + update.collaborators = collaboratorsChange } - this._triggerUpdateEvent(update, info); - }; + this._triggerUpdateEvent(update, info) + } /* Apply a change to the document */ - this._applyRemoteChange = function(change) { + _applyRemoteChange(change) { // console.log('CollabSession: applying remote change'); if (change.ops.length > 0) { - this.stage._apply(change); - this.doc._apply(change); + this.stage._apply(change) + this.doc._apply(change) // Only undo+redo history is updated according to the new change - this._transformLocalChangeHistory(change); - this.selection = this._transformSelection(change); + this._transformLocalChangeHistory(change) + this.selection = this._transformSelection(change) } - }; + } /* We record all local changes into a single change (aka commit) that */ - this._recordChange = function(change) { + _recordChange(change) { if (!this._nextChange) { - this._nextChange = change; + this._nextChange = change } else { // Merge new change into nextCommit - this._nextChange.ops = this._nextChange.ops.concat(change.ops); - this._nextChange.after = change.after; + this._nextChange.ops = this._nextChange.ops.concat(change.ops) + this._nextChange.after = change.after } - this._requestSync(); - }; + this._requestSync() + } /* Get next change for sync. @@ -440,39 +434,39 @@ CollabSession.Prototype = function() { If there are no local changes we create a change that only holds the current selection. */ - this._getNextChange = function() { - var nextChange = this._nextChange; + _getNextChange() { + var nextChange = this._nextChange if (!nextChange) { // Change only holds the current selection - nextChange = this._getChangeForSelection(this.selection, this.selection); + nextChange = this._getChangeForSelection(this.selection, this.selection) } - return nextChange; - }; + return nextChange + } /* Send selection update to other collaborators */ - this._broadCastSelectionUpdate = function(beforeSel, afterSel) { + _broadCastSelectionUpdate(beforeSel, afterSel) { if (this._nextChange) { - this._nextChange.after.selection = afterSel; + this._nextChange.after.selection = afterSel } else { - this._nextChange = this._getChangeForSelection(beforeSel, afterSel); + this._nextChange = this._getChangeForSelection(beforeSel, afterSel) } - this._requestSync(); - }; + this._requestSync() + } - this.__canSync = function() { - return this.collabClient.isConnected() && !this._pendingChange; - }; + __canSync() { + return this.collabClient.isConnected() && !this._pendingChange + } /* Triggers a new sync if there is a new change and no pending sync */ - this._requestSync = function() { + _requestSync() { if (this._nextChange && this.__canSync()) { - this.sync(); + this.sync() } - }; + } /* Abots the currently running sync. @@ -480,118 +474,116 @@ CollabSession.Prototype = function() { This is called _onDisconnect and could be called after a sync request times out (not yet implemented) */ - this._abortSync = function() { - var newNextChange = this._nextChange; + _abortSync() { + let newNextChange = this._nextChange if (this._pendingChange) { - newNextChange = this._pendingChange; + newNextChange = this._pendingChange // If we have local changes also, we append them to the new nextChange if (this._nextChange) { - newNextChange.ops = newNextChange.ops.concat(this._nextChange.ops); - newNextChange.after = this._nextChange.after; + newNextChange.ops = newNextChange.ops.concat(this._nextChange.ops) + newNextChange.after = this._nextChange.after } - this._pendingChange = null; + this._pendingChange = null } - this._error = null; - this._nextChange = newNextChange; - }; + this._error = null + this._nextChange = newNextChange + } - this._transformCollaboratorSelections = function(change) { + _transformCollaboratorSelections(change) { // console.log('Transforming selection...', this.__id__); // Transform the selection - var collaborators = this.getCollaborators(); + let collaborators = this.getCollaborators() if (collaborators) { forEach(collaborators, function(collaborator) { - DocumentChange.transformSelection(collaborator.selection, change); - }); + DocumentChange.transformSelection(collaborator.selection, change) + }) } - }; + } - this._updateCollaborators = function(collaborators) { - var collaboratorsChange = {}; + _updateCollaborators(collaborators) { + let collaboratorsChange = {} forEach(collaborators, function(collaborator, collaboratorId) { if (collaborator) { - var oldSelection; - var old = this.collaborators[collaboratorId]; + let oldSelection + let old = this.collaborators[collaboratorId] if (old) { - oldSelection = old.selection; + oldSelection = old.selection } - var newSelection = Selection.fromJSON(collaborator.selection); - newSelection.attach(this.doc); + let newSelection = Selection.fromJSON(collaborator.selection) + newSelection.attach(this.doc) // Assign colorIndex (try to restore from old record) - collaborator.colorIndex = old ? old.colorIndex : this._getNextColorIndex(); - collaborator.selection = newSelection; - this.collaborators[collaboratorId] = collaborator; + collaborator.colorIndex = old ? old.colorIndex : this._getNextColorIndex() + collaborator.selection = newSelection + this.collaborators[collaboratorId] = collaborator if (!newSelection.equals(oldSelection)) { - collaboratorsChange[collaboratorId] = collaborator; + collaboratorsChange[collaboratorId] = collaborator } } else { - collaboratorsChange[collaboratorId] = null; - delete this.collaborators[collaboratorId]; + collaboratorsChange[collaboratorId] = null + delete this.collaborators[collaboratorId] } - }.bind(this)); + }.bind(this)) if (Object.keys(collaboratorsChange).length>0) { - return collaboratorsChange; + return collaboratorsChange } - }; + } /* Sets the correct state after a collab session has been disconnected either explicitly or triggered by a connection drop out. */ - this._afterDisconnected = function() { - var oldCollaborators = this.collaborators; - this.collaborators = {}; - var collaboratorIds = Object.keys(oldCollaborators); + _afterDisconnected() { + let oldCollaborators = this.collaborators + this.collaborators = {} + let collaboratorIds = Object.keys(oldCollaborators) if (collaboratorIds.length > 0) { - var collaboratorsChange = {}; + let collaboratorsChange = {} // when this user disconnects we will need to remove all rendered collaborator infos (such as selection) collaboratorIds.forEach(function(collaboratorId) { - collaboratorsChange[collaboratorId] = null; - }); + collaboratorsChange[collaboratorId] = null + }) this._triggerUpdateEvent({ collaborators: collaboratorsChange - }); + }) } - this._connected = false; - this.emit('disconnected'); - }; + this._connected = false + this.emit('disconnected') + } /* Takes beforeSel + afterSel and wraps it in a no-op DocumentChange */ - this._getChangeForSelection = function(beforeSel, afterSel) { - var change = new DocumentChange([], { + _getChangeForSelection(beforeSel, afterSel) { + let change = new DocumentChange([], { selection: beforeSel }, { selection: afterSel - }); - return change; - }; + }) + return change + } /* Returns true if there are local changes */ - this._hasLocalChanges = function() { - return this._nextChange && this._nextChange.ops.length > 0; - }; + _hasLocalChanges() { + return this._nextChange && this._nextChange.ops.length > 0 + } /* Get color index for rendering cursors and selections in round robin style. Note: This implementation considers a configured maxColors value. The first color will be reused as more then maxColors collaborators arrive. */ - this._getNextColorIndex = function() { - var colorIndex = this.__nextColorIndex; - this.__nextColorIndex = (this.__nextColorIndex + 1) % this.__maxColors; - return colorIndex + 1; // so we can 1..5 instead of 0..4 - }; - -}; + _getNextColorIndex() { + let colorIndex = this.__nextColorIndex + this.__nextColorIndex = (this.__nextColorIndex + 1) % this.__maxColors + return colorIndex + 1 // so we can 1..5 instead of 0..4 + } -DocumentSession.extend(CollabSession); +} -export default CollabSession; +export default CollabSession diff --git a/collab/DocumentClient.js b/collab/DocumentClient.js index 7dd5a89f5..b6a937c77 100644 --- a/collab/DocumentClient.js +++ b/collab/DocumentClient.js @@ -1,16 +1,13 @@ -"use strict"; - import oo from '../util/oo' import request from '../util/request' /* HTTP client for talking with DocumentServer */ -function DocumentClient(config) { - this.config = config; -} - -DocumentClient.Prototype = function() { +class DocumentClient { + constructor(config) { + this.config = config + } /* Create a new document on the server @@ -26,9 +23,9 @@ DocumentClient.Prototype = function() { } }); */ - this.createDocument = function(newDocument, cb) { - request('POST', this.config.httpUrl, newDocument, cb); - }; + createDocument(newDocument, cb) { + request('POST', this.config.httpUrl, newDocument, cb) + } /* Get a document from the server @@ -40,9 +37,9 @@ DocumentClient.Prototype = function() { ``` */ - this.getDocument = function(documentId, cb) { - request('GET', this.config.httpUrl+documentId, null, cb); - }; + getDocument(documentId, cb) { + request('GET', this.config.httpUrl+documentId, null, cb) + } /* Remove a document from the server @@ -54,12 +51,12 @@ DocumentClient.Prototype = function() { ``` */ - this.deleteDocument = function(documentId, cb) { - request('DELETE', this.config.httpUrl+documentId, null, cb); - }; + deleteDocument(documentId, cb) { + request('DELETE', this.config.httpUrl+documentId, null, cb) + } -}; +} -oo.initClass(DocumentClient); +oo.initClass(DocumentClient) -export default DocumentClient; +export default DocumentClient diff --git a/collab/DocumentEngine.js b/collab/DocumentEngine.js index 0de3cd43c..ca94d7ca3 100644 --- a/collab/DocumentEngine.js +++ b/collab/DocumentEngine.js @@ -1,5 +1,3 @@ -"use strict"; - import EventEmitter from '../util/EventEmitter' import JSONConverter from '../model/JSONConverter' import Err from '../util/SubstanceError' @@ -8,23 +6,22 @@ import SnapshotEngine from './SnapshotEngine' /* DocumentEngine */ -function DocumentEngine(config) { - DocumentEngine.super.apply(this); - - this.configurator = config.configurator; - // Where changes are stored - this.documentStore = config.documentStore; - this.changeStore = config.changeStore; - - // SnapshotEngine instance is required - this.snapshotEngine = config.snapshotEngine || new SnapshotEngine({ - configurator: this.configurator, - documentStore: this.documentStore, - changeStore: this.changeStore - }); -} - -DocumentEngine.Prototype = function() { +class DocumentEngine extends EventEmitter { + constructor(config) { + super() + + this.configurator = config.configurator + // Where changes are stored + this.documentStore = config.documentStore + this.changeStore = config.changeStore + + // SnapshotEngine instance is required + this.snapshotEngine = config.snapshotEngine || new SnapshotEngine({ + configurator: this.configurator, + documentStore: this.documentStore, + changeStore: this.changeStore + }) + } /* Creates a new empty or prefilled document @@ -32,15 +29,15 @@ DocumentEngine.Prototype = function() { Writes the initial change into the database. Returns the JSON serialized version, as a starting point */ - this.createDocument = function(args, cb) { - var schema = this.configurator.getSchema(); + createDocument(args, cb) { + let schema = this.configurator.getSchema() if (!schema) { return cb(new Err('SchemaNotFoundError', { message: 'Schema not found for ' + args.schemaName - })); + })) } - var doc = this.configurator.createArticle(); + let doc = this.configurator.createArticle() // TODO: I have the feeling that this is the wrong approach. // While in our tests we have seeds I don't think that this is a general pattern. @@ -62,17 +59,17 @@ DocumentEngine.Prototype = function() { if (err) { return cb(new Err('CreateError', { cause: err - })); + })) } - var converter = new JSONConverter(); + let converter = new JSONConverter(); cb(null, { documentId: docRecord.documentId, data: converter.exportDocument(doc), version: 0 - }); - }.bind(this)); //eslint-disable-line - }; + }) + }.bind(this)) //eslint-disable-line + } /* Get a document snapshot. @@ -80,80 +77,80 @@ DocumentEngine.Prototype = function() { @param args.documentId @param args.version */ - this.getDocument = function(args, cb) { - this.snapshotEngine.getSnapshot(args, cb); - }; + getDocument(args, cb) { + this.snapshotEngine.getSnapshot(args, cb) + } /* Delete document by documentId */ - this.deleteDocument = function(documentId, cb) { + deleteDocument(documentId, cb) { this.changeStore.deleteChanges(documentId, function(err) { if (err) { return cb(new Err('DeleteError', { cause: err - })); + })) } this.documentStore.deleteDocument(documentId, function(err, doc) { if (err) { return cb(new Err('DeleteError', { cause: err - })); + })) } - cb(null, doc); + cb(null, doc) }); - }.bind(this)); - }; + }.bind(this)) + } /* Check if a given document exists */ - this.documentExists = function(documentId, cb) { - this.documentStore.documentExists(documentId, cb); - }; + documentExists(documentId, cb) { + this.documentStore.documentExists(documentId, cb) + } /* Get changes based on documentId, sinceVersion */ - this.getChanges = function(args, cb) { + getChanges(args, cb) { this.documentExists(args.documentId, function(err, exists) { if (err || !exists) { return cb(new Err('ReadError', { message: !exists ? 'Document does not exist' : null, cause: err - })); + })) } - this.changeStore.getChanges(args, cb); - }.bind(this)); - }; + this.changeStore.getChanges(args, cb) + }.bind(this)) + } /* Get version for given documentId */ - this.getVersion = function(documentId, cb) { + getVersion(documentId, cb) { this.documentExists(documentId, function(err, exists) { if (err || !exists) { return cb(new Err('ReadError', { message: !exists ? 'Document does not exist' : null, cause: err - })); + })) } - this.changeStore.getVersion(documentId, cb); - }.bind(this)); - }; + this.changeStore.getVersion(documentId, cb) + }.bind(this)) + } /* Add change to a given documentId args: documentId, change [, documentInfo] */ - this.addChange = function(args, cb) { + addChange(args, cb) { this.documentExists(args.documentId, function(err, exists) { if (err || !exists) { return cb(new Err('ReadError', { message: !exists ? 'Document does not exist' : null, cause: err - })); + })) } this.changeStore.addChange(args, function(err, newVersion) { if (err) return cb(err); @@ -163,19 +160,17 @@ DocumentEngine.Prototype = function() { // Store custom documentInfo info: args.documentInfo }, function(err) { - if (err) return cb(err); + if (err) return cb(err) this.snapshotEngine.requestSnapshot(args.documentId, newVersion, function() { // no matter if errored or not we will complete the addChange // successfully - cb(null, newVersion); - }); - }.bind(this)); - }.bind(this)); - }.bind(this)); - }; - -}; + cb(null, newVersion) + }) + }.bind(this)) + }.bind(this)) + }.bind(this)) + } -EventEmitter.extend(DocumentEngine); +} -export default DocumentEngine; +export default DocumentEngine diff --git a/collab/DocumentServer.js b/collab/DocumentServer.js index ea4336a1a..1fdd09190 100644 --- a/collab/DocumentServer.js +++ b/collab/DocumentServer.js @@ -1,66 +1,64 @@ -'use strict'; - import oo from '../util/oo' /* DocumentServer module. Can be bound to an express instance */ -function DocumentServer(config) { - this.engine = config.documentEngine; - this.path = config.path; -} - -DocumentServer.Prototype = function() { +class DocumentServer { + constructor(config) { + this.engine = config.documentEngine + this.path = config.path + } /* Attach this server to an express instance */ - this.bind = function(app) { - app.post(this.path, this._createDocument.bind(this)); - app.get(this.path + '/:id', this._getDocument.bind(this)); - app.delete(this.path + '/:id', this._deleteDocument.bind(this)); - }; + bind(app) { + app.post(this.path, this._createDocument.bind(this)) + app.get(this.path + '/:id', this._getDocument.bind(this)) + app.delete(this.path + '/:id', this._deleteDocument.bind(this)) + } /* Create a new document, given a schemaName and schemaVersion */ - this._createDocument = function(req, res, next) { - var args = req.body; - var newDoc = { + _createDocument(req, res, next) { + let args = req.body + let newDoc = { schemaName: args.schemaName, // e.g. prose-article info: args.info // optional - }; + } this.engine.createDocument(newDoc, function(err, result) { - if (err) return next(err); - res.json(result); - }); - }; + if (err) return next(err) + res.json(result) + }) + } /* Get a document with given document id */ - this._getDocument = function(req, res, next) { - var documentId = req.params.id; + _getDocument(req, res, next) { + let documentId = req.params.id this.engine.getDocument({ documentId: documentId }, function(err, result) { - if (err) return next(err); - res.json(result); - }); - }; + if (err) return next(err) + res.json(result) + }) + } /* Remove a document with given document id */ - this._deleteDocument = function(req, res, next) { - var documentId = req.params.id; + _deleteDocument(req, res, next) { + let documentId = req.params.id this.engine.deleteDocument(documentId, function(err, result) { - if (err) return next(err); - res.json(result); - }); - }; -}; + if (err) return next(err) + res.json(result) + }) + } +} + +oo.initClass(DocumentServer) -oo.initClass(DocumentServer); -export default DocumentServer; \ No newline at end of file +export default DocumentServer diff --git a/collab/DocumentStore.js b/collab/DocumentStore.js index e309e0e92..214ea69fc 100644 --- a/collab/DocumentStore.js +++ b/collab/DocumentStore.js @@ -1,5 +1,3 @@ -'use strict'; - import oo from '../util/oo' import extend from 'lodash/extend' import Err from '../util/SubstanceError' @@ -9,117 +7,115 @@ import uuid from '../util/uuid' Implements Substance DocumentStore API. This is just a dumb store. No integrity checks are made, as this is the task of DocumentEngine */ -function DocumentStore(config) { - this.config = config; -} - -DocumentStore.Prototype = function() { +class DocumentStore { + constructor(config) { + this.config = config + } /* Create a new document record @return {Object} document record */ - this.createDocument = function(props, cb) { + createDocument(props, cb) { if (!props.documentId) { // We generate a documentId ourselves - props.documentId = uuid(); + props.documentId = uuid() } - var exists = this._documentExists(props.documentId); + let exists = this._documentExists(props.documentId); if (exists) { return cb(new Err('DocumentStore.CreateError', { message: 'Could not create because document already exists.' - })); + })) } - this._createDocument(props); - cb(null, this._getDocument(props.documentId)); - }; + this._createDocument(props) + } /* Get document by documentId */ - this.getDocument = function(documentId, cb) { - var doc = this._getDocument(documentId); + getDocument(documentId, cb) { + let doc = this._getDocument(documentId) if (!doc) { return cb(new Err('DocumentStore.ReadError', { message: 'Document could not be found.' - })); + })) } - cb(null, doc); - }; + cb(null, doc) + } /* Update document record */ - this.updateDocument = function(documentId, newProps, cb) { - var exists = this._documentExists(documentId); + updateDocument(documentId, newProps, cb) { + let exists = this._documentExists(documentId) if (!exists) { return cb(new Err('DocumentStore.UpdateError', { message: 'Document does not exist.' - })); + })) } - this._updateDocument(documentId, newProps); - cb(null, this._getDocument(documentId)); - }; + this._updateDocument(documentId, newProps) + cb(null, this._getDocument(documentId)) + } /* Delete document */ - this.deleteDocument = function(documentId, cb) { - var doc = this._getDocument(documentId); + deleteDocument(documentId, cb) { + let doc = this._getDocument(documentId) if (!doc) { return cb(new Err('DocumentStore.DeleteError', { message: 'Document does not exist.' - })); + })) } - this._deleteDocument(documentId); - cb(null, doc); - }; + this._deleteDocument(documentId) + cb(null, doc) + } /* Returns true if changeset exists */ - this.documentExists = function(documentId, cb) { - cb(null, this._documentExists(documentId)); - }; + documentExists(documentId, cb) { + cb(null, this._documentExists(documentId)) + } /* Seeds the database */ - this.seed = function(documents, cb) { - this._documents = documents; - if (cb) { cb(null); } - return this; - }; + seed(documents, cb) { + this._documents = documents + if (cb) { cb(null) } + return this + } // Handy synchronous helpers // ------------------------- - this._createDocument = function(props) { - this._documents[props.documentId] = props; - }; + _createDocument(props) { + this._documents[props.documentId] = props + } - this._deleteDocument = function(documentId) { - delete this._documents[documentId]; - }; + _deleteDocument(documentId) { + delete this._documents[documentId] + } // Get document record - this._getDocument = function(documentId) { - return this._documents[documentId]; - }; - - this._updateDocument = function(documentId, props) { - var doc = this._documents[documentId]; - extend(doc, props); - }; - - this._documentExists = function(documentId) { - return Boolean(this._documents[documentId]); - }; -}; + _getDocument(documentId) { + return this._documents[documentId] + } + + _updateDocument(documentId, props) { + let doc = this._documents[documentId] + extend(doc, props) + } + + _documentExists(documentId) { + return Boolean(this._documents[documentId]) + } +} +oo.initClass(DocumentStore) -oo.initClass(DocumentStore); -export default DocumentStore; +export default DocumentStore diff --git a/collab/Server.js b/collab/Server.js index bb05f4958..76b633a45 100644 --- a/collab/Server.js +++ b/collab/Server.js @@ -1,4 +1,3 @@ -"use strict"; /* global WeakMap */ import oo from '../util/oo' @@ -10,134 +9,133 @@ import EventEmitter from '../util/EventEmitter' Implements a generic layered architecture */ -function Server(config) { - Server.super.apply(this); +class Server extends EventEmitter { + constructor(config) { + super() - this.config = config; - this._onConnection = this._onConnection.bind(this); -} - -Server.Prototype = function() { + this.config = config + this._onConnection = this._onConnection.bind(this) + } - this.bind = function(wss) { + bind(wss) { if (this.wss) { - throw new Error('Server is already bound to a websocket'); + throw new Error('Server is already bound to a websocket') } - this.wss = wss; - this._connections = new WeakMap(); - this._collaborators = {}; - this.wss.on('connection', this._onConnection); + this.wss = wss + this._connections = new WeakMap() + this._collaborators = {} + this.wss.on('connection', this._onConnection) - var interval = this.config.heartbeat; + let interval = this.config.heartbeat if (interval) { - this._heartbeat = setInterval(this._sendHeartbeat.bind(this), interval); + this._heartbeat = setInterval(this._sendHeartbeat.bind(this), interval) } - this._bound = true; - }; + this._bound = true + } /* NOTE: This method is yet untested */ - this.unbind = function() { + unbind() { if (this._bound) { - this.wss.off('connection', this._onConnection); + this.wss.off('connection', this._onConnection) } else { - throw new Error('Server is not yet bound to a websocket.'); + throw new Error('Server is not yet bound to a websocket.') } - }; + } /* Hook called when a collaborator connects */ - this.onConnection = function(/*collaboratorId*/) { + onConnection(/*collaboratorId*/) { // noop - }; + } /* Hook called when a collaborator disconnects */ - this.onDisconnect = function(/*collaboratorId*/) { + onDisconnect(/*collaboratorId*/) { // noop - }; + } /* Stub implementation for authenticate middleware. Implement your own as a hook */ - this.authenticate = function(req, res) { - req.setAuthenticated(); - this.next(req, res); - }; + authenticate(req, res) { + req.setAuthenticated() + this.next(req, res) + } /* Stub implementation for authorize middleware Implement your own as a hook */ - this.authorize = function(req, res) { - req.setAuthorized(); - this.next(req, res); - }; + authorize(req, res) { + req.setAuthorized() + this.next(req, res) + } /* Ability to enrich the request data */ - this.enhanceRequest = function(req, res) { - req.setEnhanced(); - this.next(req, res); - }; + enhanceRequest(req, res) { + req.setEnhanced() + this.next(req, res) + } /* Executes the API according to the message type Implement your own as a hook */ - this.execute = function(/*req, res*/) { - throw new Error('This method needs to be specified'); - }; + execute(/*req, res*/) { + throw new Error('This method needs to be specified') + } /* Ability to enrich the response data */ - this.enhanceResponse = function(req, res) { - res.setEnhanced(); - this.next(req, res); - }; + enhanceResponse(req, res) { + res.setEnhanced() + this.next(req, res) + } /* When a new collaborator connects we generate a unique id for them */ - this._onConnection = function(ws) { - var collaboratorId = uuid(); - var connection = { + _onConnection(ws) { + let collaboratorId = uuid() + let connection = { collaboratorId: collaboratorId - }; - this._connections.set(ws, connection); + } + this._connections.set(ws, connection) // Mapping to find connection for collaboratorId this._collaborators[collaboratorId] = { connection: ws - }; + } - ws.on('message', this._onMessage.bind(this, ws)); - ws.on('close', this._onClose.bind(this, ws)); - }; + ws.on('message', this._onMessage.bind(this, ws)) + ws.on('close', this._onClose.bind(this, ws)) + } /* When websocket connection closes */ - this._onClose = function(ws) { - var conn = this._connections.get(ws); - var collaboratorId = conn.collaboratorId; + _onClose(ws) { + let conn = this._connections.get(ws) + let collaboratorId = conn.collaboratorId - this.onDisconnect(collaboratorId); + this.onDisconnect(collaboratorId) // Remove the connection records - delete this._collaborators[collaboratorId]; - this._connections.delete(ws); - }; + delete this._collaborators[collaboratorId] + this._connections.delete(ws) + } /* Implements state machine for handling the request response cycle @@ -151,126 +149,126 @@ Server.Prototype = function() { __error -> sendError -> __done __done // end state */ - this.__initial = function(req, res) { - return !req.isAuthenticated && !req.isAuthorized && !res.isReady; - }; + __initial(req, res) { + return !req.isAuthenticated && !req.isAuthorized && !res.isReady + } - this.__authenticated = function(req, res) { - return req.isAuthenticated && !req.isAuthorized && !res.isReady; - }; + __authenticated(req, res) { + return req.isAuthenticated && !req.isAuthorized && !res.isReady + } - this.__authorized = function(req, res) { - return req.isAuthenticated && req.isAuthorized && !req.isEnhanced && !res.isReady; - }; + __authorized(req, res) { + return req.isAuthenticated && req.isAuthorized && !req.isEnhanced && !res.isReady + } - this.__requestEnhanced = function(req, res) { - return req.isAuthenticated && req.isAuthorized && req.isEnhanced && !res.isReady; - }; + __requestEnhanced(req, res) { + return req.isAuthenticated && req.isAuthorized && req.isEnhanced && !res.isReady + } - this.__executed = function(req, res) { + __executed(req, res) { // excecute must call res.send() so res.data is set - return req.isAuthenticated && req.isAuthorized && res.isReady && res.data && !res.isEnhanced; - }; + return req.isAuthenticated && req.isAuthorized && res.isReady && res.data && !res.isEnhanced + } - this.__enhanced = function(req, res) { - return res.isReady && res.isEnhanced && !res.isSent; - }; + __enhanced(req, res) { + return res.isReady && res.isEnhanced && !res.isSent + } - this.__error = function(req, res) { - return res.err && !res.isSent; - }; + __error(req, res) { + return res.err && !res.isSent + } - this.__done = function(req, res) { - return res.isSent; - }; + __done(req, res) { + return res.isSent + } - this.next = function(req, res) { + next(req, res) { if (this.__initial(req, res)) { - this.authenticate(req, res); + this.authenticate(req, res) } else if (this.__authenticated(req, res)) { - this.authorize(req, res); + this.authorize(req, res) } else if (this.__authorized(req, res)) { - this.enhanceRequest(req, res); + this.enhanceRequest(req, res) } else if (this.__requestEnhanced(req, res)) { - this.execute(req, res); + this.execute(req, res) } else if (this.__executed(req, res)) { - this.enhanceResponse(req, res); + this.enhanceResponse(req, res) } else if (this.__enhanced(req, res)) { - this.sendResponse(req, res); + this.sendResponse(req, res) } else if (this.__error(req, res)) { - this.sendError(req, res); + this.sendError(req, res) } else if (this.__done(req,res)) { // console.log('We are done with processing the request.'); } - }; + } /* Send error response */ - this.sendError = function(req, res) { - var collaboratorId = req.message.collaboratorId; - var msg = res.err; - this.send(collaboratorId, msg); - res.setSent(); - this.next(req, res); - }; + sendError(req, res) { + let collaboratorId = req.message.collaboratorId + let msg = res.err + this.send(collaboratorId, msg) + res.setSent() + this.next(req, res) + } /* Sends a heartbeat message to all connected collaborators */ - this._sendHeartbeat = function() { + _sendHeartbeat() { Object.keys(this._collaborators).forEach(function(collaboratorId) { this.send(collaboratorId, { type: 'highfive', scope: '_internal' }); - }.bind(this)); - }; + }.bind(this)) + } /* Send response */ - this.sendResponse = function(req, res) { - var collaboratorId = req.message.collaboratorId; - this.send(collaboratorId, res.data); - res.setSent(); - this.next(req, res); - }; + sendResponse(req, res) { + let collaboratorId = req.message.collaboratorId + this.send(collaboratorId, res.data) + res.setSent() + this.next(req, res) + } - this._isWebsocketOpen = function(ws) { - return ws && ws.readyState === 1; - }; + _isWebsocketOpen(ws) { + return ws && ws.readyState === 1 + } /* Send message to collaborator */ - this.send = function(collaboratorId, message) { + send(collaboratorId, message) { if (!message.scope && this.config.scope) { - message.scope = this.config.scope; + message.scope = this.config.scope } - var ws = this._collaborators[collaboratorId].connection; + let ws = this._collaborators[collaboratorId].connection if (this._isWebsocketOpen(ws)) { - ws.send(this.serializeMessage(message)); + ws.send(this.serializeMessage(message)) } else { - console.error('Server#send: Websocket for collaborator', collaboratorId, 'is no longer open', message); + console.error('Server#send: Websocket for collaborator', collaboratorId, 'is no longer open', message) } - }; + } /* Send message to collaborator */ - this.broadCast = function(collaborators, message) { + broadCast(collaborators, message) { collaborators.forEach(function(collaboratorId) { - this.send(collaboratorId, message); - }.bind(this)); - }; + this.send(collaboratorId, message) + }.bind(this)) + } // Takes a request object - this._processRequest = function(req) { - var res = new ServerResponse(); - this.next(req, res); - }; + _processRequest(req) { + let res = new ServerResponse() + this.next(req, res) + } /* Handling of client messages. @@ -284,81 +282,78 @@ Server.Prototype = function() { The first argument is always the websocket so we can respond to messages after some operations have been performed. */ - this._onMessage = function(ws, msg) { + _onMessage(ws, msg) { // Retrieve the connection data - var conn = this._connections.get(ws); - msg = this.deserializeMessage(msg); + let conn = this._connections.get(ws) + msg = this.deserializeMessage(msg) if (msg.scope === this.scope) { // We attach a unique collaborator id to each message - msg.collaboratorId = conn.collaboratorId; - var req = new ServerRequest(msg, ws); - this._processRequest(req); + msg.collaboratorId = conn.collaboratorId + let req = new ServerRequest(msg, ws) + this._processRequest(req) } - }; - - this.serializeMessage = function(msg) { - return JSON.stringify(msg); - }; + } - this.deserializeMessage = function(msg) { - return JSON.parse(msg); - }; + serializeMessage(msg) { + return JSON.stringify(msg) + } -}; + deserializeMessage(msg) { + return JSON.parse(msg) + } -EventEmitter.extend(Server); +} /* ServerRequest */ -function ServerRequest(message, ws) { - this.message = message; - this.ws = ws; - this.isAuthenticated = false; - this.isAuhorized = false; -} +class ServerRequest { + constructor(message, ws) { + this.message = message + this.ws = ws + this.isAuthenticated = false + this.isAuhorized = false + } -ServerRequest.Prototype = function() { /* Marks a request as authenticated */ - this.setAuthenticated = function(session) { - this.isAuthenticated = true; - this.session = session; - }; + setAuthenticated(session) { + this.isAuthenticated = true + this.session = session + } /* Marks a request as authorized (authorizationData is optional) */ - this.setAuthorized = function(authorizationData) { - this.isAuthorized = true; - this.authorizationData = authorizationData; - }; + setAuthorized(authorizationData) { + this.isAuthorized = true + this.authorizationData = authorizationData + } /* Sets the isEnhanced flag */ - this.setEnhanced = function() { - this.isEnhanced = true; - }; -}; + setEnhanced() { + this.isEnhanced = true + } +} -oo.initClass(ServerRequest); +oo.initClass(ServerRequest) /* ServerResponse */ -function ServerResponse() { - this.isReady = false; // once the response has been set using send - this.isEnhanced = false; // after response has been enhanced by enhancer - this.isSent = false; // after response has been sent - this.err = null; - this.data = null; -} - -ServerResponse.Prototype = function() { +class ServerResponse { + constructor() { + this.isReady = false // once the response has been set using send + this.isEnhanced = false // after response has been enhanced by enhancer + this.isSent = false // after response has been sent + this.err = null + this.data = null + } /* Sends an error response @@ -373,31 +368,31 @@ ServerResponse.Prototype = function() { }); ``` */ - this.error = function(err) { - this.err = err; - this.isReady = true; - }; + error(err) { + this.err = err + this.isReady = true + } /* Send response data */ - this.send = function(data) { - this.data = data; - this.isReady = true; - }; + send(data) { + this.data = data + this.isReady = true + } /* Sets the isEnhanced flag */ - this.setEnhanced = function() { - this.isEnhanced = true; - }; + setEnhanced() { + this.isEnhanced = true + } - this.setSent = function() { - this.isSent = true; - }; -}; + setSent() { + this.isSent = true + } +} -oo.initClass(ServerResponse); +oo.initClass(ServerResponse) -export default Server; +export default Server diff --git a/collab/SnapshotEngine.js b/collab/SnapshotEngine.js index 9aee1f3f2..677bd3aa7 100644 --- a/collab/SnapshotEngine.js +++ b/collab/SnapshotEngine.js @@ -1,40 +1,37 @@ -'use strict'; - import oo from '../util/oo' import JSONConverter from '../model/JSONConverter' -var converter = new JSONConverter(); +let converter = new JSONConverter() import each from 'lodash/each' import Err from '../util/SubstanceError' /** API for creating and retrieving snapshots of documents */ -function SnapshotEngine(config) { - this.configurator = config.configurator; - this.changeStore = config.changeStore; - this.documentStore = config.documentStore; - - // Optional - this.snapshotStore = config.snapshotStore; - // Snapshot creation frequency, - // e.g. if it's equals 15 then every - // 15th version will be saved as snapshot - this.frequency = config.frequency || 1; -} - -SnapshotEngine.Prototype = function() { +class SnapshotEngine { + constructor(config) { + this.configurator = config.configurator + this.changeStore = config.changeStore + this.documentStore = config.documentStore + + // Optional + this.snapshotStore = config.snapshotStore + // Snapshot creation frequency, + // e.g. if it's equals 15 then every + // 15th version will be saved as snapshot + this.frequency = config.frequency || 1 + } /* Returns a snapshot for a given documentId and version */ - this.getSnapshot = function(args, cb) { + getSnapshot(args, cb) { if (!args || !args.documentId) { return cb(new Err('InvalidArgumentsError', { message: 'args requires a documentId' - })); + })) } - this._computeSnapshot(args, cb); - }; + this._computeSnapshot(args, cb) + } /* Called by DocumentEngine.addChange. @@ -45,54 +42,54 @@ SnapshotEngine.Prototype = function() { TODO: this could potentially live in DocumentEngine */ - this.requestSnapshot = function(documentId, version, cb) { + requestSnapshot(documentId, version, cb) { if (this.snapshotStore && version % this.frequency === 0) { this.createSnapshot({ documentId: documentId - }, cb); + }, cb) } else { - cb(null); // do nothing + cb(null) // do nothing } - }; + } /* Creates a snapshot */ - this.createSnapshot = function(args, cb) { + createSnapshot(args, cb) { if (!this.snapshotStore) { throw new Err('SnapshotStoreRequiredError', { message: 'You must provide a snapshot store to be able to create snapshots' - }); + }) } this._computeSnapshot(args, function(err, snapshot) { - if (err) return cb(err); - this.snapshotStore.saveSnapshot(snapshot, cb); - }.bind(this)); - }; + if (err) return cb(err) + this.snapshotStore.saveSnapshot(snapshot, cb) + }.bind(this)) + } /* Compute a snapshot based on the documentId and version (optional) If no version is provided a snaphot for the latest version is created. */ - this._computeSnapshot = function(args, cb) { + _computeSnapshot(args, cb) { this.documentStore.getDocument(args.documentId, function(err, docRecord) { - if (err) return cb(err); + if (err) return cb(err) if (args.version === undefined) { - args.version = docRecord.version; // set version to the latest version + args.version = docRecord.version // set version to the latest version } // We add the docRecord to the args object - args.docRecord = docRecord; + args.docRecord = docRecord if (this.snapshotStore && args.version !== 0) { - this._computeSnapshotSmart(args, cb); + this._computeSnapshotSmart(args, cb) } else { - this._computeSnapshotDumb(args, cb); + this._computeSnapshotDumb(args, cb) } - }.bind(this)); - }; + }.bind(this)) + } /* Used when a snapshot store is present. This way gives a huge performance @@ -102,11 +99,11 @@ SnapshotEngine.Prototype = function() { Now getLatestSnapshot will give us version 15. This requires us to fetch the changes since version 16 and apply those, plus the very new change. */ - this._computeSnapshotSmart = function(args, cb) { - var documentId = args.documentId; - var version = args.version; - var docRecord = args.docRecord; - var doc; + _computeSnapshotSmart(args, cb) { + let documentId = args.documentId + let version = args.version + let docRecord = args.docRecord + let doc // snaphot = null if no snapshot has been found this.snapshotStore.getSnapshot({ @@ -114,23 +111,23 @@ SnapshotEngine.Prototype = function() { version: version, findClosest: true }, function(err, snapshot) { - if (err) return cb(err); + if (err) return cb(err) if (snapshot && version === snapshot.version) { // we alread have a snapshot for this version - return cb(null, snapshot); + return cb(null, snapshot) } - var knownVersion; + let knownVersion if (snapshot) { - knownVersion = snapshot.version; + knownVersion = snapshot.version } else { - knownVersion = 0; // we need to fetch all changes + knownVersion = 0 // we need to fetch all changes } - doc = this._createDocumentInstance(docRecord.schemaName); + doc = this._createDocumentInstance(docRecord.schemaName) if (snapshot) { - doc = converter.importDocument(doc, snapshot.data); + doc = converter.importDocument(doc, snapshot.data) } // Now we get the remaining changes after the known version @@ -139,28 +136,28 @@ SnapshotEngine.Prototype = function() { sinceVersion: knownVersion, // 1 toVersion: version // 2 }, function(err, result) { - if (err) cb(err); + if (err) cb(err) // Apply remaining changes to the doc - this._applyChanges(doc, result.changes); + this._applyChanges(doc, result.changes) // doc here should be already restored - var snapshot = { + let snapshot = { documentId: documentId, version: version, data: converter.exportDocument(doc) - }; - cb(null, snapshot); - }.bind(this)); - }.bind(this)); - }; + } + cb(null, snapshot) + }.bind(this)) + }.bind(this)) + } /* Compute a snapshot in a dumb way by applying the full change history */ - this._computeSnapshotDumb = function(args, cb) { - var documentId = args.documentId; - var version = args.version; - var docRecord = args.docRecord; - var doc; + _computeSnapshotDumb(args, cb) { + let documentId = args.documentId + let version = args.version + let docRecord = args.docRecord + let doc // Get all changes for a document this.changeStore.getChanges({ @@ -168,48 +165,48 @@ SnapshotEngine.Prototype = function() { sinceVersion: 0 }, function(err, result) { if (err) cb(err); - doc = this._createDocumentInstance(docRecord.schemaName); + doc = this._createDocumentInstance(docRecord.schemaName) // Apply remaining changes to the doc - this._applyChanges(doc, result.changes); + this._applyChanges(doc, result.changes) // doc here should be already restored - var snapshot = { + let snapshot = { documentId: documentId, version: version, data: converter.exportDocument(doc) - }; - cb(null, snapshot); - }.bind(this)); - }; + } + cb(null, snapshot) + }.bind(this)) + } /* Based on a given schema create a document instance based on given schema configuration */ - this._createDocumentInstance = function(schemaName) { - var schema = this.configurator.getSchema(); + _createDocumentInstance(schemaName) { + let schema = this.configurator.getSchema() if (schema.name !== schemaName) { throw new Err('SnapshotEngine.SchemaNotFoundError', { message:'Schema ' + schemaName + ' not found' - }); + }) } - var doc = this.configurator.createArticle(); - return doc; - }; + let doc = this.configurator.createArticle() + return doc + } /* Takes a document and applies the given changes */ - this._applyChanges = function(doc, changes) { + _applyChanges(doc, changes) { each(changes, function(change) { each(change.ops, function(op) { - doc.data.apply(op); - }); - }); - }; + doc.data.apply(op) + }) + }) + } -}; +} -oo.initClass(SnapshotEngine); +oo.initClass(SnapshotEngine) -export default SnapshotEngine; +export default SnapshotEngine diff --git a/collab/SnapshotStore.js b/collab/SnapshotStore.js index c5b3ce20f..aadd8f45d 100644 --- a/collab/SnapshotStore.js +++ b/collab/SnapshotStore.js @@ -1,5 +1,3 @@ -'use strict'; - import oo from '../util/oo' import Err from '../util/SubstanceError' @@ -7,15 +5,13 @@ import Err from '../util/SubstanceError' Implements Substance SnapshotStore API. This is just a dumb store. No integrity checks are made, as this is the task of SnapshotEngine */ -function SnapshotStore(config) { - this.config = config; - - // Snapshots will stored here - this._snapshots = {}; -} - -SnapshotStore.Prototype = function() { +class SnapshotStore { + constructor(config) { + this.config = config + // Snapshots will stored here + this._snapshots = {} + } /* Get Snapshot by documentId and version. If no version is provided @@ -23,118 +19,117 @@ SnapshotStore.Prototype = function() { @return {Object} snapshot record */ - this.getSnapshot = function(args, cb) { + getSnapshot(args, cb) { if (!args || !args.documentId) { return cb(new Err('InvalidArgumentsError', { message: 'args require a documentId' - })); + })) } - var documentId = args.documentId; - var version = args.version; - var docEntry = this._snapshots[documentId]; - var result; + let documentId = args.documentId + let version = args.version + let docEntry = this._snapshots[documentId] + let result - if (!docEntry) return cb(null, undefined); + if (!docEntry) return cb(null, undefined) - var availableVersions = Object.keys(docEntry); + let availableVersions = Object.keys(docEntry) // Exit if no versions are available - if (availableVersions.length === 0) return cb(null, undefined); + if (availableVersions.length === 0) return cb(null, undefined) // If no version is given we return the latest version available if (!version) { - var latestVersion = Math.max.apply(null, availableVersions); - result = docEntry[latestVersion]; + let latestVersion = Math.max.apply(null, availableVersions) + result = docEntry[latestVersion] } else { // Attemt to get the version - result = docEntry[version]; + result = docEntry[version] if (!result && args.findClosest) { // We don't have a snaphot for that requested version - var smallerVersions = availableVersions.filter(function(v) { - return parseInt(v, 10) < version; - }); + let smallerVersions = availableVersions.filter(function(v) { + return parseInt(v, 10) < version + }) // Take the closest version if there is any - var clostestVersion = Math.max.apply(null, smallerVersions); - result = docEntry[clostestVersion]; + let clostestVersion = Math.max.apply(null, smallerVersions) + result = docEntry[clostestVersion] } } - cb(null, result); - }; + cb(null, result) + } /* Stores a snapshot for a given documentId and version. Please not that an existing snapshot will be overwritten. */ - this.saveSnapshot = function(args, cb) { - var documentId = args.documentId; - var version = args.version; - var data = args.data; - var docEntry = this._snapshots[documentId]; + saveSnapshot(args, cb) { + let documentId = args.documentId + let version = args.version + let data = args.data + let docEntry = this._snapshots[documentId] if (!docEntry) { - docEntry = this._snapshots[documentId] = {}; + docEntry = this._snapshots[documentId] = {} } docEntry[version] = { documentId: documentId, version: version, data: data - }; - cb(null, docEntry[version]); - }; + } + cb(null, docEntry[version]) + } /* Removes a snapshot for a given documentId + version */ - this.deleteSnaphot = function(documentId, version, cb) { - var docEntry = this._snapshots[documentId]; + deleteSnaphot(documentId, version, cb) { + let docEntry = this._snapshots[documentId] if (!docEntry || !docEntry[version]) { return cb(new Err('DeleteError', { message: 'Snapshot could not be found' - })); + })) } - var snapshot = this._snapshots[documentId][version]; - delete this._snapshots[documentId][version]; - cb(null, snapshot); - }; + let snapshot = this._snapshots[documentId][version] + delete this._snapshots[documentId][version] + cb(null, snapshot) + } /* Deletes all snapshots for a given documentId */ - this.deleteSnapshotsForDocument = function(documentId, cb) { - var docEntry = this._snapshots[documentId]; - var deleteCount = 0; - if (docEntry) deleteCount = Object.keys(docEntry).length; - delete this._snapshots[documentId]; - cb(null, deleteCount); - }; + deleteSnapshotsForDocument(documentId, cb) { + let docEntry = this._snapshots[documentId] + let deleteCount = 0 + if (docEntry) deleteCount = Object.keys(docEntry).length + delete this._snapshots[documentId] + cb(null, deleteCount) + } /* Returns true if a snapshot exists for a certain version */ - this.snapshotExists = function(documentId, version, cb) { - var exists = false; - var docRecord = this._snapshots[documentId]; + snapshotExists(documentId, version, cb) { + let exists = false + let docRecord = this._snapshots[documentId] if (docRecord) { - exists = docRecord[version]; + exists = docRecord[version] } - cb(null, exists); - }; + cb(null, exists) + } /* Seeds the database */ - this.seed = function(snapshots, cb) { - this._snapshots = snapshots; - if (cb) { cb(null); } - return this; - }; - -}; + seed(snapshots, cb) { + this._snapshots = snapshots + if (cb) { cb(null) } + return this + } +} -oo.initClass(SnapshotStore); +oo.initClass(SnapshotStore) -export default SnapshotStore; +export default SnapshotStore diff --git a/collab/WebSocketConnection.js b/collab/WebSocketConnection.js index c388ce58a..dd833c447 100644 --- a/collab/WebSocketConnection.js +++ b/collab/WebSocketConnection.js @@ -1,22 +1,12 @@ -"use strict"; - import ClientConnection from './ClientConnection' /** Browser WebSocket abstraction. Handles reconnects etc. */ -function WebSocketConnection() { - WebSocketConnection.super.apply(this, arguments); -} - -WebSocketConnection.Prototype = function() { - - this._createWebSocket = function() { +class WebSocketConnection extends ClientConnection { + _createWebSocket() { return new window.WebSocket(this.config.wsUrl); - }; - -}; - -ClientConnection.extend(WebSocketConnection); + } +} -export default WebSocketConnection; \ No newline at end of file +export default WebSocketConnection diff --git a/ui/ContainerEditor.js b/ui/ContainerEditor.js index b638692bd..af0eebb5a 100644 --- a/ui/ContainerEditor.js +++ b/ui/ContainerEditor.js @@ -235,13 +235,6 @@ class ContainerEditor extends Surface { return super.isEditable.call(this) && !this.isEmpty() } - /* - TODO: Select first content to be found - */ - selectFirst() { - console.warn('TODO: Implement selection of first content to be found.') - } - /* Register custom editor behavior using this method */