summaryrefslogtreecommitdiff
path: root/ext/js/general
diff options
context:
space:
mode:
Diffstat (limited to 'ext/js/general')
-rw-r--r--ext/js/general/cache-map.js140
-rw-r--r--ext/js/general/object-property-accessor.js342
-rw-r--r--ext/js/general/task-accumulator.js81
3 files changed, 563 insertions, 0 deletions
diff --git a/ext/js/general/cache-map.js b/ext/js/general/cache-map.js
new file mode 100644
index 00000000..c7d72e6b
--- /dev/null
+++ b/ext/js/general/cache-map.js
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2020-2021 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 which caches a map of values, keeping the most recently accessed values.
+ */
+class CacheMap {
+ /**
+ * Creates a new CacheMap.
+ * @param maxSize The maximum number of entries able to be stored in the cache.
+ */
+ constructor(maxSize) {
+ if (!(
+ typeof maxSize === 'number' &&
+ Number.isFinite(maxSize) &&
+ maxSize >= 0 &&
+ Math.floor(maxSize) === maxSize
+ )) {
+ throw new Error('Invalid maxCount');
+ }
+
+ this._maxSize = maxSize;
+ this._map = new Map();
+ this._listFirst = this._createNode(null, null);
+ this._listLast = this._createNode(null, null);
+ this._resetEndNodes();
+ }
+
+ /**
+ * Returns the number of items in the cache.
+ */
+ get size() {
+ return this._map.size;
+ }
+
+ /**
+ * Returns the maximum number of items that can be added to the cache.
+ */
+ get maxSize() {
+ return this._maxSize;
+ }
+
+ /**
+ * Returns whether or not an element exists at the given key.
+ * @param key The key of the element.
+ * @returns `true` if an element with the specified key exists, `false` otherwise.
+ */
+ has(key) {
+ return this._map.has(key);
+ }
+
+ /**
+ * Gets an element at the given key, if it exists. Otherwise, returns undefined.
+ * @param key The key of the element.
+ * @returns The existing value at the key, if any; `undefined` otherwise.
+ */
+ get(key) {
+ const node = this._map.get(key);
+ if (typeof node === 'undefined') { return void 0; }
+ this._updateRecency(node);
+ return node.value;
+ }
+
+ /**
+ * Sets a value at a given key.
+ * @param key The key of the element.
+ * @param value The value to store in the cache.
+ */
+ set(key, value) {
+ let node = this._map.get(key);
+ if (typeof node !== 'undefined') {
+ this._updateRecency(node);
+ node.value = value;
+ } else {
+ if (this._maxSize <= 0) { return; }
+
+ node = this._createNode(key, value);
+ this._addNode(node, this._listFirst);
+ this._map.set(key, node);
+
+ // Remove
+ for (let removeCount = this._map.size - this._maxSize; removeCount > 0; --removeCount) {
+ node = this._listLast.previous;
+ this._removeNode(node);
+ this._map.delete(node.key);
+ }
+ }
+ }
+
+ /**
+ * Clears the cache.
+ */
+ clear() {
+ this._map.clear();
+ this._resetEndNodes();
+ }
+
+ // Private
+
+ _updateRecency(node) {
+ this._removeNode(node);
+ this._addNode(node, this._listFirst);
+ }
+
+ _createNode(key, value) {
+ return {key, value, previous: null, next: null};
+ }
+
+ _addNode(node, previous) {
+ const next = previous.next;
+ node.next = next;
+ node.previous = previous;
+ previous.next = node;
+ next.previous = node;
+ }
+
+ _removeNode(node) {
+ node.next.previous = node.previous;
+ node.previous.next = node.next;
+ }
+
+ _resetEndNodes() {
+ this._listFirst.next = this._listLast;
+ this._listLast.previous = this._listFirst;
+ }
+}
diff --git a/ext/js/general/object-property-accessor.js b/ext/js/general/object-property-accessor.js
new file mode 100644
index 00000000..95773029
--- /dev/null
+++ b/ext/js/general/object-property-accessor.js
@@ -0,0 +1,342 @@
+/*
+ * Copyright (C) 2016-2021 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 used to get and mutate generic properties of an object by using path strings.
+ */
+class ObjectPropertyAccessor {
+ /**
+ * Create a new accessor for a specific object.
+ * @param target The object which the getter and mutation methods are applied to.
+ * @returns A new ObjectPropertyAccessor instance.
+ */
+ constructor(target) {
+ this._target = target;
+ }
+
+ /**
+ * Gets the value at the specified path.
+ * @param pathArray The path to the property on the target object.
+ * @param pathLength How many parts of the pathArray to use.
+ * This parameter is optional and defaults to the length of pathArray.
+ * @returns The value found at the path.
+ * @throws An error is thrown if pathArray is not valid for the target object.
+ */
+ get(pathArray, pathLength) {
+ let target = this._target;
+ const ii = typeof pathLength === 'number' ? Math.min(pathArray.length, pathLength) : pathArray.length;
+ for (let i = 0; i < ii; ++i) {
+ const key = pathArray[i];
+ if (!ObjectPropertyAccessor.hasProperty(target, key)) {
+ throw new Error(`Invalid path: ${ObjectPropertyAccessor.getPathString(pathArray.slice(0, i + 1))}`);
+ }
+ target = target[key];
+ }
+ return target;
+ }
+
+ /**
+ * Sets the value at the specified path.
+ * @param pathArray The path to the property on the target object.
+ * @param value The value to assign to the property.
+ * @throws An error is thrown if pathArray is not valid for the target object.
+ */
+ set(pathArray, value) {
+ const ii = pathArray.length - 1;
+ if (ii < 0) { throw new Error('Invalid path'); }
+
+ const target = this.get(pathArray, ii);
+ const key = pathArray[ii];
+ if (!ObjectPropertyAccessor.isValidPropertyType(target, key)) {
+ throw new Error(`Invalid path: ${ObjectPropertyAccessor.getPathString(pathArray)}`);
+ }
+
+ target[key] = value;
+ }
+
+ /**
+ * Deletes the property of the target object at the specified path.
+ * @param pathArray The path to the property on the target object.
+ * @throws An error is thrown if pathArray is not valid for the target object.
+ */
+ delete(pathArray) {
+ const ii = pathArray.length - 1;
+ if (ii < 0) { throw new Error('Invalid path'); }
+
+ const target = this.get(pathArray, ii);
+ const key = pathArray[ii];
+ if (!ObjectPropertyAccessor.isValidPropertyType(target, key)) {
+ throw new Error(`Invalid path: ${ObjectPropertyAccessor.getPathString(pathArray)}`);
+ }
+
+ if (Array.isArray(target)) {
+ throw new Error('Invalid type');
+ }
+
+ delete target[key];
+ }
+
+ /**
+ * Swaps two properties of an object or array.
+ * @param pathArray1 The path to the first property on the target object.
+ * @param pathArray2 The path to the second property on the target object.
+ * @throws An error is thrown if pathArray1 or pathArray2 is not valid for the target object,
+ * or if the swap cannot be performed.
+ */
+ swap(pathArray1, pathArray2) {
+ const ii1 = pathArray1.length - 1;
+ if (ii1 < 0) { throw new Error('Invalid path 1'); }
+ const target1 = this.get(pathArray1, ii1);
+ const key1 = pathArray1[ii1];
+ if (!ObjectPropertyAccessor.isValidPropertyType(target1, key1)) { throw new Error(`Invalid path 1: ${ObjectPropertyAccessor.getPathString(pathArray1)}`); }
+
+ const ii2 = pathArray2.length - 1;
+ if (ii2 < 0) { throw new Error('Invalid path 2'); }
+ const target2 = this.get(pathArray2, ii2);
+ const key2 = pathArray2[ii2];
+ if (!ObjectPropertyAccessor.isValidPropertyType(target2, key2)) { throw new Error(`Invalid path 2: ${ObjectPropertyAccessor.getPathString(pathArray2)}`); }
+
+ const value1 = target1[key1];
+ const value2 = target2[key2];
+
+ target1[key1] = value2;
+ try {
+ target2[key2] = value1;
+ } catch (e) {
+ // Revert
+ try {
+ target1[key1] = value1;
+ } catch (e2) {
+ // NOP
+ }
+ throw e;
+ }
+ }
+
+ /**
+ * Converts a path string to a path array.
+ * @param pathArray The path array to convert.
+ * @returns A string representation of pathArray.
+ */
+ static getPathString(pathArray) {
+ const regexShort = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
+ let pathString = '';
+ let first = true;
+ for (let part of pathArray) {
+ switch (typeof part) {
+ case 'number':
+ if (Math.floor(part) !== part || part < 0) {
+ throw new Error('Invalid index');
+ }
+ part = `[${part}]`;
+ break;
+ case 'string':
+ if (!regexShort.test(part)) {
+ const escapedPart = part.replace(/["\\]/g, '\\$&');
+ part = `["${escapedPart}"]`;
+ } else {
+ if (!first) {
+ part = `.${part}`;
+ }
+ }
+ break;
+ default:
+ throw new Error(`Invalid type: ${typeof part}`);
+ }
+ pathString += part;
+ first = false;
+ }
+ return pathString;
+ }
+
+ /**
+ * Converts a path array to a path string. For the most part, the format of this string
+ * matches Javascript's notation for property access.
+ * @param pathString The path string to convert.
+ * @returns An array representation of pathString.
+ */
+ static getPathArray(pathString) {
+ const pathArray = [];
+ let state = 'empty';
+ let quote = 0;
+ let value = '';
+ let escaped = false;
+ for (const c of pathString) {
+ const v = c.codePointAt(0);
+ switch (state) {
+ case 'empty': // Empty
+ case 'id-start': // Expecting identifier start
+ if (v === 0x5b) { // '['
+ if (state === 'id-start') {
+ throw new Error(`Unexpected character: ${c}`);
+ }
+ state = 'open-bracket';
+ } else if (
+ (v >= 0x41 && v <= 0x5a) || // ['A', 'Z']
+ (v >= 0x61 && v <= 0x7a) || // ['a', 'z']
+ v === 0x5f // '_'
+ ) {
+ state = 'id';
+ value += c;
+ } else {
+ throw new Error(`Unexpected character: ${c}`);
+ }
+ break;
+ case 'id': // Identifier
+ if (
+ (v >= 0x41 && v <= 0x5a) || // ['A', 'Z']
+ (v >= 0x61 && v <= 0x7a) || // ['a', 'z']
+ (v >= 0x30 && v <= 0x39) || // ['0', '9']
+ v === 0x5f // '_'
+ ) {
+ value += c;
+ } else if (v === 0x5b) { // '['
+ pathArray.push(value);
+ value = '';
+ state = 'open-bracket';
+ } else if (v === 0x2e) { // '.'
+ pathArray.push(value);
+ value = '';
+ state = 'id-start';
+ } else {
+ throw new Error(`Unexpected character: ${c}`);
+ }
+ break;
+ case 'open-bracket': // Open bracket
+ if (v === 0x22 || v === 0x27) { // '"' or '\''
+ quote = v;
+ state = 'string';
+ } else if (v >= 0x30 && v <= 0x39) { // ['0', '9']
+ state = 'number';
+ value += c;
+ } else {
+ throw new Error(`Unexpected character: ${c}`);
+ }
+ break;
+ case 'string': // Quoted string
+ if (escaped) {
+ value += c;
+ escaped = false;
+ } else if (v === 0x5c) { // '\\'
+ escaped = true;
+ } else if (v !== quote) {
+ value += c;
+ } else {
+ state = 'close-bracket';
+ }
+ break;
+ case 'number': // Number
+ if (v >= 0x30 && v <= 0x39) { // ['0', '9']
+ value += c;
+ } else if (v === 0x5d) { // ']'
+ pathArray.push(Number.parseInt(value, 10));
+ value = '';
+ state = 'next';
+ } else {
+ throw new Error(`Unexpected character: ${c}`);
+ }
+ break;
+ case 'close-bracket': // Expecting closing bracket after quoted string
+ if (v === 0x5d) { // ']'
+ pathArray.push(value);
+ value = '';
+ state = 'next';
+ } else {
+ throw new Error(`Unexpected character: ${c}`);
+ }
+ break;
+ case 'next': // Expecting . or [
+ if (v === 0x5b) { // '['
+ state = 'open-bracket';
+ } else if (v === 0x2e) { // '.'
+ state = 'id-start';
+ } else {
+ throw new Error(`Unexpected character: ${c}`);
+ }
+ break;
+ }
+ }
+ switch (state) {
+ case 'empty':
+ case 'next':
+ break;
+ case 'id':
+ pathArray.push(value);
+ value = '';
+ break;
+ default:
+ throw new Error('Path not terminated correctly');
+ }
+ return pathArray;
+ }
+
+ /**
+ * Checks whether an object or array has the specified property.
+ * @param object The object to test.
+ * @param property The property to check for existence.
+ * This value should be a string if the object is a non-array object.
+ * For arrays, it should be an integer.
+ * @returns true if the property exists, otherwise false.
+ */
+ static hasProperty(object, property) {
+ switch (typeof property) {
+ case 'string':
+ return (
+ typeof object === 'object' &&
+ object !== null &&
+ !Array.isArray(object) &&
+ Object.prototype.hasOwnProperty.call(object, property)
+ );
+ case 'number':
+ return (
+ Array.isArray(object) &&
+ property >= 0 &&
+ property < object.length &&
+ property === Math.floor(property)
+ );
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Checks whether a property is valid for the given object
+ * @param object The object to test.
+ * @param property The property to check for existence.
+ * @returns true if the property is correct for the given object type, otherwise false.
+ * For arrays, this means that the property should be a positive integer.
+ * For non-array objects, the property should be a string.
+ */
+ static isValidPropertyType(object, property) {
+ switch (typeof property) {
+ case 'string':
+ return (
+ typeof object === 'object' &&
+ object !== null &&
+ !Array.isArray(object)
+ );
+ case 'number':
+ return (
+ Array.isArray(object) &&
+ property >= 0 &&
+ property === Math.floor(property)
+ );
+ default:
+ return false;
+ }
+ }
+}
diff --git a/ext/js/general/task-accumulator.js b/ext/js/general/task-accumulator.js
new file mode 100644
index 00000000..82691b43
--- /dev/null
+++ b/ext/js/general/task-accumulator.js
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2020-2021 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;
+ }
+}