Skip to content

Commit c95b938

Browse files
Allow queueing dictionary update & delete together (#1561)
* dictionary update queue * naming * update conditions * lint * remove unneeded async await * allow queueing delete and update in 1 queue * await database update before running next task * remove cmt * naming * Rename var * move await logic to correct place * use deferPromise * move promise to _deleteDictionaryInternal * removeconsole.log * preserve update button after database update
1 parent 7110544 commit c95b938

File tree

4 files changed

+149
-40
lines changed

4 files changed

+149
-40
lines changed

ext/js/pages/settings/dictionary-controller.js

Lines changed: 105 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import * as ajvSchemas0 from '../../../lib/validate-schemas.js';
2020
import {EventListenerCollection} from '../../core/event-listener-collection.js';
2121
import {readResponseJson} from '../../core/json.js';
2222
import {log} from '../../core/log.js';
23+
import {deferPromise} from '../../core/utilities.js';
2324
import {compareRevisions} from '../../dictionary/dictionary-data-util.js';
2425
import {DictionaryWorker} from '../../dictionary/dictionary-worker.js';
2526
import {querySelectorNotNull} from '../../dom/query-selector.js';
@@ -32,14 +33,17 @@ class DictionaryEntry {
3233
* @param {DocumentFragment} fragment
3334
* @param {number} index
3435
* @param {import('dictionary-importer').Summary} dictionaryInfo
36+
* @param {string | null} updateDownloadUrl
3537
*/
36-
constructor(dictionaryController, fragment, index, dictionaryInfo) {
38+
constructor(dictionaryController, fragment, index, dictionaryInfo, updateDownloadUrl) {
3739
/** @type {DictionaryController} */
3840
this._dictionaryController = dictionaryController;
3941
/** @type {number} */
4042
this._index = index;
4143
/** @type {import('dictionary-importer').Summary} */
4244
this._dictionaryInfo = dictionaryInfo;
45+
/** @type {string | null} */
46+
this._updateDownloadUrl = updateDownloadUrl;
4347
/** @type {EventListenerCollection} */
4448
this._eventListeners = new EventListenerCollection();
4549
/** @type {?import('dictionary-database').DictionaryCountGroup} */
@@ -86,6 +90,7 @@ class DictionaryEntry {
8690
this._outdatedButton.hidden = (version >= 3);
8791
this._priorityInput.dataset.setting = `dictionaries[${index}].priority`;
8892
this._enabledCheckbox.dataset.setting = `dictionaries[${index}].enabled`;
93+
this._showUpdatesAvailableButton();
8994
this._eventListeners.addEventListener(this._enabledCheckbox, 'settingChanged', this._onEnabledChanged.bind(this), false);
9095
this._eventListeners.addEventListener(this._menuButton, 'menuOpen', this._onMenuOpen.bind(this), false);
9196
this._eventListeners.addEventListener(this._menuButton, 'menuClose', this._onMenuClose.bind(this), false);
@@ -122,6 +127,11 @@ class DictionaryEntry {
122127
this._enabledCheckbox.checked = value;
123128
}
124129

130+
/** */
131+
hideUpdatesAvailableButton() {
132+
this._updatesAvailable.hidden = true;
133+
}
134+
125135
/**
126136
* @returns {Promise<boolean>}
127137
*/
@@ -147,21 +157,38 @@ class DictionaryEntry {
147157

148158
const downloadUrl = latestDownloadUrl ?? currentDownloadUrl;
149159

150-
this._updatesAvailable.dataset.downloadUrl = downloadUrl;
151-
this._updatesAvailable.hidden = false;
160+
this._updateDownloadUrl = downloadUrl;
161+
this._showUpdatesAvailableButton();
152162
return true;
153163
}
154164

165+
/**
166+
* @returns {string | null}
167+
*/
168+
get updateDownloadUrl() {
169+
return this._updateDownloadUrl;
170+
}
171+
155172
// Private
156173

174+
/** */
175+
_showUpdatesAvailableButton() {
176+
if (this._updateDownloadUrl === null || this._dictionaryController.isDictionaryInTaskQueue(this.dictionaryTitle)) {
177+
return;
178+
}
179+
this._updatesAvailable.dataset.downloadUrl = this._updateDownloadUrl;
180+
this._updatesAvailable.hidden = false;
181+
}
182+
157183
/**
158184
* @param {import('popup-menu').MenuOpenEvent} e
159185
*/
160186
_onMenuOpen(e) {
161187
const bodyNode = e.detail.menu.bodyNode;
162188
const count = this._dictionaryController.dictionaryOptionCount;
163189
this._setMenuActionEnabled(bodyNode, 'moveTo', count > 1);
164-
this._setMenuActionEnabled(bodyNode, 'delete', !this._dictionaryController.isDictionaryInDeleteQueue(this.dictionaryTitle));
190+
const deleteDisabled = this._dictionaryController.isDictionaryInTaskQueue(this.dictionaryTitle);
191+
this._setMenuActionEnabled(bodyNode, 'delete', !deleteDisabled);
165192
}
166193

167194
/**
@@ -504,10 +531,12 @@ export class DictionaryController {
504531
this._allCheckbox = querySelectorNotNull(document, '#all-dictionaries-enabled');
505532
/** @type {?DictionaryExtraInfo} */
506533
this._extraInfo = null;
534+
/** @type {import('dictionary-controller.js').DictionaryTask[]} */
535+
this._dictionaryTaskQueue = [];
507536
/** @type {boolean} */
508-
this._isDeleting = false;
509-
/** @type {string[]} */
510-
this._dictionaryDeleteQueue = [];
537+
this._isTaskQueueRunning = false;
538+
/** @type {(() => void) | null} */
539+
this._onDictionariesUpdate = null;
511540
}
512541

513542
/** @type {import('./modal-controller.js').ModalController} */
@@ -738,6 +767,10 @@ export class DictionaryController {
738767
this._dictionaries = dictionaries;
739768

740769
await this._updateEntries();
770+
771+
if (this._onDictionariesUpdate) {
772+
this._onDictionariesUpdate();
773+
}
741774
}
742775

743776
/** */
@@ -754,7 +787,10 @@ export class DictionaryController {
754787
if (dictionaries === null) { return; }
755788
this._updateMainDictionarySelectOptions(dictionaries);
756789

790+
/** @type {Map<string, string | null>} */
791+
const dictionaryUpdateDownloadUrlMap = new Map();
757792
for (const entry of this._dictionaryEntries) {
793+
dictionaryUpdateDownloadUrlMap.set(entry.dictionaryTitle, entry.updateDownloadUrl);
758794
entry.cleanup();
759795
}
760796
this._dictionaryEntries = [];
@@ -784,8 +820,9 @@ export class DictionaryController {
784820
for (let i = 0, ii = dictionaryOptionsArray.length; i < ii; ++i) {
785821
const {name} = dictionaryOptionsArray[i];
786822
const dictionaryInfo = dictionaryInfoMap.get(name);
823+
const updateDownloadUrl = dictionaryUpdateDownloadUrlMap.get(name) ?? null;
787824
if (typeof dictionaryInfo === 'undefined') { continue; }
788-
this._createDictionaryEntry(i, dictionaryInfo);
825+
this._createDictionaryEntry(i, dictionaryInfo, updateDownloadUrl);
789826
}
790827
}
791828

@@ -842,11 +879,12 @@ export class DictionaryController {
842879
const modal = /** @type {import('./modal.js').Modal} */ (this._deleteDictionaryModal);
843880
modal.setVisible(false);
844881

845-
const title = modal.node.dataset.dictionaryTitle;
846-
if (typeof title !== 'string') { return; }
882+
const dictionaryTitle = modal.node.dataset.dictionaryTitle;
883+
if (typeof dictionaryTitle !== 'string') { return; }
847884
delete modal.node.dataset.dictionaryTitle;
848885

849-
void this._enqueueDictionaryDelete(title);
886+
void this._enqueueTask({type: 'delete', dictionaryTitle});
887+
this._hideUpdatesAvailableButton(dictionaryTitle);
850888
}
851889

852890
/**
@@ -858,12 +896,25 @@ export class DictionaryController {
858896
const modal = /** @type {import('./modal.js').Modal} */ (this._updateDictionaryModal);
859897
modal.setVisible(false);
860898

861-
const title = modal.node.dataset.dictionaryTitle;
899+
const dictionaryTitle = modal.node.dataset.dictionaryTitle;
862900
const downloadUrl = modal.node.dataset.downloadUrl;
863-
if (typeof title !== 'string') { return; }
901+
if (typeof dictionaryTitle !== 'string') { return; }
864902
delete modal.node.dataset.dictionaryTitle;
865903

866-
void this._updateDictionary(title, downloadUrl);
904+
void this._enqueueTask({type: 'update', dictionaryTitle, downloadUrl});
905+
this._hideUpdatesAvailableButton(dictionaryTitle);
906+
}
907+
908+
/**
909+
* @param {string} dictionaryTitle
910+
*/
911+
_hideUpdatesAvailableButton(dictionaryTitle) {
912+
for (const entry of this._dictionaryEntries) {
913+
if (entry.dictionaryTitle === dictionaryTitle) {
914+
entry.hideUpdatesAvailableButton();
915+
break;
916+
}
917+
}
867918
}
868919

869920
/**
@@ -954,7 +1005,7 @@ export class DictionaryController {
9541005

9551006
/** */
9561007
async _checkForUpdates() {
957-
if (this._dictionaries === null || this._checkingIntegrity || this._checkingUpdates || this._isDeleting) { return; }
1008+
if (this._dictionaries === null || this._checkingIntegrity || this._checkingUpdates || this._isTaskQueueRunning) { return; }
9581009
let hasUpdates;
9591010
try {
9601011
this._checkingUpdates = true;
@@ -977,7 +1028,7 @@ export class DictionaryController {
9771028

9781029
/** */
9791030
async _checkIntegrity() {
980-
if (this._dictionaries === null || this._checkingIntegrity || this._checkingUpdates || this._isDeleting) { return; }
1031+
if (this._dictionaries === null || this._checkingIntegrity || this._checkingUpdates || this._isTaskQueueRunning) { return; }
9811032

9821033
try {
9831034
this._checkingIntegrity = true;
@@ -1033,11 +1084,12 @@ export class DictionaryController {
10331084
/**
10341085
* @param {number} index
10351086
* @param {import('dictionary-importer').Summary} dictionaryInfo
1087+
* @param {string|null} updateDownloadUrl
10361088
*/
1037-
_createDictionaryEntry(index, dictionaryInfo) {
1089+
_createDictionaryEntry(index, dictionaryInfo, updateDownloadUrl) {
10381090
const fragment = this.instantiateTemplateFragment('dictionary');
10391091

1040-
const entry = new DictionaryEntry(this, fragment, index, dictionaryInfo);
1092+
const entry = new DictionaryEntry(this, fragment, index, dictionaryInfo, updateDownloadUrl);
10411093
this._dictionaryEntries.push(entry);
10421094
entry.prepare();
10431095

@@ -1053,30 +1105,41 @@ export class DictionaryController {
10531105
* @param {string} dictionaryTitle
10541106
* @returns {boolean}
10551107
*/
1056-
isDictionaryInDeleteQueue(dictionaryTitle) {
1057-
return this._dictionaryDeleteQueue.includes(dictionaryTitle);
1108+
isDictionaryInTaskQueue(dictionaryTitle) {
1109+
return this._dictionaryTaskQueue.some((task) => task.dictionaryTitle === dictionaryTitle);
10581110
}
10591111

10601112
/**
1061-
* @param {string} dictionaryTitle
1113+
* @param {import('dictionary-controller.js').DictionaryTask} task
10621114
*/
1063-
async _enqueueDictionaryDelete(dictionaryTitle) {
1064-
if (this.isDictionaryInDeleteQueue(dictionaryTitle)) { return; }
1065-
this._dictionaryDeleteQueue.push(dictionaryTitle);
1066-
if (this._isDeleting) { return; }
1067-
while (this._dictionaryDeleteQueue.length > 0) {
1068-
const title = this._dictionaryDeleteQueue[0];
1069-
if (!title) { continue; }
1070-
await this._deleteDictionary(title);
1071-
void this._dictionaryDeleteQueue.shift();
1115+
_enqueueTask(task) {
1116+
if (this.isDictionaryInTaskQueue(task.dictionaryTitle)) { return; }
1117+
this._dictionaryTaskQueue.push(task);
1118+
void this._runTaskQueue();
1119+
}
1120+
1121+
1122+
/** */
1123+
async _runTaskQueue() {
1124+
if (this._isTaskQueueRunning) { return; }
1125+
this._isTaskQueueRunning = true;
1126+
while (this._dictionaryTaskQueue.length > 0) {
1127+
const task = this._dictionaryTaskQueue[0];
1128+
if (task.type === 'delete') {
1129+
await this._deleteDictionary(task.dictionaryTitle);
1130+
} else if (task.type === 'update') {
1131+
await this._updateDictionary(task.dictionaryTitle, task.downloadUrl);
1132+
}
1133+
void this._dictionaryTaskQueue.shift();
10721134
}
1135+
this._isTaskQueueRunning = false;
10731136
}
10741137

10751138
/**
10761139
* @param {string} dictionaryTitle
10771140
*/
10781141
async _deleteDictionary(dictionaryTitle) {
1079-
if (this._isDeleting || this._checkingIntegrity) { return; }
1142+
if (this._checkingIntegrity) { return; }
10801143

10811144
const index = this._dictionaryEntries.findIndex((entry) => entry.dictionaryTitle === dictionaryTitle);
10821145
if (index < 0) { return; }
@@ -1089,7 +1152,6 @@ export class DictionaryController {
10891152
const statusLabels = /** @type {NodeListOf<HTMLElement>} */ (document.querySelectorAll(`${progressSelector} .progress-status`));
10901153
const prevention = this._settingsController.preventPageExit();
10911154
try {
1092-
this._isDeleting = true;
10931155
this._setButtonsEnabled(false);
10941156

10951157
/**
@@ -1115,14 +1177,14 @@ export class DictionaryController {
11151177

11161178
await this._deleteDictionaryInternal(dictionaryTitle, onProgress);
11171179
await this._deleteDictionarySettings(dictionaryTitle);
1180+
this._onDictionariesUpdate = null;
11181181
} catch (e) {
11191182
log.error(e);
11201183
} finally {
11211184
prevention.end();
11221185
for (const progress of progressContainers) { progress.hidden = true; }
11231186
if (statusFooter !== null) { statusFooter.setTaskActive(progressSelector, false); }
11241187
this._setButtonsEnabled(true);
1125-
this._isDeleting = false;
11261188
this._triggerStorageChanged();
11271189
}
11281190
}
@@ -1132,7 +1194,7 @@ export class DictionaryController {
11321194
* @param {string|undefined} downloadUrl
11331195
*/
11341196
async _updateDictionary(dictionaryTitle, downloadUrl) {
1135-
if (this._checkingIntegrity || this._checkingUpdates || this._isDeleting || this._dictionaries === null) { return; }
1197+
if (this._checkingIntegrity || this._checkingUpdates || this._dictionaries === null) { return; }
11361198

11371199
const dictionaryInfo = this._dictionaries.find((entry) => entry.title === dictionaryTitle);
11381200
if (typeof dictionaryInfo === 'undefined') { throw new Error('Dictionary not found'); }
@@ -1156,7 +1218,10 @@ export class DictionaryController {
11561218
}
11571219

11581220
await this._deleteDictionary(dictionaryTitle);
1159-
this._settingsController.trigger('importDictionaryFromUrl', {url: downloadUrl, profilesDictionarySettings});
1221+
/** @type {import('core').DeferredPromiseDetails<void>} */
1222+
const {promise: importPromise, resolve} = deferPromise();
1223+
this._settingsController.trigger('importDictionaryFromUrl', {url: downloadUrl, profilesDictionarySettings, onImportDone: resolve});
1224+
await importPromise;
11601225
}
11611226

11621227
/**
@@ -1175,7 +1240,12 @@ export class DictionaryController {
11751240
*/
11761241
async _deleteDictionaryInternal(dictionaryTitle, onProgress) {
11771242
await new DictionaryWorker().deleteDictionary(dictionaryTitle, onProgress);
1243+
/** @type {import('core').DeferredPromiseDetails<void>} */
1244+
const {promise: dictionariesUpdatePromise, resolve} = deferPromise();
1245+
this._onDictionariesUpdate = resolve;
11781246
void this._settingsController.application.api.triggerDatabaseUpdated('dictionary', 'delete');
1247+
await dictionariesUpdatePromise;
1248+
this._onDictionariesUpdate = null;
11791249
}
11801250

11811251
/**

0 commit comments

Comments
 (0)