UNPKG

@schukai/monster

Version:

Monster is a simple library for creating fast, robust and lightweight websites.

353 lines (306 loc) 7.84 kB
/** * Copyright © Volker Schukai and all contributing authors, {{copyRightYear}}. All rights reserved. * Node module: @schukai/monster * * This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3). * The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html * * For those who do not wish to adhere to the AGPLv3, a commercial license is available. * Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms. * For more information about purchasing a commercial license, please contact Volker Schukai. * * SPDX-License-Identifier: AGPL-3.0 */ import { assembleMethodSymbol, CustomElement, registerCustomElement, } from "../../dom/customelement.mjs"; import { ConfigManagerStyleSheet } from "./stylesheet/config-manager.mjs"; import { getWindow } from "../../dom/util.mjs"; import { instanceSymbol } from "../../constants.mjs"; import { diff } from "../../data/diff.mjs"; export { ConfigManager }; /** * @private * @type {symbol} */ const indexDBInstanceSymbol = Symbol("indexDBInstance"); /** * @private * @type {symbol} */ const initializedPromiseSymbol = Symbol("initializedPromiseSymbol"); /** * @private * @type {string} */ const MODE_READONLY = "readonly"; /** * @private * @type {string} */ const MODE_READ_WRITE = "readwrite"; /** * The Config Manager component is used to encapsulate the configuration of the application. * * @fragments /fragments/components/host/config-manager/ * * @example /examples/components/host/config-manager-simple Config manager * * @copyright Volker Schukai * @summary A config manager component */ class ConfigManager extends CustomElement { /** * This method is called by the `instanceof` operator. * @return {symbol} */ static get [instanceSymbol]() { return Symbol.for("@schukai/component-host/config-manager@@instance"); } constructor() { super(); /** * @private * @type {symbol} */ this[initializedPromiseSymbol] = []; this[indexDBInstanceSymbol] = null; this[initializedPromiseSymbol].push(openDatabase.call(this)); } /** * @return {Promise} */ ready() { return Promise.all(this[initializedPromiseSymbol]); } /** * To set the options via the HTML tag, the attribute `data-monster-options` must be used. * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control} * * The individual configuration values can be found in the table. * * @property {Object} templates Template definitions * @property {string} templates.main Main template */ get defaults() { return Object.assign({}, super.defaults, { templates: { main: getTemplate(), }, indexDB: { name: "monster", version: 2, objectStore: { name: "config", keyPath: "key", }, }, }); } /** * @param {string} key * @return {Promise<unknown>} */ getConfig(key) { return this.ready().then(() => { return getBlob.call(this, key); }); } /** * @param {string} key * @return {Promise<boolean>} */ hasConfig(key) { return this.ready() .then(() => { return getBlob.call(this, key); }) .then(() => { return true; }) .catch(() => { return false; }); } /** * @param {string} key * @param {*} value * @return {Promise<unknown>} */ setConfig(key, value) { return this.ready().then(() => { return getBlob .call(this, key) .then((storedValue) => { if (diff(storedValue, value).length === 0) { return; } return setBlob.call(this, key, value); }) .catch((error) => { if (error?.message?.match(/is not defined/)) { return setBlob.call(this, key, value); } throw error; }); }); } deleteConfig(key) { return this.ready().then(() => { return deleteBlob.call(this, key); }); } /** * * @return {string} */ static getTag() { return "monster-config-manager"; } /** * @return {CSSStyleSheet[]} */ static getCSSStyleSheet() { return [ConfigManagerStyleSheet]; } /** */ [assembleMethodSymbol]() { super[assembleMethodSymbol](); } } /** * @private * @returns {Promise<unknown>} */ function openDatabase() { const window = getWindow(); const name = this.getOption("indexDB.name"); const version = this.getOption("indexDB.version"); const storageName = this.getOption("indexDB.objectStore.name"); const KeyPath = this.getOption("indexDB.objectStore.keyPath"); if (!name || !version) { throw new Error("The database name and version must be set."); } const request = window.indexedDB.open(name, version); return new Promise((resolve, reject) => { let upgradeComplete = true; let openComplete = false; request.onerror = (event) => { console.error("Error opening database", event); reject(request.error); }; request.onsuccess = (event) => { this[indexDBInstanceSymbol] = event?.target?.result; openComplete = true; if (upgradeComplete) { resolve(request.result); } }; request.onupgradeneeded = (event) => { const db = event.target.result; this[indexDBInstanceSymbol] = db; upgradeComplete = false; if (!db.objectStoreNames.contains(storageName)) { db.createObjectStore(storageName, { keyPath: KeyPath }); } event.target.transaction.oncomplete = () => { upgradeComplete = true; if (openComplete) { resolve(request.result); } }; }; }); } /** * @param {string} mode either "readonly" or "readwrite" */ function getObjectStore(mode) { const storageName = this.getOption("indexDB.objectStore.name"); if (!this[indexDBInstanceSymbol]) { throw new Error("The database is not open."); } // @see https://developer.mozilla.org/en-US/docs/Web/API/IDBDatabase/transaction // transaction(storeNames, mode, options) const tx = this[indexDBInstanceSymbol].transaction(storageName, mode); return tx.objectStore(storageName); } /** * @return {Promise<unknown>} */ function clearObjectStore() { const store = getObjectStore.call(this, "readwrite"); return new Promise((resolve, reject) => { const req = store.clear(); req.onsuccess = function (evt) { resolve(); }; req.onerror = function (evt) { reject(evt.target?.errorCode); }; }); } function getBlob(key) { const store = getObjectStore.call(this, MODE_READONLY); const req = store.get(key); return new Promise((resolve, reject) => { req.onsuccess = function (evt) { const value = evt.target.result; if (value) { resolve(value.blob); return; } reject(new Error("The value of the key '" + key + "' is not defined.")); }; req.onerror = function () { reject(req.error || new Error("IndexedDB get() failed")); }; }); } function deleteBlob(key) { const store = getObjectStore.call(this, MODE_READ_WRITE); const req = store.delete(key); return new Promise((resolve, reject) => { req.onsuccess = function (evt) { resolve(); }; req.onerror = function (evt) { console.error("deleteBlob:", evt.target.errorCode); reject(evt.target.errorCode); }; }); } /** * @private * @param key * @param blob * @returns {Promise<unknown>} */ function setBlob(key, blob) { const store = getObjectStore.call(this, MODE_READ_WRITE); const KeyPath = this.getOption("indexDB.objectStore.keyPath"); const obj = {}; obj[KeyPath] = key; obj.blob = blob; const req = store.put(obj); return new Promise((resolve, reject) => { req.onsuccess = function (evt) { resolve(); }; req.onerror = function (evt) { console.error("setBlob:", evt.target.errorCode); reject(evt.target.errorCode); }; }); } function getTemplate() { // language=HTML return ` <div data-monster-role="control" part="control"> <slot></slot> </div>`; } registerCustomElement(ConfigManager);