UNPKG

webgme-engine

Version:

WebGME server and Client API without a GUI

1,023 lines (877 loc) 50.2 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>JSDoc: Source: common/storage/storageclasses/editorstorage.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/editorstorage.js</h1> <section> <article> <pre class="prettyprint source linenums"><code>/*globals define*/ /*eslint-env node*/ /** * This class implements the functionality needed to edit a model in a specific project and branch in a * collaborative fashion. * * It keeps a state of the open projects which in turn keeps track of the open branches. * * Each project is associated with a project-cache which is shared amongst the branches. So switching * between branches is (potentially) an operation that does not require lots of server round-trips. * * It is possible to have multiple projects open and multiple branches within each project. However * one instance of a storage can only hold a single instance of a project (or branch within a project). * * @author pmeijer / https://github.com/pmeijer */ define([ 'common/storage/storageclasses/objectloaders', 'common/storage/constants', 'common/storage/project/project', 'common/storage/project/branch', 'common/util/assert', 'common/util/key', 'common/storage/util', 'q' ], function (StorageObjectLoaders, CONSTANTS, Project, Branch, assert, generateKey, UTIL, Q) { 'use strict'; /** * * @param webSocket * @param mainLogger * @param gmeConfig * @constructor */ function EditorStorage(webSocket, mainLogger, gmeConfig) { var self = this, logger = mainLogger.fork('storage'), projects = {}; self.logger = logger; self.userId = null; self.serverVersion = null; StorageObjectLoaders.call(this, webSocket, mainLogger, gmeConfig); function triggerNetworkChange(connectionState, networkHandler) { networkHandler(connectionState); self.dispatchEvent(CONSTANTS.NETWORK_STATUS_CHANGED, connectionState); } /** * Dig out the context for the server-worker request. Needed to determine if * the request needs be queued on the current commit-queue. * @param {object} swmParams * @returns {object} If the request contains a projectId and (branchName and/or commitHash). It * will return an object with projectId and (branchName and/or commitHash). */ function extractSWMContext(swmParams) { var result = {}; if (swmParams.projectId) { result.projectId = swmParams.projectId; if (swmParams.branchName || swmParams.branch || swmParams.commitHash || swmParams.commit) { // Add any of these. result.branchName = swmParams.branchName || swmParams.branch; result.commitHash = swmParams.commitHash || swmParams.commit; } } else if (swmParams.context &amp;&amp; swmParams.context.managerConfig &amp;&amp; swmParams.context.managerConfig.project) { // This is a plugin request.. result.projectId = swmParams.context.managerConfig.project; result.commitHash = swmParams.context.managerConfig.commitHash; result.branchName = swmParams.context.managerConfig.branchName; } return result; } this.open = function (networkHandler) { webSocket.connect(function (err, connectionState) { if (err) { logger.error(err); triggerNetworkChange(CONSTANTS.CONNECTION_ERROR, networkHandler); } else if (connectionState === CONSTANTS.CONNECTED) { self.connected = true; self.userId = webSocket.userId; self.serverVersion = webSocket.serverVersion; triggerNetworkChange(CONSTANTS.CONNECTED, networkHandler); } else if (connectionState === CONSTANTS.RECONNECTING) { // This is an internal state only to handle rejoining of rooms. // Technically the websocket is connected at this point. self.reconnecting = true; } else if (connectionState === CONSTANTS.RECONNECTED) { self.connected = true; self._rejoinWatcherRooms() .then(function () { return self._rejoinBranchRooms(); }) .then(function () { self.reconnecting = false; triggerNetworkChange(CONSTANTS.RECONNECTED, networkHandler); }) .catch(function (err) { logger.error('failing during reconnect', err); triggerNetworkChange(CONSTANTS.CONNECTION_ERROR, networkHandler); }); } else if (connectionState === CONSTANTS.DISCONNECTED) { self.connected = false; triggerNetworkChange(CONSTANTS.DISCONNECTED, networkHandler); } else if (connectionState === CONSTANTS.INCOMPATIBLE_CONNECTION) { triggerNetworkChange(connectionState, networkHandler); } else if (connectionState === CONSTANTS.JWT_ABOUT_TO_EXPIRE) { triggerNetworkChange(connectionState, networkHandler); } else if (connectionState === CONSTANTS.JWT_EXPIRED) { triggerNetworkChange(connectionState, networkHandler); } else { logger.error('unexpected connection state'); triggerNetworkChange(CONSTANTS.CONNECTION_ERROR, networkHandler); } }); }; this.close = function (callback) { logger.debug('Closing storage, openProjects', Object.keys(projects)); return Q.allSettled(Object.keys(projects) .map(function (projectId) { return self.closeProject(projectId); })) .then(function () { // Remove the handler for the socket.io events 'connect' and 'disconnect'. logger.debug('Removing connect and disconnect events'); webSocket.socket.removeAllListeners('connect'); webSocket.socket.removeAllListeners('disconnect'); // Disconnect from the server. logger.debug('Disconnecting web-socket'); webSocket.disconnect(); self.connected = false; // Remove all local event-listeners. webSocket.clearAllEvents(); self.clearAllEvents(); }) .nodeify(callback); }; this.getToken = function () { return webSocket.ioClient.getToken(); }; this.setToken = function (newToken) { return webSocket.ioClient.setToken(newToken); }; /** * Callback for openProject. * * @callback EditorStorage~openProjectCallback * @param {string} err - error string. * @param {Project} project - the newly opened project. * @param {object} branches - the newly opened project. * @example * // branches is of the form * // { master: '#somevalidhash', b1: '#someothervalidhash' } */ /** * * @param {string} projectId - name of project to open. * @param {EditorStorage~openProjectCallback} - callback */ this.openProject = function (projectId, callback) { var deferred, data = { projectId: projectId }; if (projects[projectId]) { return Q.reject(new Error('project is already open ' + projectId)).nodeify(callback); } webSocket.openProject(data) .spread(function (branches, access) { var project = new Project(projectId, self, logger, gmeConfig); projects[projectId] = project; if (callback) { callback(null, project, branches, access); } else { deferred.resolve([project, branches, access]); } }) .catch(function (err) { if (callback) { callback(err); } else { deferred.reject(err); } }); if (!callback) { deferred = Q.defer(); return deferred.promise; } }; this.closeProject = function (projectId, callback) { logger.debug('closeProject', projectId); if (projects[projectId]) { return Q.allSettled(Object.keys(projects[projectId].branches) .map(function (branchName) { return self.closeBranch(projectId, branchName); })) .then(function () { if (self.connected) { return webSocket.closeProject({projectId: projectId}); } else { logger.debug('Disconnected while closing project.. skipping webSocket request to server.'); } }) .then(function () { delete projects[projectId]; }) .nodeify(callback); } else { logger.warn('Project is not open ', projectId); return Q().nodeify(callback); } }; this.openBranch = function (projectId, branchName, hashUpdateHandler, branchStatusHandler, callback) { var project = projects[projectId], data = { projectId: projectId, branchName: branchName }, deferred, branch; if (!project) { return Q.reject( new Error('Cannot open branch, ' + branchName + ', project ' + projectId + ' is not opened.')) .nodeify(callback); } if (project.branches[branchName]) { return Q.reject(new Error('Branch is already open ' + branchName + ', project: ' + projectId)) .nodeify(callback); } logger.debug('openBranch, calling webSocket openBranch', projectId, branchName); deferred = Q.defer(); webSocket.openBranch(data) .then(function (latestCommit) { var branchHash; branch = new Branch(branchName, project.logger); project.branches[branchName] = branch; // Update state of branch branch.latestCommitData = latestCommit; branchHash = latestCommit.commitObject[CONSTANTS.MONGO_ID]; branch.updateHashes(branchHash, branchHash); // Add handlers to branch and set the remote update handler for the web-socket. branch.addHashUpdateHandler(hashUpdateHandler); branch.addBranchStatusHandler(branchStatusHandler); branch._remoteUpdateHandler = function (_ws, updateData, initCallback) { var j, originHash = updateData.commitObject[CONSTANTS.MONGO_ID]; logger.debug('_remoteUpdateHandler invoked for project, branch', projectId, branchName); for (j = 0; j &lt; updateData.coreObjects.length; j += 1) { if (updateData.coreObjects[j] &amp;&amp; updateData.coreObjects[j].type === 'patch') { project.insertPatchObject(updateData.coreObjects[j]); } else { project.insertObject(updateData.coreObjects[j]); } } branch.queueUpdate(updateData); branch.updateHashes(null, originHash); if (branch.getCommitQueue().length === 0) { if (branch.getUpdateQueue().length === 1) { self._pullNextQueuedCommit(projectId, branchName, initCallback); // hashUpdateHandlers } } else { logger.debug('commitQueue is not empty, only updating originHash.'); } }; branch._remoteUpdateHandler(null, latestCommit, function (err) { webSocket.addEventListener(webSocket.getBranchUpdateEventName(projectId, branchName), branch._remoteUpdateHandler); if (err) { deferred.reject(err); } else { deferred.resolve(latestCommit); } }); }) .catch(deferred.reject); return deferred.promise.nodeify(callback); }; this.closeBranch = function (projectId, branchName, callback) { var project = projects[projectId], branch; logger.debug('closeBranch', projectId, branchName); if (!project) { logger.warn('closeBranch: project is not open', projectId, branchName); return Q(null).nodeify(callback); } branch = project.branches[branchName]; if (!branch) { logger.warn('closeBranch: project does not have given branch.', projectId, branchName); return Q(null).nodeify(callback); } // This will prevent memory leaks and expose if a commit is being // processed at the server this time (see last error in _pushNextQueuedCommit). branch.dispatchBranchStatus(null); // Stop listening to events from the server webSocket.removeEventListener(webSocket.getBranchUpdateEventName(projectId, branchName), branch._remoteUpdateHandler); branch.cleanUp(); if (self.connected) { return webSocket.closeBranch({projectId: projectId, branchName: branchName}) .then(function () { delete project.branches[branchName]; }) .nodeify(callback); } else { logger.debug('Disconnected while closing branch.. skipping webSocket request to server.'); delete project.branches[branchName]; return Q(null).nodeify(callback); } }; this.forkBranch = function (projectId, branchName, forkName, commitHash, callback) { var project = projects[projectId], branch, forkData; this.logger.debug('forkBranch', projectId, branchName, forkName, commitHash); if (!project) { return Q.reject(new Error('Cannot fork branch, ' + branchName + ', project ' + projectId + ' is not opened.')) .nodeify(callback); } branch = project.branches[branchName]; if (!branch) { return Q.reject(new Error('Cannot fork branch, branch is not open ' + branchName + ', project: ' + projectId)) .nodeify(callback); } forkData = branch.getCommitsForNewFork(commitHash, forkName); // commitHash = null defaults to latest commit self.logger.debug('forkBranch - forkData', forkData); if (forkData === false) { return Q.reject(new Error('Could not find specified commitHash: ' + commitHash)).nodeify(callback); } return self.persistCommits(forkData.queue) .then(function () { return self.createBranch(projectId, forkName, forkData.commitHash); }) .then(function () { return forkData.commitHash; }) .nodeify(callback); }; this.persistCommits = function (commitQueue, callback) { var deferred = Q.defer(), commitHash; function commitNext(i) { var currentCommitData = commitQueue[i]; if (i &lt; commitQueue.length) { currentCommitData = commitQueue[i]; logger.debug('persistCommits - commitNext, currentCommitData', currentCommitData); delete currentCommitData.branchName; commitHash = currentCommitData.commitObject[CONSTANTS.MONGO_ID]; webSocket.makeCommit(currentCommitData) .then(function (result) { logger.debug('persistCommits - commit successful, hash', result); commitNext(i += 1); }) .catch(deferred.reject); } else { deferred.resolve(commitHash); } } commitNext(0); return deferred.promise.nodeify(callback); }; this.makeCommit = function (projectId, branchName, parents, rootHash, coreObjects, msg, callback) { var project = projects[projectId], branch, commitId, commitCallback, persistQueueElement = {}, commitData = { rootHash: rootHash, projectId: projectId, commitObject: null, coreObjects: {}, changedNodes: null }, keys = Object.keys(coreObjects), i; //handling patch object creation // console.time('patch-computation'); for (i = 0; i &lt; keys.length; i += 1) { if (UTIL.coreObjectHasOldAndNewData(coreObjects[keys[i]])) { // Patch type object. persistQueueElement[keys[i]] = coreObjects[keys[i]].newData; if (keys[i] === rootHash) { // console.time('root-patch-computation'); } commitData.coreObjects[keys[i]] = UTIL.getPatchObject(coreObjects[keys[i]].oldData, coreObjects[keys[i]].newData); //if (keys[i] === rootHash) { // console.timeEnd('root-patch-computation'); //} } else if (coreObjects[keys[i]].newData &amp;&amp; coreObjects[keys[i]].newHash) { // A new object with no previous data (send the entire data). commitData.coreObjects[keys[i]] = coreObjects[keys[i]].newData; persistQueueElement[keys[i]] = coreObjects[keys[i]].newData; } else { // A regular object. commitData.coreObjects[keys[i]] = coreObjects[keys[i]]; persistQueueElement[keys[i]] = coreObjects[keys[i]]; } } // console.timeEnd('patch-computation'); // console.time('getChangedNodes'); commitData.changedNodes = UTIL.getChangedNodes(commitData.coreObjects, rootHash); // console.timeEnd('getChangedNodes'); commitData.commitObject = self._getCommitObject(projectId, parents, commitData.rootHash, msg); if (project) { project.insertObject(commitData.commitObject); commitId = commitData.commitObject[CONSTANTS.MONGO_ID]; commitCallback = function commitCallback() { delete project.projectCache.queuedPersists[commitId]; self.logger.debug('Removed now persisted core-objects from cache: ', Object.keys(project.projectCache.queuedPersists).length); callback.apply(null, arguments); }; project.projectCache.queuedPersists[commitId] = persistQueueElement; logger.debug('Queued non-persisted core-objects in cache: ', Object.keys(project.projectCache.queuedPersists).length); } else { commitCallback = callback; } if (typeof branchName === 'string') { commitData.branchName = branchName; branch = project ? project.branches[branchName] : null; } logger.debug('makeCommit', commitData); if (branch) { logger.debug('makeCommit, branch is open will commit using commitQueue. branchName:', branchName); self._commitToBranch(projectId, branchName, commitData, parents[0], commitCallback); } else { webSocket.makeCommit(commitData, commitCallback); } return commitData.commitObject; }; this.setBranchHash = function (projectId, branchName, newHash, oldHash, callback) { var project = projects[projectId]; logger.debug('setBranchHash', projectId, branchName, newHash, oldHash); if (project &amp;&amp; project.branches[branchName]) { logger.debug('setBranchHash, branch is open, will notify other local users about change'); project.loadObject(newHash, function (err, commitObject) { var commitData; if (err) { logger.error('setBranchHash, failed to load in commitObject'); //branch.dispatchBranchStatus(CONSTANTS.BRANCH_STATUS.ERROR, err); callback(err); return; } logger.debug('setBranchHash, loaded commitObject'); commitData = { projectId: projectId, branchName: branchName, coreObjects: {}, changedNodes: null, commitObject: commitObject, oldHash: oldHash }; self._commitToBranch(projectId, branchName, commitData, oldHash, callback); }); } else { StorageObjectLoaders.prototype.setBranchHash.call(self, projectId, branchName, newHash, oldHash) .nodeify(callback); } }; this.simpleRequest = function (parameters, callback) { // This method is overridden here in order to avoid worker-requests // to be sent out referencing commits that haven't made it to the server yet. var context = extractSWMContext(parameters), commitHash, deferred, queuedInBranch; if (context.projectId &amp;&amp; projects[context.projectId]) { // The request deals with a currently opened project - let's see if there is // a commitHash and/or branch associated with the request.. if (context.commitHash) { commitHash = context.commitHash; if (context.branchName &amp;&amp; projects[context.projectId].branches[context.branchName] &amp;&amp; projects[context.projectId].branches[context.branchName].getQueuedHashes() .indexOf(context.commitHash) > -1) { // Since both commitHash and branchName was specified and the commitHash was queued // in that branch - this is the branch to pick. queuedInBranch = context.branchName; } else { // No branch was specified - let's see if the commit is queued in any opened branch. // (Typically there's really only one open.) Object.keys(projects[context.projectId].branches) .forEach(function (branchName) { if (projects[context.projectId].branches[branchName].getQueuedHashes() .indexOf(context.commitHash) > -1) { queuedInBranch = branchName; } }); } } else if (context.branchName &amp;&amp; projects[context.projectId].branches[context.branchName]) { // There is no specific commit-associated with request. However since branchName was passed // we can only assume that it should run on the last commit in that branch. commitHash = projects[context.projectId].branches[context.branchName].getQueuedHashes()[0]; if (commitHash) { queuedInBranch = context.branchName; } } if (queuedInBranch) { deferred = Q.defer(); projects[context.projectId].branches[queuedInBranch].queueWorkerRequest(commitHash, { release: function () { StorageObjectLoaders.prototype.simpleRequest.call(self, parameters) .then(deferred.resolve) .catch(deferred.reject); }, abort: function () { deferred.reject(new Error('Queued worker request was aborted. Commit ' + commitHash + ' in branch [' + queuedInBranch + '] never made it to the server.')); } }); return deferred.promise.nodeify(callback); } } return StorageObjectLoaders.prototype.simpleRequest.call(self, parameters).nodeify(callback); }; this._commitToBranch = function (projectId, branchName, commitData, oldCommitHash, callback) { var project = projects[projectId], newCommitHash = commitData.commitObject._id, branch = project.branches[branchName], wasFirstInQueue, eventData = { commitData: commitData, local: true }; logger.debug('_commitToBranch, [oldCommitHash, localHash]', oldCommitHash, branch.getLocalHash()); if (oldCommitHash === branch.getLocalHash()) { branch.updateHashes(newCommitHash, null); branch.queueCommit(commitData, callback); if (branch.inSync === false) { branch.dispatchBranchStatus(CONSTANTS.BRANCH_STATUS.AHEAD_NOT_SYNC); } else { branch.dispatchBranchStatus(CONSTANTS.BRANCH_STATUS.AHEAD_SYNC); } // Get the queue length before dispatching because within the asynchrony, // the queue may get longer and we end up never pushing any commit. wasFirstInQueue = branch.getCommitQueue().length === 1; branch.dispatchHashUpdate(eventData, function (err, proceed) { logger.debug('_commitToBranch, dispatchHashUpdate done. [err, proceed]', err, proceed); if (err) { branch.dispatchBranchStatus(CONSTANTS.BRANCH_STATUS.ERROR, err); callback(new Error('Commit failed being loaded in users: ' + err)); } else if (proceed === true) { if (wasFirstInQueue) { logger.debug('_commitToBranch, commit was first in queue - will start pushing commit'); self._pushNextQueuedCommit(projectId, branchName); } else { logger.debug('_commitToBranch, commit was NOT first in queue'); } } else { callback(new Error('Commit halted when loaded in users (proceed was not true).')); } }); } else { // The current user is behind the local branch, e.g. plugin trying to save after client changes. logger.warn('_commitToBranch, incoming commit parent was not the same as the localHash ' + 'for the branch, commit will be canceled!'); callback(null, {status: CONSTANTS.CANCELED, hash: newCommitHash}); } }; this._pushNextQueuedCommit = function (projectId, branchName) { var project = projects[projectId], branch = project.branches[branchName], commitData; logger.debug('_pushNextQueuedCommit, length=', branch.getCommitQueue().length); commitData = branch.getFirstCommit(); logger.debug('_pushNextQueuedCommit, makeCommit [from# -> to#]', commitData.commitObject.parents[0], commitData.commitObject._id); webSocket.makeCommit(commitData, function (err, result) { var mergeCommitData; if (err) { logger.error('makeCommit failed', err); } if (branch.isOpen) { branch.callbackQueue[0](err, result); if (!err &amp;&amp; result) { branch.commitInserted(result.hash); if (result.status === CONSTANTS.SYNCED) { branch.inSync = true; branch.updateHashes(null, result.hash); branch.getFirstCommit(true); if (branch.getCommitQueue().length === 0) { branch.dispatchBranchStatus(CONSTANTS.BRANCH_STATUS.SYNC); } else { branch.dispatchBranchStatus(CONSTANTS.BRANCH_STATUS.AHEAD_SYNC); self._pushNextQueuedCommit(projectId, branchName); } } else if (result.status === CONSTANTS.MERGED) { branch.inSync = true; branch.updateHashes(null, result.mergeHash); if (branch.getCommitQueue().length === 1) { // Finds the MERGED commit-data and clears the update-queue. mergeCommitData = branch.getMergedCommit(result.mergeHash); branch.dispatchBranchStatus(CONSTANTS.BRANCH_STATUS.MERGING); branch.dispatchHashUpdate({commitData: mergeCommitData, local: false}, function (err, proceed) { branch.getFirstCommit(true); if (err) { logger.error('Loading of merged commit failed with error', err, {metadata: mergeCommitData}); branch.dispatchBranchStatus(CONSTANTS.BRANCH_STATUS.ERROR, err); } else if (proceed === true) { logger.debug('Merged commit was successfully loaded, updating localHash.'); branch.updateHashes(result.mergeHash, null); // TODO: What if a commit is made during the hashUpdate? branch.dispatchBranchStatus(CONSTANTS.BRANCH_STATUS.SYNC); return; } else { logger.warn('Loading of update commit was aborted', {metadata: result.mergeHash}); } } ); } else { branch.getFirstCommit(true); branch.dispatchBranchStatus(CONSTANTS.BRANCH_STATUS.MERGING); self._pushNextQueuedCommit(projectId, branchName); } } else if (result.status === CONSTANTS.FORKED) { branch.inSync = false; branch.dispatchBranchStatus(CONSTANTS.BRANCH_STATUS.AHEAD_NOT_SYNC); } else { err = new Error('Unsupported commit status ' + result.status); logger.error(err); branch.dispatchBranchStatus(CONSTANTS.BRANCH_STATUS.ERROR, err); } } else { branch.dispatchBranchStatus(CONSTANTS.BRANCH_STATUS.ERROR, err || new Error('No result at commit.')); } } else { logger.error('_pushNextQueuedCommit returned from server but the branch was closed, ' + 'the branch has probably been closed while waiting for the response.', projectId, branchName); } }); }; this._pullNextQueuedCommit = function (projectId, branchName, callback) { assert(Object.hasOwn(projects, projectId), 'Project not opened: ' + projectId); var project = projects[projectId], branch = project.branches[branchName], error, updateData; if (!branch) { error = new Error('Branch, ' + branchName + ', not in project ' + projectId + '.'); if (callback) { callback(error); return; } else { throw error; } } logger.debug('About to update, updateQueue', {metadata: branch.getUpdateQueue()}); if (branch.getUpdateQueue().length === 0) { logger.debug('No queued updates, returns'); branch.dispatchBranchStatus(CONSTANTS.BRANCH_STATUS.SYNC); if (callback) { callback(null); } return; } updateData = branch.getFirstUpdate(); if (branch.isOpen) { branch.dispatchBranchStatus(CONSTANTS.BRANCH_STATUS.PULLING); branch.dispatchHashUpdate({commitData: updateData, local: false}, function (err, proceed) { var originHash = updateData.commitObject[CONSTANTS.MONGO_ID]; if (err) { logger.error('Loading of update commit failed with error', err, {metadata: updateData}); branch.dispatchBranchStatus(CONSTANTS.BRANCH_STATUS.ERROR, err); } else if (proceed === true) { logger.debug('New commit was successfully loaded, updating localHash.'); branch.updateHashes(originHash, null); branch.getFirstUpdate(true); if (branch.getCommitQueue().length === 0) { self._pullNextQueuedCommit(projectId, branchName, callback); } return; } else { logger.warn('Loading of update commit was aborted', {metadata: updateData}); } if (callback) { callback(new Error('Loading the first commit was aborted')); } }); } else { logger.error('_pullNextQueuedCommit returned from server but the branch was closed.', projectId, branchName); } }; this._getCommitObject = function (projectId, parents, rootHash, msg) { msg = msg || 'n/a'; var commitObj = { root: rootHash, parents: parents, updater: [self.userId], time: Date.now(), message: msg, type: CONSTANTS.COMMIT_TYPE, __v: CONSTANTS.VERSION }, commitHash = '#' + generateKey(commitObj, gmeConfig); commitObj[CONSTANTS.MONGO_ID] = commitHash; return commitObj; }; this._rejoinBranchRooms = function (callback) { var projectId, project, branchName, branchRooms = [], promises = []; logger.debug('_rejoinBranchRooms'); function afterRejoin(projectId, branchName) { var project = projects[projectId], deferred = Q.defer(); logger.debug('_rejoinBranchRooms, rejoined branch room', projectId, branchName); if (project) { project.getBranchHash(branchName) .then(function (branchHash) { var branch = project.branches[branchName], queuedCommitHash; logger.debug('_rejoinBranchRooms received branchHash', projectId, branchName, branchHash); if (!branch) { throw new Error('_rejoinBranchRooms branch was closed ' + projectId + ':' + branchName); } if (branch.getCommitQueue().length > 0) { queuedCommitHash = branch.getFirstCommit().commitObject._id; logger.debug('_rejoinBranchRooms, commits were queued length=, firstQueuedCommitHash', branch.getCommitQueue().length, queuedCommitHash); project.getCommonAncestorCommit(branchHash, queuedCommitHash) .then(function (commonCommitHash) { var result; // The commit made it to the server but the acknowledgement was // interrupted by the disconnect. logger.debug('_rejoinBranchRooms getCommonAncestorCommit', projectId, branchName, commonCommitHash); if (branch.isOpen === false) { throw new Error('_rejoinBranchRooms branch was closed ' + projectId + ':' + branchName); } function dispatchSynced() { result = {status: CONSTANTS.SYNCED, hash: branchHash}; branch.callbackQueue[0](null, result); branch.inSync = true; branch.updateHashes(null, branchHash); branch.getFirstCommit(true); if (branch.getCommitQueue().length === 0) { branch.dispatchBranchStatus(CONSTANTS.BRANCH_STATUS.SYNC); } else { branch.dispatchBranchStatus(CONSTANTS.BRANCH_STATUS.AHEAD_SYNC); self._pushNextQueuedCommit(projectId, branchName); } } function dispatchForked() { result = {status: CONSTANTS.FORKED, hash: branchHash}; branch.callbackQueue[0](null, result); branch.inSync = false; branch.dispatchBranchStatus(CONSTANTS.BRANCH_STATUS.AHEAD_NOT_SYNC); } // c - the commit made by this storage // H - the head of the branch if (commonCommitHash === queuedCommitHash) { // The commit is (or was) in sync with the branch. // Hc or H // | c // | | // In case two the next commit made will be forked. dispatchSynced(); } else if (commonCommitHash === branchHash) { // The branch has moved back since the commit was made. // Treat it like the commit was forked. // c // H dispatchForked(); } else { // The branch has moved forward in a different direction. // c H // \ / dispatchForked(); } deferred.resolve(); }) .catch(function (err) { try { if (err.message.indexOf('Commit object does not exist [' + queuedCommitHash) > -1) { // Commit never made it to the server - push it. logger.debug('First queued commit never made it to the server - push!'); self._pushNextQueuedCommit(projectId, branchName); deferred.resolve(); } else { deferred.reject(err); } } catch (err) { deferred.reject(err); } }); } else { logger.debug('_rejoinBranchRooms, no commits were queued during disconnect.'); deferred.resolve(); } }) .catch(function (err) { deferred.reject(err); }); } else { deferred.reject(new Error('_rejoinBranchRooms project was closed ' + projectId + ':' + branchName)); } return deferred.promise; } for (projectId in projects) { if (Object.hasOwn(projects, projectId)) { project = projects[projectId]; logger.debug('_rejoinBranchRooms found project', projectId); for (branchName in project.branches) { if (Object.hasOwn(project.branches, branchName)) { logger.debug('_rejoinBranchRooms joining branch', projectId, branchName); branchRooms.push({ projectId: projectId, branchName: branchName }); promises.push(webSocket.watchBranch({ projectId: projectId, branchName: branchName, join: true })); } } } } return Q.all(promises) .then(function () { return Q.all(branchRooms.map(function (data) { // Deal with commit queue for each room after rejoining. return afterRejoin(data.projectId, data.branchName); })); }) .nodeify(callback); }; this.sendWsRouterMessage = webSocket.sendWsRouterMessage; this.onWebsocketRouterMessage = webSocket.onWebsocketRouterMessage; this.CONSTANTS = CONSTANTS; } EditorStorage.prototype = Object.create(StorageObjectLoaders.prototype); EditorStorage.prototype.constructor = EditorStorage; EditorStorage.CONSTANTS = CONSTANTS; return EditorStorage; }); </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