webgme-engine
Version:
WebGME server and Client API without a GUI
557 lines (473 loc) • 27.4 kB
HTML
<!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 < 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] < 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') && !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) &&
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) &&
Object.hasOwn(this.watchers.documents[data.docId], data.watcherId) &&
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) &&
Object.hasOwn(this.watchers.documents[data.docId], data.watcherId) &&
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) && 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 &&
joinData.operations[0].metadata.sessionId === self.watchers.sessionId &&
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) &&
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>