UNPKG

webgme-engine

Version:

WebGME server and Client API without a GUI

557 lines (473 loc) 27.4 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>JSDoc: Source: common/storage/storageclasses/watchers.js</title> <script src="scripts/prettify/prettify.js"> </script> <script src="scripts/prettify/lang-css.js"> </script> <!--[if lt IE 9]> <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script> <![endif]--> <link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css"> <link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css"> </head> <body> <div id="main"> <h1 class="page-title">Source: common/storage/storageclasses/watchers.js</h1> <section> <article> <pre class="prettyprint source linenums"><code>/*globals define*/ /*eslint-env node, browser*/ /** * Provides watching-functionality of the database and specific projects. * Keeps a state of the registered watchers. * * @author pmeijer / https://github.com/pmeijer */ define([ 'q', 'webgme-ot', 'common/storage/constants', 'common/util/guid', 'common/EventDispatcher' ], function (Q, ot, CONSTANTS, GUID, EventDispatcher) { 'use strict'; function StorageWatcher(webSocket, logger, gmeConfig) { EventDispatcher.call(this); // watcher counters determining when to join/leave a room on the sever this.watchers = { sessionId: GUID(), // Need at reconnect since socket.id changes. database: 0, projects: {}, documents: {} }; this.webSocket = webSocket; this.logger = this.logger || logger.fork('storage'); this.gmeConfig = gmeConfig; this.logger.debug('StorageWatcher ctor'); this.connected = false; } // Inherit from the EventDispatcher StorageWatcher.prototype = Object.create(EventDispatcher.prototype); StorageWatcher.prototype.constructor = StorageWatcher; function _splitDocId(docId) { var pieces = docId.split(CONSTANTS.ROOM_DIVIDER); return { projectId: pieces[0], branchName: pieces[1], nodeId: pieces[2], attrName: pieces[3] }; } StorageWatcher.prototype.watchDatabase = function (eventHandler, callback) { this.logger.debug('watchDatabase - handler added'); this.webSocket.addEventListener(CONSTANTS.PROJECT_DELETED, eventHandler); this.webSocket.addEventListener(CONSTANTS.PROJECT_CREATED, eventHandler); this.watchers.database += 1; this.logger.debug('Nbr of database watchers:', this.watchers.database); if (this.watchers.database === 1) { this.logger.debug('First watcher will enter database room.'); return this.webSocket.watchDatabase({join: true}).nodeify(callback); } else { return Q().nodeify(callback); } }; StorageWatcher.prototype.unwatchDatabase = function (eventHandler, callback) { var deferred = Q.defer(); this.logger.debug('unwatchDatabase - handler will be removed'); this.logger.debug('Nbr of database watchers (before removal):', this.watchers.database); this.webSocket.removeEventListener(CONSTANTS.PROJECT_DELETED, eventHandler); this.webSocket.removeEventListener(CONSTANTS.PROJECT_CREATED, eventHandler); this.watchers.database -= 1; if (this.watchers.database === 0) { this.logger.debug('No more watchers will exit database room.'); if (this.connected) { this.webSocket.watchDatabase({join: false}) .then(deferred.resolve) .catch(deferred.reject); } else { deferred.resolve(); } } else if (this.watchers.database &lt; 0) { this.logger.error('Number of database watchers became negative!'); deferred.reject(new Error('Number of database watchers became negative!')); } else { deferred.resolve(); } return deferred.promise.nodeify(callback); }; StorageWatcher.prototype.watchProject = function (projectId, eventHandler, callback) { this.logger.debug('watchProject - handler added for project', projectId); this.webSocket.addEventListener(CONSTANTS.BRANCH_DELETED + projectId, eventHandler); this.webSocket.addEventListener(CONSTANTS.BRANCH_CREATED + projectId, eventHandler); this.webSocket.addEventListener(CONSTANTS.BRANCH_HASH_UPDATED + projectId, eventHandler); this.watchers.projects[projectId] = Object.hasOwn(this.watchers.projects, projectId) ? this.watchers.projects[projectId] + 1 : 1; this.logger.debug('Nbr of watchers for project:', projectId, this.watchers.projects[projectId]); if (this.watchers.projects[projectId] === 1) { this.logger.debug('First watcher will enter project room:', projectId); this.webSocket.watchProject({projectId: projectId, join: true}) .nodeify(callback); } else { return Q().nodeify(callback); } }; StorageWatcher.prototype.unwatchProject = function (projectId, eventHandler, callback) { var deferred = Q.defer(); this.logger.debug('unwatchProject - handler will be removed', projectId); this.logger.debug('Nbr of database watchers (before removal):', projectId, this.watchers.projects[projectId]); this.webSocket.removeEventListener(CONSTANTS.BRANCH_DELETED + projectId, eventHandler); this.webSocket.removeEventListener(CONSTANTS.BRANCH_CREATED + projectId, eventHandler); this.webSocket.removeEventListener(CONSTANTS.BRANCH_HASH_UPDATED + projectId, eventHandler); this.watchers.projects[projectId] = Object.hasOwn(this.watchers.projects, projectId) ? this.watchers.projects[projectId] - 1 : -1; if (this.watchers.projects[projectId] === 0) { this.logger.debug('No more watchers will exit project room:', projectId); delete this.watchers.projects[projectId]; if (this.connected) { this.webSocket.watchProject({projectId: projectId, join: false}) .then(deferred.resolve) .catch(deferred.reject); } else { deferred.resolve(); } } else if (this.watchers.projects[projectId] &lt; 0) { this.logger.error('Number of project watchers became negative!:', projectId); deferred.reject(new Error('Number of project watchers became negative!')); } else { deferred.resolve(); } return deferred.promise.nodeify(callback); }; /** * Start watching the document at the provided context. * @param {object} data * @param {string} data.projectId * @param {string} data.branchName * @param {string} data.nodeId * @param {string} data.attrName * @param {string} data.attrValue - If the first client entering the document the value will be used * @param {function} atOperation - Triggered when other clients made changes * @param {ot.Operation} atOperation.operation - Triggered when other clients' operations were applied * @param {function} atSelection - Triggered when other clients send their selection info * @param {object} atSelection.data * @param {ot.Selection | null} atSelection.data.selection - null is passed when other client leaves * @param {string} atSelection.data.userId - name/id of other user * @param {string} atSelection.data.socketId - unique id of other user * @param {function} [callback] * @param {Error | null} callback.err - If failed to watch the document * @param {object} callback.data * @param {string} callback.data.docId - Id of document * @param {string} callback.data.document - Current document on server * @param {number} callback.data.revision - Revision at server when connecting * @param {object} callback.data.users - Users that were connected when connecting * @returns {Promise} */ StorageWatcher.prototype.watchDocument = function (data, atOperation, atSelection, callback) { var self = this, docUpdateEventName = this.webSocket.getDocumentUpdatedEventName(data), docSelectionEventName = this.webSocket.getDocumentSelectionEventName(data), docId = docUpdateEventName.substring(CONSTANTS.DOCUMENT_OPERATION.length), watcherId = GUID(); data = JSON.parse(JSON.stringify(data)); this.logger.debug('watchDocument - handler added for project', data); this.watchers.documents[docId] = this.watchers.documents[docId] || {}; this.watchers.documents[docId][watcherId] = { eventHandler: function (_ws, eData) { var otClient = self.watchers.documents[eData.docId][watcherId].otClient; self.logger.debug('eventHandler for document', {metadata: eData}); if (eData.watcherId === watcherId) { self.logger.info('event from same watcher, skipping...'); return; } if (eData.operation) { if (self.reconnecting) { // We are reconnecting.. Put these on the queue. self.watchers.documents[docId][watcherId].applyBuffer.push(eData); } else { otClient.applyServer(ot.TextOperation.fromJSON(eData.operation)); } } if (Object.hasOwn(eData, 'selection') &amp;&amp; !self.reconnecting) { atSelection({ selection: eData.selection ? otClient.transformSelection(ot.Selection.fromJSON(eData.selection)) : null, socketId: eData.socketId, userId: eData.userId }); } }, applyBuffer: [], awaitingAck: null }; this.webSocket.addEventListener(docUpdateEventName, this.watchers.documents[docId][watcherId].eventHandler); this.webSocket.addEventListener(docSelectionEventName, this.watchers.documents[docId][watcherId].eventHandler); data.join = true; data.sessionId = this.watchers.sessionId; data.watcherId = watcherId; return this.webSocket.watchDocument(data) .then(function (initData) { self.watchers.documents[initData.docId][watcherId].otClient = new ot.Client(initData.revision); self.watchers.documents[initData.docId][watcherId].otClient.sendOperation = function (revision, operation) { var sendData = { docId: initData.docId, projectId: initData.projectId, branchName: initData.branchName, revision: revision, operation: operation, selection: self.watchers.documents[initData.docId][watcherId].selection, sessionId: self.watchers.sessionId, watcherId: watcherId }; self.watchers.documents[initData.docId][watcherId].awaitingAck = { revision: revision, operation: operation }; self.webSocket.sendDocumentOperation(sendData, function (err) { if (err) { self.logger.error('Failed to sendDocument', err); return; } if (Object.hasOwn(self.watchers.documents, initData.docId) &amp;&amp; Object.hasOwn(self.watchers.documents[initData.docId], watcherId)) { self.watchers.documents[initData.docId][watcherId].awaitingAck = null; self.watchers.documents[initData.docId][watcherId].otClient.serverAck(revision); } else { self.logger.error(new Error('Received document acknowledgement ' + 'after watcher left document ' + initData.docId)); } }); }; self.watchers.documents[initData.docId][watcherId].otClient.applyOperation = atOperation; return initData; }) .nodeify(callback); }; /** * Stop watching the document. * @param {object} data * @param {string} data.docId - document id, if not provided projectId, branchName, nodeId, attrName must be. * @param {string} data.watcherId * @param {string} [data.projectId] * @param {string} [data.branchName] * @param {string} [data.nodeId] * @param {string} [data.attrName] * @param {function} [callback] * @param {Error | null} callback.err - If failed to unwatch the document * @returns {Promise} */ StorageWatcher.prototype.unwatchDocument = function (data, callback) { var deferred = Q.defer(), docUpdateEventName = this.webSocket.getDocumentUpdatedEventName(data), docSelectionEventName = this.webSocket.getDocumentSelectionEventName(data), pieces; if (typeof data.docId === 'string') { pieces = _splitDocId(data.docId); Object.keys(pieces) .forEach(function (key) { data[key] = pieces[key]; }); } else { data.docId = docUpdateEventName.substring(CONSTANTS.DOCUMENT_OPERATION.length); } if (typeof data.watcherId !== 'string') { deferred.reject(new Error('data.watcherId not provided - use the one given at watchDocument.')); } else if (Object.hasOwn(this.watchers.documents, data.docId) === false || Object.hasOwn(this.watchers.documents[data.docId], data.watcherId) === false) { deferred.reject(new Error('Document is not being watched ' + data.docId + ' by watcherId [' + data.watcherId + ']')); } else { // Remove handler from web-socket module. this.webSocket.removeEventListener(docUpdateEventName, this.watchers.documents[data.docId][data.watcherId].eventHandler); this.webSocket.removeEventListener(docSelectionEventName, this.watchers.documents[data.docId][data.watcherId].eventHandler); // "Remove" handlers attached to the otClient. this.watchers.documents[data.docId][data.watcherId].otClient.sendOperation = this.watchers.documents[data.docId][data.watcherId].otClient.applyOperation = function () { }; delete this.watchers.documents[data.docId][data.watcherId]; if (Object.keys(this.watchers.documents[data.docId]).length === 0) { delete this.watchers.documents[data.docId]; } // Finally exit socket.io room on server if connected. if (this.connected) { data.join = false; this.webSocket.watchDocument(data) .then(deferred.resolve) .catch(deferred.reject); } else { deferred.resolve(); } } return deferred.promise.nodeify(callback); }; /** * Send operation made, and optionally selection, on document at docId. * @param {object} data * @param {string} data.docId * @param {string} data.watcherId * @param {ot.TextOperation} data.operation * @param {ot.Selection} [data.selection] */ StorageWatcher.prototype.sendDocumentOperation = function (data) { // TODO: Do we need to add a callback for confirmation here? if (typeof data.watcherId !== 'string') { throw new Error('data.watcherId not provided - use the one given at watchDocument.'); } else if (Object.hasOwn(this.watchers.documents, data.docId) &amp;&amp; Object.hasOwn(this.watchers.documents[data.docId], data.watcherId) &amp;&amp; this.watchers.documents[data.docId][data.watcherId].otClient instanceof ot.Client) { this.watchers.documents[data.docId][data.watcherId].selection = data.selection; this.watchers.documents[data.docId][data.watcherId].otClient.applyClient(data.operation); } else { throw new Error('Document not being watched ' + data.docId + '. (If "watchDocument" was initiated make sure to wait for the callback.)'); } }; /** * Send selection on document at docId. (Will only be transmitted if client is Synchronized.) * @param {object} data * @param {string} data.docId * @param {string} data.watcherId * @param {ot.Selection} data.selection */ StorageWatcher.prototype.sendDocumentSelection = function (data) { var self = this, otClient; if (typeof data.watcherId !== 'string') { throw new Error('data.watcherId not provided - use the one given at watchDocument.'); } else if (Object.hasOwn(this.watchers.documents, data.docId) &amp;&amp; Object.hasOwn(this.watchers.documents[data.docId], data.watcherId) &amp;&amp; this.watchers.documents[data.docId][data.watcherId].otClient instanceof ot.Client) { otClient = this.watchers.documents[data.docId][data.watcherId].otClient; if (otClient.state instanceof ot.Client.Synchronized) { // Only broadcast the selection when synchronized this.webSocket.sendDocumentSelection({ docId: data.docId, watcherId: data.watcherId, revision: otClient.revision, selection: data.selection }, function (err) { if (err) { self.logger.error(err); } }); } } else { throw new Error('Document not being watched ' + data.docId + '. (If "watchDocument" was initiated make sure to wait for the callback.)'); } }; StorageWatcher.prototype._rejoinWatcherRooms = function (callback) { var self = this, promises = [], projectId; // When this is called were are in the self.reconnecting === true state until callback resolved. this.logger.debug('rejoinWatcherRooms'); if (this.watchers.database > 0) { this.logger.debug('Rejoining database room.'); promises.push(Q.ninvoke(this.webSocket, 'watchDatabase', {join: true})); } for (projectId in this.watchers.projects) { if (Object.hasOwn(this.watchers.projects, projectId) &amp;&amp; this.watchers.projects[projectId] > 0) { this.logger.debug('Rejoining project room', projectId, this.watchers.projects[projectId]); promises.push(this.webSocket.watchProject({projectId: projectId, join: true})); } } function rejoinWatchers(docId, watcherIds) { var rejoinData = _splitDocId(docId), watcherId = watcherIds.pop(); rejoinData.docId = docId; rejoinData.rejoin = true; rejoinData.revision = self.watchers.documents[docId][watcherId].otClient.revision; rejoinData.sessionId = self.watchers.sessionId; rejoinData.watcherId = watcherId; return self.webSocket.watchDocument(rejoinData) .then(function (joinData) { var deferred = Q.defer(), awaiting = self.watchers.documents[docId][watcherId].awaitingAck, sendData; function applyFromServer() { joinData.operations.forEach(function (op) { self.watchers.documents[docId][watcherId].otClient.applyServer(op.wrapped); }); self.watchers.documents[docId][watcherId].applyBuffer.forEach(function (op) { self.watchers.documents[docId][watcherId].otClient.applyServer(op); }); self.watchers.documents[docId][watcherId].applyBuffer = []; } if (awaiting === null) { // We had no outstanding operations - apply all from the server. applyFromServer(); deferred.resolve(); } else { // We were awaiting an acknowledgement, did it make it to the server? if (joinData.operations.length > 0 &amp;&amp; joinData.operations[0].metadata.sessionId === self.watchers.sessionId &amp;&amp; joinData.operations[0].metadata.watcherId === watcherId) { // It made it to the server - so send the acknowledgement to the otClient. self.watchers.documents[docId][watcherId].awaitingAck = null; self.watchers.documents[docId][watcherId].otClient.serverAck(awaiting.revision); // Remove it from the operations and apply the other joinData.operations.shift(); applyFromServer(); deferred.resolve(); } else { applyFromServer(); sendData = { docId: docId, projectId: rejoinData.projectId, branchName: rejoinData.branchName, revision: awaiting.revision, operation: awaiting.operation, sessionId: self.watchers.sessionId, watcherId: watcherId }; self.webSocket.sendDocumentOperation(sendData, function (err) { if (err) { deferred.reject(err); return; } if (Object.hasOwn(self.watchers.documents, docId) &amp;&amp; Object.hasOwn(self.watchers.documents[docId], watcherId)) { self.watchers.documents[docId][watcherId].awaitingAck = null; self.watchers.documents[docId][watcherId].otClient.serverAck(sendData.revision); } else { self.logger.error(new Error('Received document acknowledgement ' + 'after leaving document ' + docId)); } deferred.resolve(); }); } } return deferred.promise; }) .then(function () { if (watcherIds.length > 0) { rejoinWatchers(docId, watcherIds); } }); } Object.keys(this.watchers.documents).forEach(function (docId) { promises.push(rejoinWatchers(docId, Object.keys(self.watchers.documents[docId]))); }); return Q.all(promises).nodeify(callback); }; return StorageWatcher; });</code></pre> </article> </section> </div> <nav> <h2><a href="index.html">Home</a></h2><h3>Modules</h3><ul><li><a href="Server_GMEAuth.html">Server:GMEAuth</a></li><li><a href="Server_SafeStorage.html">Server:SafeStorage</a></li><li><a href="Server_UserProject.html">Server:UserProject</a></li><li><a href="module-Core.html">Core</a></li><li><a href="module-Storage.html">Storage</a></li><li><a href="module-crosscuts.html">crosscuts</a></li><li><a href="module-serialization.html">serialization</a></li></ul><h3>Externals</h3><ul><li><a href="external-Promise.html">Promise</a></li></ul><h3>Classes</h3><ul><li><a href="AddOnBase.html">AddOnBase</a></li><li><a href="AddOnUpdateResult.html">AddOnUpdateResult</a></li><li><a href="Artifact.html">Artifact</a></li><li><a href="BlobClient.html">BlobClient</a></li><li><a href="BlobMetadata.html">BlobMetadata</a></li><li><a href="BlobRunPluginClient.html">BlobRunPluginClient</a></li><li><a href="Client.html">Client</a></li><li><a href="Core.html">Core</a></li><li><a href="ExecutorClient.html">ExecutorClient</a></li><li><a href="GMENode.html">GMENode</a></li><li><a href="GmeLogger.html">GmeLogger</a></li><li><a href="InterPluginResult.html">InterPluginResult</a></li><li><a href="JobInfo.html">JobInfo</a></li><li><a href="OutputInfo.html">OutputInfo</a></li><li><a href="PluginBase.html">PluginBase</a></li><li><a href="PluginConfig.html">PluginConfig</a></li><li><a href="PluginMessage.html">PluginMessage</a></li><li><a href="PluginNodeDescription.html">PluginNodeDescription</a></li><li><a href="PluginResult.html">PluginResult</a></li><li><a href="Project.html">Project</a></li><li><a href="ProjectInterface.html">ProjectInterface</a></li><li><a href="Server_GMEAuth-GMEAuth.html">GMEAuth</a></li><li><a href="Server_SafeStorage-SafeStorage.html">SafeStorage</a></li><li><a href="Server_UserProject-UserProject.html">UserProject</a></li><li><a href="WebsocketRouter.html">WebsocketRouter</a></li><li><a href="WebsocketRouterUser.html">WebsocketRouterUser</a></li></ul><h3>Events</h3><ul><li><a href="Client.html#event:BRANCH_CHANGED">BRANCH_CHANGED</a></li><li><a href="Client.html#event:BRANCH_CLOSED">BRANCH_CLOSED</a></li><li><a href="Client.html#event:BRANCH_OPENED">BRANCH_OPENED</a></li><li><a href="Client.html#event:BRANCH_STATUS_CHANGED">BRANCH_STATUS_CHANGED</a></li><li><a href="Client.html#event:CONNECTED_USERS_CHANGED">CONNECTED_USERS_CHANGED</a></li><li><a href="Client.html#event:NETWORK_STATUS_CHANGED">NETWORK_STATUS_CHANGED</a></li><li><a href="Client.html#event:NOTIFICATION">NOTIFICATION</a></li><li><a href="Client.html#event:PLUGIN_FINISHED">PLUGIN_FINISHED</a></li><li><a href="Client.html#event:PLUGIN_INITIATED">PLUGIN_INITIATED</a></li><li><a href="Client.html#event:PLUGIN_NOTIFICATION">PLUGIN_NOTIFICATION</a></li><li><a href="Client.html#event:PROJECT_CLOSED">PROJECT_CLOSED</a></li><li><a href="Client.html#event:PROJECT_OPENED">PROJECT_OPENED</a></li></ul><h3><a href="global.html">Global</a></h3> </nav> <br class="clear"> <footer> Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.2</a> on Fri Jun 21 2024 09:43:40 GMT-0400 (Eastern Daylight Time) </footer> <script> prettyPrint(); </script> <script src="scripts/linenumber.js"> </script> </body> </html>