Settings binder (#542)
* Fix _modifySetting being async * Return values for modifySettings's set and splice actions * Add apiGetSettings * Create a class which can accumulate tasks to run in bulk * Create a class which binds input elements to customizable sources * Create class which binds input elements to settings * Add support for value transforms * Remove redundant ObjectPropertyAccessor.getPathArray * Fix not using correct types for input.min/max/step * Fix wrong condition * Use api object
This commit is contained in:
parent
c800444a0d
commit
66e1185686
@ -119,7 +119,8 @@ class Backend {
|
||||
['log', {async: false, contentScript: true, handler: this._onApiLog.bind(this)}],
|
||||
['logIndicatorClear', {async: false, contentScript: true, handler: this._onApiLogIndicatorClear.bind(this)}],
|
||||
['createActionPort', {async: false, contentScript: true, handler: this._onApiCreateActionPort.bind(this)}],
|
||||
['modifySettings', {async: true, contentScript: true, handler: this._onApiModifySettings.bind(this)}]
|
||||
['modifySettings', {async: true, contentScript: true, handler: this._onApiModifySettings.bind(this)}],
|
||||
['getSettings', {async: false, contentScript: true, handler: this._onApiGetSettings.bind(this)}]
|
||||
]);
|
||||
this._messageHandlersWithProgress = new Map([
|
||||
['importDictionaryArchive', {async: true, contentScript: false, handler: this._onApiImportDictionaryArchive.bind(this)}],
|
||||
@ -831,8 +832,8 @@ class Backend {
|
||||
const results = [];
|
||||
for (const target of targets) {
|
||||
try {
|
||||
this._modifySetting(target);
|
||||
results.push({result: true});
|
||||
const result = this._modifySetting(target);
|
||||
results.push({result: utilIsolate(result)});
|
||||
} catch (e) {
|
||||
results.push({error: errorToJson(e)});
|
||||
}
|
||||
@ -841,6 +842,19 @@ class Backend {
|
||||
return results;
|
||||
}
|
||||
|
||||
_onApiGetSettings({targets}) {
|
||||
const results = [];
|
||||
for (const target of targets) {
|
||||
try {
|
||||
const result = this._getSetting(target);
|
||||
results.push({result: utilIsolate(result)});
|
||||
} catch (e) {
|
||||
results.push({error: errorToJson(e)});
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
// Command handlers
|
||||
|
||||
_createActionListenerPort(port, sender, handlers) {
|
||||
@ -1017,45 +1031,53 @@ class Backend {
|
||||
}
|
||||
}
|
||||
|
||||
async _modifySetting(target) {
|
||||
_getSetting(target) {
|
||||
const options = this._getModifySettingObject(target);
|
||||
const accessor = new ObjectPropertyAccessor(options);
|
||||
const {path} = target;
|
||||
if (typeof path !== 'string') { throw new Error('Invalid path'); }
|
||||
return accessor.get(ObjectPropertyAccessor.getPathArray(path));
|
||||
}
|
||||
|
||||
_modifySetting(target) {
|
||||
const options = this._getModifySettingObject(target);
|
||||
const accessor = new ObjectPropertyAccessor(options);
|
||||
const action = target.action;
|
||||
switch (action) {
|
||||
case 'set':
|
||||
{
|
||||
const {path, value} = target;
|
||||
if (typeof path !== 'string') { throw new Error('Invalid path'); }
|
||||
accessor.set(ObjectPropertyAccessor.getPathArray(path), value);
|
||||
}
|
||||
break;
|
||||
{
|
||||
const {path, value} = target;
|
||||
if (typeof path !== 'string') { throw new Error('Invalid path'); }
|
||||
const pathArray = ObjectPropertyAccessor.getPathArray(path);
|
||||
accessor.set(pathArray, value);
|
||||
return accessor.get(pathArray);
|
||||
}
|
||||
case 'delete':
|
||||
{
|
||||
const {path} = target;
|
||||
if (typeof path !== 'string') { throw new Error('Invalid path'); }
|
||||
accessor.delete(ObjectPropertyAccessor.getPathArray(path));
|
||||
}
|
||||
break;
|
||||
{
|
||||
const {path} = target;
|
||||
if (typeof path !== 'string') { throw new Error('Invalid path'); }
|
||||
accessor.delete(ObjectPropertyAccessor.getPathArray(path));
|
||||
return true;
|
||||
}
|
||||
case 'swap':
|
||||
{
|
||||
const {path1, path2} = target;
|
||||
if (typeof path1 !== 'string') { throw new Error('Invalid path1'); }
|
||||
if (typeof path2 !== 'string') { throw new Error('Invalid path2'); }
|
||||
accessor.swap(ObjectPropertyAccessor.getPathArray(path1), ObjectPropertyAccessor.getPathArray(path2));
|
||||
}
|
||||
break;
|
||||
{
|
||||
const {path1, path2} = target;
|
||||
if (typeof path1 !== 'string') { throw new Error('Invalid path1'); }
|
||||
if (typeof path2 !== 'string') { throw new Error('Invalid path2'); }
|
||||
accessor.swap(ObjectPropertyAccessor.getPathArray(path1), ObjectPropertyAccessor.getPathArray(path2));
|
||||
return true;
|
||||
}
|
||||
case 'splice':
|
||||
{
|
||||
const {path, start, deleteCount, items} = target;
|
||||
if (typeof path !== 'string') { throw new Error('Invalid path'); }
|
||||
if (typeof start !== 'number' || Math.floor(start) !== start) { throw new Error('Invalid start'); }
|
||||
if (typeof deleteCount !== 'number' || Math.floor(deleteCount) !== deleteCount) { throw new Error('Invalid deleteCount'); }
|
||||
if (!Array.isArray(items)) { throw new Error('Invalid items'); }
|
||||
const array = accessor.get(ObjectPropertyAccessor.getPathArray(path));
|
||||
if (!Array.isArray(array)) { throw new Error('Invalid target type'); }
|
||||
array.splice(start, deleteCount, ...items);
|
||||
}
|
||||
break;
|
||||
{
|
||||
const {path, start, deleteCount, items} = target;
|
||||
if (typeof path !== 'string') { throw new Error('Invalid path'); }
|
||||
if (typeof start !== 'number' || Math.floor(start) !== start) { throw new Error('Invalid start'); }
|
||||
if (typeof deleteCount !== 'number' || Math.floor(deleteCount) !== deleteCount) { throw new Error('Invalid deleteCount'); }
|
||||
if (!Array.isArray(items)) { throw new Error('Invalid items'); }
|
||||
const array = accessor.get(ObjectPropertyAccessor.getPathArray(path));
|
||||
if (!Array.isArray(array)) { throw new Error('Invalid target type'); }
|
||||
return array.splice(start, deleteCount, ...items);
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown action: ${action}`);
|
||||
}
|
||||
|
122
ext/bg/js/settings/dom-settings-binder.js
Normal file
122
ext/bg/js/settings/dom-settings-binder.js
Normal file
@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Yomichan Authors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* global
|
||||
* DOMDataBinder
|
||||
* api
|
||||
* getOptionsContext
|
||||
*/
|
||||
|
||||
class DOMSettingsBinder {
|
||||
constructor({getOptionsContext, transforms=null}) {
|
||||
this._getOptionsContext = getOptionsContext;
|
||||
this._defaultScope = 'profile';
|
||||
this._dataBinder = new DOMDataBinder({
|
||||
selector: '[data-setting]',
|
||||
createElementMetadata: this._createElementMetadata.bind(this),
|
||||
compareElementMetadata: this._compareElementMetadata.bind(this),
|
||||
getValues: this._getValues.bind(this),
|
||||
setValues: this._setValues.bind(this)
|
||||
});
|
||||
this._transforms = new Map(transforms !== null ? transforms : []);
|
||||
}
|
||||
|
||||
observe(element) {
|
||||
this._dataBinder.observe(element);
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this._dataBinder.disconnect();
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this._dataBinder.refresh();
|
||||
}
|
||||
|
||||
// Private
|
||||
|
||||
_createElementMetadata(element) {
|
||||
return {
|
||||
path: element.dataset.setting,
|
||||
scope: element.dataset.scope,
|
||||
transformPre: element.dataset.transformPre,
|
||||
transformPost: element.dataset.transformPost
|
||||
};
|
||||
}
|
||||
|
||||
_compareElementMetadata(metadata1, metadata2) {
|
||||
return (
|
||||
metadata1.path === metadata2.path &&
|
||||
metadata1.scope === metadata2.scope &&
|
||||
metadata1.transformPre === metadata2.transformPre &&
|
||||
metadata1.transformPost === metadata2.transformPost
|
||||
);
|
||||
}
|
||||
|
||||
async _getValues(targets) {
|
||||
const settingsTargets = [];
|
||||
for (const {metadata: {path, scope}} of targets) {
|
||||
const target = {
|
||||
path,
|
||||
scope: scope || this._defaultScope
|
||||
};
|
||||
if (target.scope === 'profile') {
|
||||
target.optionsContext = this._getOptionsContext();
|
||||
}
|
||||
settingsTargets.push(target);
|
||||
}
|
||||
return this._transformResults(await api.getSettings(settingsTargets), targets);
|
||||
}
|
||||
|
||||
async _setValues(targets) {
|
||||
const settingsTargets = [];
|
||||
for (const {metadata, value, element} of targets) {
|
||||
const {path, scope, transformPre} = metadata;
|
||||
const target = {
|
||||
path,
|
||||
scope: scope || this._defaultScope,
|
||||
action: 'set',
|
||||
value: this._transform(value, transformPre, metadata, element)
|
||||
};
|
||||
if (target.scope === 'profile') {
|
||||
target.optionsContext = this._getOptionsContext();
|
||||
}
|
||||
settingsTargets.push(target);
|
||||
}
|
||||
return this._transformResults(await api.modifySettings(settingsTargets), targets);
|
||||
}
|
||||
|
||||
_transform(value, transform, metadata, element) {
|
||||
if (typeof transform === 'string') {
|
||||
const transformFunction = this._transforms.get(transform);
|
||||
if (typeof transformFunction !== 'undefined') {
|
||||
value = transformFunction(value, metadata, element);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
_transformResults(values, targets) {
|
||||
return values.map((value, i) => {
|
||||
const error = value.error;
|
||||
if (error) { return jsonToError(error); }
|
||||
const {metadata, element} = targets[i];
|
||||
const result = this._transform(value.result, metadata.transformPost, metadata, element);
|
||||
return {result};
|
||||
});
|
||||
}
|
||||
}
|
@ -172,6 +172,10 @@ const api = (() => {
|
||||
return this._invoke('modifySettings', {targets, source});
|
||||
}
|
||||
|
||||
getSettings(targets) {
|
||||
return this._invoke('getSettings', {targets});
|
||||
}
|
||||
|
||||
// Invoke functions with progress
|
||||
|
||||
importDictionaryArchive(archiveContent, details, onProgress) {
|
||||
|
349
ext/mixed/js/dom-data-binder.js
Normal file
349
ext/mixed/js/dom-data-binder.js
Normal file
@ -0,0 +1,349 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Yomichan Authors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* global
|
||||
* TaskAccumulator
|
||||
*/
|
||||
|
||||
class DOMDataBinder {
|
||||
constructor({selector, ignoreSelectors=[], createElementMetadata, compareElementMetadata, getValues, setValues, onError=null}) {
|
||||
this._selector = selector;
|
||||
this._ignoreSelectors = ignoreSelectors;
|
||||
this._createElementMetadata = createElementMetadata;
|
||||
this._compareElementMetadata = compareElementMetadata;
|
||||
this._getValues = getValues;
|
||||
this._setValues = setValues;
|
||||
this._onError = onError;
|
||||
this._updateTasks = new TaskAccumulator(this._onBulkUpdate.bind(this));
|
||||
this._assignTasks = new TaskAccumulator(this._onBulkAssign.bind(this));
|
||||
this._mutationObserver = new MutationObserver(this._onMutation.bind(this));
|
||||
this._observingElement = null;
|
||||
this._elementMap = new Map(); // Map([element => observer]...)
|
||||
this._elementAncestorMap = new Map(); // Map([element => Set([observer]...))
|
||||
}
|
||||
|
||||
observe(element) {
|
||||
if (this._isObserving) { return; }
|
||||
|
||||
this._observingElement = element;
|
||||
this._mutationObserver.observe(element, {
|
||||
attributes: true,
|
||||
attributeOldValue: true,
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
this._onMutation([{
|
||||
type: 'childList',
|
||||
target: element.parentNode,
|
||||
addedNodes: [element],
|
||||
removedNodes: []
|
||||
}]);
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (!this._isObserving) { return; }
|
||||
|
||||
this._mutationObserver.disconnect();
|
||||
this._observingElement = null;
|
||||
|
||||
for (const observer of this._elementMap.values()) {
|
||||
this._removeObserver(observer);
|
||||
}
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
await this._updateTasks.enqueue(null, {all: true});
|
||||
}
|
||||
|
||||
// Private
|
||||
|
||||
_onMutation(mutationList) {
|
||||
for (const mutation of mutationList) {
|
||||
switch (mutation.type) {
|
||||
case 'childList':
|
||||
this._onChildListMutation(mutation);
|
||||
break;
|
||||
case 'attributes':
|
||||
this._onAttributeMutation(mutation);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onChildListMutation({addedNodes, removedNodes, target}) {
|
||||
const selector = this._selector;
|
||||
const ELEMENT_NODE = Node.ELEMENT_NODE;
|
||||
|
||||
for (const node of removedNodes) {
|
||||
const observers = this._elementAncestorMap.get(node);
|
||||
if (typeof observers === 'undefined') { continue; }
|
||||
for (const observer of observers) {
|
||||
this._removeObserver(observer);
|
||||
}
|
||||
}
|
||||
|
||||
for (const node of addedNodes) {
|
||||
if (node.nodeType !== ELEMENT_NODE) { continue; }
|
||||
if (node.matches(selector)) {
|
||||
this._createObserver(node);
|
||||
}
|
||||
for (const childNode of node.querySelectorAll(selector)) {
|
||||
this._createObserver(childNode);
|
||||
}
|
||||
}
|
||||
|
||||
if (addedNodes.length !== 0 || addedNodes.length !== 0) {
|
||||
const observer = this._elementMap.get(target);
|
||||
if (typeof observer !== 'undefined') {
|
||||
observer.updateValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onAttributeMutation({target}) {
|
||||
const selector = this._selector;
|
||||
const observers = this._elementAncestorMap.get(target);
|
||||
if (typeof observers !== 'undefined') {
|
||||
for (const observer of observers) {
|
||||
const element = observer.element;
|
||||
if (
|
||||
!element.matches(selector) ||
|
||||
this._shouldIgnoreElement(element) ||
|
||||
this._isObserverStale(observer)
|
||||
) {
|
||||
this._removeObserver(observer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (target.matches(selector)) {
|
||||
this._createObserver(target);
|
||||
}
|
||||
}
|
||||
|
||||
async _onBulkUpdate(tasks) {
|
||||
let all = false;
|
||||
const targets = [];
|
||||
for (const [observer, task] of tasks) {
|
||||
if (observer === null) {
|
||||
if (task.data.all) {
|
||||
all = true;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
targets.push([observer, task]);
|
||||
}
|
||||
}
|
||||
if (all) {
|
||||
targets.length = 0;
|
||||
for (const observer of this._elementMap.values()) {
|
||||
targets.push([observer, null]);
|
||||
}
|
||||
}
|
||||
|
||||
const args = targets.map(([observer]) => ({
|
||||
element: observer.element,
|
||||
metadata: observer.metadata
|
||||
}));
|
||||
const responses = await this._getValues(args);
|
||||
this._applyValues(targets, responses, true);
|
||||
}
|
||||
|
||||
async _onBulkAssign(tasks) {
|
||||
const targets = tasks;
|
||||
const args = targets.map(([observer, task]) => ({
|
||||
element: observer.element,
|
||||
metadata: observer.metadata,
|
||||
value: task.data.value
|
||||
}));
|
||||
const responses = await this._setValues(args);
|
||||
this._applyValues(targets, responses, false);
|
||||
}
|
||||
|
||||
_onElementChange(observer) {
|
||||
const value = this._getElementValue(observer.element);
|
||||
observer.value = value;
|
||||
observer.hasValue = true;
|
||||
this._assignTasks.enqueue(observer, {value});
|
||||
}
|
||||
|
||||
_applyValues(targets, response, ignoreStale) {
|
||||
if (!Array.isArray(response)) { return; }
|
||||
|
||||
for (let i = 0, ii = targets.length; i < ii; ++i) {
|
||||
const [observer, task] = targets[i];
|
||||
const {error, result} = response[i];
|
||||
const stale = (task !== null && task.stale);
|
||||
|
||||
if (error) {
|
||||
if (typeof this._onError === 'function') {
|
||||
this._onError(error, stale, observer.element, observer.metadata);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (stale && !ignoreStale) { continue; }
|
||||
|
||||
observer.value = result;
|
||||
observer.hasValue = true;
|
||||
this._setElementValue(observer.element, result);
|
||||
}
|
||||
}
|
||||
|
||||
_createObserver(element) {
|
||||
if (this._elementMap.has(element) || this._shouldIgnoreElement(element)) { return; }
|
||||
|
||||
const metadata = this._createElementMetadata(element);
|
||||
const nodeName = element.nodeName.toUpperCase();
|
||||
const ancestors = this._getAncestors(element);
|
||||
const observer = {
|
||||
element,
|
||||
ancestors,
|
||||
type: (nodeName === 'INPUT' ? element.type : null),
|
||||
value: null,
|
||||
hasValue: false,
|
||||
onChange: null,
|
||||
metadata
|
||||
};
|
||||
observer.onChange = this._onElementChange.bind(this, observer);
|
||||
this._elementMap.set(element, observer);
|
||||
|
||||
element.addEventListener('change', observer.onChange, false);
|
||||
|
||||
for (const ancestor of ancestors) {
|
||||
let observers = this._elementAncestorMap.get(ancestor);
|
||||
if (typeof observers === 'undefined') {
|
||||
observers = new Set();
|
||||
this._elementAncestorMap.set(ancestor, observers);
|
||||
}
|
||||
observers.add(observer);
|
||||
}
|
||||
|
||||
this._updateTasks.enqueue(observer);
|
||||
}
|
||||
|
||||
_removeObserver(observer) {
|
||||
const {element, ancestors} = observer;
|
||||
|
||||
element.removeEventListener('change', observer.onChange, false);
|
||||
observer.onChange = null;
|
||||
|
||||
this._elementMap.delete(element);
|
||||
|
||||
for (const ancestor of ancestors) {
|
||||
const observers = this._elementAncestorMap.get(ancestor);
|
||||
if (typeof observers === 'undefined') { continue; }
|
||||
|
||||
observers.delete(observer);
|
||||
if (observers.size === 0) {
|
||||
this._elementAncestorMap.delete(ancestor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_isObserverStale(observer) {
|
||||
const {element, type, metadata} = observer;
|
||||
const nodeName = element.nodeName.toUpperCase();
|
||||
return !(
|
||||
type === (nodeName === 'INPUT' ? element.type : null) &&
|
||||
this._compareElementMetadata(metadata, this._createElementMetadata(element))
|
||||
);
|
||||
}
|
||||
|
||||
_shouldIgnoreElement(element) {
|
||||
for (const selector of this._ignoreSelectors) {
|
||||
if (element.matches(selector)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
_getAncestors(node) {
|
||||
const root = this._observingElement;
|
||||
const results = [];
|
||||
while (true) {
|
||||
results.push(node);
|
||||
if (node === root) { break; }
|
||||
node = node.parentNode;
|
||||
if (node === null) { break; }
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
_setElementValue(element, value) {
|
||||
switch (element.nodeName.toUpperCase()) {
|
||||
case 'INPUT':
|
||||
switch (element.type) {
|
||||
case 'checkbox':
|
||||
element.checked = value;
|
||||
break;
|
||||
case 'text':
|
||||
case 'number':
|
||||
element.value = value;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'TEXTAREA':
|
||||
case 'SELECT':
|
||||
element.value = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_getElementValue(element) {
|
||||
switch (element.nodeName.toUpperCase()) {
|
||||
case 'INPUT':
|
||||
switch (element.type) {
|
||||
case 'checkbox':
|
||||
return !!element.checked;
|
||||
case 'text':
|
||||
return `${element.value}`;
|
||||
case 'number':
|
||||
return this._getInputNumberValue(element);
|
||||
}
|
||||
break;
|
||||
case 'TEXTAREA':
|
||||
case 'SELECT':
|
||||
return element.value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
_getInputNumberValue(element) {
|
||||
let value = parseFloat(element.value);
|
||||
if (!Number.isFinite(value)) { return 0; }
|
||||
|
||||
let {min, max, step} = element;
|
||||
min = this._stringValueToNumberOrNull(min);
|
||||
max = this._stringValueToNumberOrNull(max);
|
||||
step = this._stringValueToNumberOrNull(step);
|
||||
if (typeof min === 'number') { value = Math.max(value, min); }
|
||||
if (typeof max === 'number') { value = Math.min(value, max); }
|
||||
if (typeof step === 'number' && step !== 0) { value = Math.round(value / step) * step; }
|
||||
return value;
|
||||
}
|
||||
|
||||
_stringValueToNumberOrNull(value) {
|
||||
if (typeof value !== 'string' || value.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const number = parseFloat(value);
|
||||
return !Number.isNaN(number) ? number : null;
|
||||
}
|
||||
}
|
81
ext/mixed/js/task-accumulator.js
Normal file
81
ext/mixed/js/task-accumulator.js
Normal file
@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Yomichan Authors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
class TaskAccumulator {
|
||||
constructor(runTasks) {
|
||||
this._deferPromise = null;
|
||||
this._activePromise = null;
|
||||
this._tasks = [];
|
||||
this._tasksActive = [];
|
||||
this._uniqueTasks = new Map();
|
||||
this._uniqueTasksActive = new Map();
|
||||
this._runTasksBind = this._runTasks.bind(this);
|
||||
this._tasksCompleteBind = this._tasksComplete.bind(this);
|
||||
this._runTasks = runTasks;
|
||||
}
|
||||
|
||||
enqueue(key, data) {
|
||||
if (this._deferPromise === null) {
|
||||
const promise = this._activePromise !== null ? this._activePromise : Promise.resolve();
|
||||
this._deferPromise = promise.then(this._runTasksBind);
|
||||
}
|
||||
|
||||
const task = {data, stale: false};
|
||||
if (key !== null) {
|
||||
const activeTaskInfo = this._uniqueTasksActive.get(key);
|
||||
if (typeof activeTaskInfo !== 'undefined') {
|
||||
activeTaskInfo.stale = true;
|
||||
}
|
||||
|
||||
this._uniqueTasks.set(key, task);
|
||||
} else {
|
||||
this._tasks.push(task);
|
||||
}
|
||||
|
||||
return this._deferPromise;
|
||||
}
|
||||
|
||||
_runTasks() {
|
||||
this._deferPromise = null;
|
||||
|
||||
// Swap
|
||||
[this._tasks, this._tasksActive] = [this._tasksActive, this._tasks];
|
||||
[this._uniqueTasks, this._uniqueTasksActive] = [this._uniqueTasksActive, this._uniqueTasks];
|
||||
|
||||
const promise = this._runTasksAsync();
|
||||
this._activePromise = promise.then(this._tasksCompleteBind);
|
||||
return this._activePromise;
|
||||
}
|
||||
|
||||
async _runTasksAsync() {
|
||||
try {
|
||||
const allTasks = [
|
||||
...this._tasksActive.map((taskInfo) => [null, taskInfo]),
|
||||
...this._uniqueTasksActive.entries()
|
||||
];
|
||||
await this._runTasks(allTasks);
|
||||
} catch (e) {
|
||||
yomichan.logError(e);
|
||||
}
|
||||
}
|
||||
|
||||
_tasksComplete() {
|
||||
this._tasksActive.length = 0;
|
||||
this._uniqueTasksActive.clear();
|
||||
this._activePromise = null;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user