diff --git a/src/public/app/services/tab_manager.js b/src/public/app/services/tab_manager.js index 2947886f6..15f939bfe 100644 --- a/src/public/app/services/tab_manager.js +++ b/src/public/app/services/tab_manager.js @@ -7,11 +7,14 @@ import treeService from "./tree.js"; import utils from "./utils.js"; import NoteContext from "./note_context.js"; import appContext from "./app_context.js"; +import Mutex from "../utils/mutex.js"; export default class TabManager extends Component { constructor() { super(); + this.mutex = new Mutex(); + this.activeNtxId = null; // elements are arrays of note contexts for each tab (one main context + subcontexts [splits]) @@ -292,51 +295,55 @@ export default class TabManager extends Component { } async removeNoteContext(ntxId) { - const noteContextToRemove = this.getNoteContextById(ntxId); + // removing note context is async process which can take some time, if users presses CTRL-W quickly, two + // close events could interleave which would then lead to attempting to activate already removed context. + await this.mutex.runExclusively(async () => { + const noteContextToRemove = this.getNoteContextById(ntxId); - if (noteContextToRemove.isMainContext()) { - // forbid removing last main note context - // this was previously allowed (was replaced with empty tab) but this proved to be prone to race conditions - const mainNoteContexts = this.getNoteContexts().filter(nc => nc.isMainContext()); + if (noteContextToRemove.isMainContext()) { + // forbid removing last main note context + // this was previously allowed (was replaced with empty tab) but this proved to be prone to race conditions + const mainNoteContexts = this.getNoteContexts().filter(nc => nc.isMainContext()); - if (mainNoteContexts.length === 1) { - mainNoteContexts[0].setEmpty(); - return; + if (mainNoteContexts.length === 1) { + mainNoteContexts[0].setEmpty(); + return; + } } - } - // close dangling autocompletes after closing the tab - $(".aa-input").autocomplete("close"); + // close dangling autocompletes after closing the tab + $(".aa-input").autocomplete("close"); - const noteContextsToRemove = noteContextToRemove.getSubContexts(); - const ntxIdsToRemove = noteContextsToRemove.map(nc => nc.ntxId); + const noteContextsToRemove = noteContextToRemove.getSubContexts(); + const ntxIdsToRemove = noteContextsToRemove.map(nc => nc.ntxId); - await this.triggerEvent('beforeTabRemove', { ntxIds: ntxIdsToRemove }); + await this.triggerEvent('beforeTabRemove', { ntxIds: ntxIdsToRemove }); - if (!noteContextToRemove.isMainContext()) { - await this.activateNoteContext(noteContextToRemove.getMainContext().ntxId); - } - else if (this.mainNoteContexts.length <= 1) { - await this.openAndActivateEmptyTab(); - } - else if (ntxIdsToRemove.includes(this.activeNtxId)) { - const idx = this.mainNoteContexts.findIndex(nc => nc.ntxId === noteContextToRemove.ntxId); - - if (idx === this.mainNoteContexts.length - 1) { - await this.activatePreviousTabCommand(); + if (!noteContextToRemove.isMainContext()) { + await this.activateNoteContext(noteContextToRemove.getMainContext().ntxId); } - else { - await this.activateNextTabCommand(); + else if (this.mainNoteContexts.length <= 1) { + await this.openAndActivateEmptyTab(); } - } + else if (ntxIdsToRemove.includes(this.activeNtxId)) { + const idx = this.mainNoteContexts.findIndex(nc => nc.ntxId === noteContextToRemove.ntxId); - this.children = this.children.filter(nc => !ntxIdsToRemove.includes(nc.ntxId)); + if (idx === this.mainNoteContexts.length - 1) { + await this.activatePreviousTabCommand(); + } + else { + await this.activateNextTabCommand(); + } + } - this.recentlyClosedTabs.push(noteContextsToRemove); + this.children = this.children.filter(nc => !ntxIdsToRemove.includes(nc.ntxId)); - this.triggerEvent('noteContextRemoved', {ntxIds: ntxIdsToRemove}); + this.recentlyClosedTabs.push(noteContextsToRemove); - this.tabsUpdate.scheduleUpdate(); + this.triggerEvent('noteContextRemoved', {ntxIds: ntxIdsToRemove}); + + this.tabsUpdate.scheduleUpdate(); + }); } tabReorderEvent({ntxIdsInOrder}) { diff --git a/src/public/app/utils/mutex.js b/src/public/app/utils/mutex.js new file mode 100644 index 000000000..a48b2660e --- /dev/null +++ b/src/public/app/utils/mutex.js @@ -0,0 +1,28 @@ +export default class Mutex { + constructor() { + this.current = Promise.resolve(); + } + + /** @returns {Promise} */ + lock() { + let resolveFun; + const subPromise = new Promise(resolve => resolveFun = () => resolve()); + // Caller gets a promise that resolves when the current outstanding lock resolves + const newPromise = this.current.then(() => resolveFun); + // Don't allow the next request until the new promise is done + this.current = subPromise; + // Return the new promise + return newPromise; + }; + + async runExclusively(cb) { + const unlock = await this.lock(); + + try { + await cb(); + } + finally { + unlock(); + } + } +}