diff --git a/README.md b/README.md index 7e23a39..635438a 100644 --- a/README.md +++ b/README.md @@ -863,60 +863,92 @@ const db = AceBase.WithIndexedDB('dbname'); It is now possible to store data in your own custom storage backend. To do this, you only have to provide a couple of methods to get, set and remove data and you're done. -The example below shows how to implement a ```CustomStorage``` class that uses the browser's localStorage (NOTE: you can also use ```LocalStorageSettings``` described above to do the same): +The example below shows how to implement a ```CustomStorage``` class that uses the browser's localStorage (NOTE: you can also use ```AceBase.WithLocalStorage``` described above to do the same): ```javascript -const { AceBase, CustomStorageSettings, CustomStorageHelpers } = require('acebase'); +const { AceBase, CustomStorageSettings, CustomStorageTransaction, CustomStorageHelpers } = require('acebase'); -// Helper functions to prefix all localStorage keys with dbname -// to allows multiple db's in localStorage: const dbname = 'test'; -const storageKeysPrefix = `${dbname}.acebase::`; -function getPathFromStorageKey(key) { - return key.slice(storageKeysPrefix.length); -} -function getStorageKeyForPath(path) { - return `${storageKeysPrefix}${path}`; -} // Setup our CustomStorageSettings -const customStorageSettings = new CustomStorageSettings({ - info: 'Custom LocalStorage', +const storageSettings = new CustomStorageSettings({ + name: 'LocalStorage', + locking: true, // Let AceBase handle resource locking to prevent multiple simultanious updates to the same data. NOTE: This does not prevent multiple tabs from doing this!! + ready() { + // LocalStorage is always ready return Promise.resolve(); }, + + getTransaction(target) { + // Create an instance of our transaction class + const context = { + debug: true, + dbname + } + const transaction = new LocalStorageTransaction(context, target); + return Promise.resolve(transaction); + } +}); + +// Setup CustomStorageTransaction for browser's LocalStorage +class LocalStorageTransaction extends CustomStorageTransaction { + + /** + * @param {{debug: boolean, dbname: string}} context + * @param {{path: string, write: boolean}} target + */ + constructor(context, target) { + super(target); + this.context = context; + this._storageKeysPrefix = `${this.context.dbname}.acebase::`; + } + + commit() { + // All changes have already been committed + return Promise.resolve(); + } + + rollback(err) { + // Not able to rollback changes, because we did not keep track + return Promise.resolve(); + } + get(path) { // Gets value from localStorage, wrapped in Promise return new Promise(resolve => { - const json = localStorage.getItem(getStorageKeyForPath(path)); + const json = localStorage.getItem(this.getStorageKeyForPath(path)); const val = JSON.parse(json); resolve(val); }); - }, + } + set(path, val) { // Sets value in localStorage, wrapped in Promise return new Promise(resolve => { const json = JSON.stringify(val); - localStorage.setItem(getStorageKeyForPath(path), json); + localStorage.setItem(this.getStorageKeyForPath(path), json); resolve(); }); - }, + } + remove(path) { // Removes a value from localStorage, wrapped in Promise return new Promise(resolve => { - localStorage.removeItem(getStorageKeyForPath(path)); + localStorage.removeItem(this.getStorageKeyForPath(path)); resolve(); }); - }, + } + childrenOf(path, include, checkCallback, addCallback) { - // Gets all child paths + // Streams all child paths // Cannot query localStorage, so loop through all stored keys to find children return new Promise(resolve => { const pathInfo = CustomStorageHelpers.PathInfo.get(path); for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); - if (!key.startsWith(storageKeysPrefix)) { continue; } - let otherPath = getPathFromStorageKey(key); + if (!key.startsWith(this._storageKeysPrefix)) { continue; } + let otherPath = this.getPathFromStorageKey(key); if (pathInfo.isParentOf(otherPath) && checkCallback(otherPath)) { let node; if (include.metadata || include.value) { @@ -929,16 +961,17 @@ const customStorageSettings = new CustomStorageSettings({ } resolve(); }); - }, + } + descendantsOf(path, include, checkCallback, addCallback) { - // Gets all descendant paths + // Streams all descendant paths // Cannot query localStorage, so loop through all stored keys to find descendants return new Promise(resolve => { const pathInfo = CustomStorageHelpers.PathInfo.get(path); for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); - if (!key.startsWith(storageKeysPrefix)) { continue; } - let otherPath = getPathFromStorageKey(key); + if (!key.startsWith(this._storageKeysPrefix)) { continue; } + let otherPath = this.getPathFromStorageKey(key); if (pathInfo.isAncestorOf(otherPath) && checkCallback(otherPath)) { let node; if (include.metadata || include.value) { @@ -952,11 +985,29 @@ const customStorageSettings = new CustomStorageSettings({ resolve(); }); } -}); -// Now create AceBase instance using our custom storage: -const db = new AceBase(dbname, { logLevel: 'log', storage: customStorageSettings }); -// That's all! + /** + * Helper function to get the path from a localStorage key + * @param {string} key + */ + getPathFromStorageKey(key) { + return key.slice(this._storageKeysPrefix.length); + } + + /** + * Helper function to get the localStorage key for a path + * @param {string} path + */ + getStorageKeyForPath(path) { + return `${this._storageKeysPrefix}${path}`; + } +} + +// Now, create the database +const db = new AceBase(dbname, { logLevel: settings.logLevel, storage: storageSettings }); +db.ready(ready => { + // That's it! +}) ``` ## Reflect API diff --git a/dist/browser.js b/dist/browser.js index bc7f725..d7d9c1c 100644 --- a/dist/browser.js +++ b/dist/browser.js @@ -698,7 +698,8 @@ class DataReference { }); } else if (event === "notify_child_added") { - // Use the reflect API to get current children + // Use the reflect API to get current children. + // NOTE: This does not work with AceBaseServer <= v0.9.7, only when signed in as admin const step = 100; let limit = step, skip = 0; const more = () => { @@ -3488,7 +3489,7 @@ module.exports = { }).call(this,require('_process')) },{"./has-flag.js":28,"_process":45,"os":44}],30:[function(require,module,exports){ const { AceBase, AceBaseLocalSettings } = require('./acebase-local'); -const { CustomStorageSettings, CustomStorageHelpers, ICustomStorageNode, ICustomStorageNodeMetaData } = require('./storage-custom'); +const { CustomStorageSettings, CustomStorageTransaction, CustomStorageHelpers, ICustomStorageNode, ICustomStorageNodeMetaData } = require('./storage-custom'); /** * @typedef {Object} IIndexedDBNodeData @@ -3560,211 +3561,27 @@ class BrowserAceBase extends AceBase { request.onsuccess = e => { db = request.result; readyResolve(); - } + }; request.onerror = e => { readyReject(e); }; const storageSettings = new CustomStorageSettings({ name: 'IndexedDB', + locking: false, ready() { return readyPromise; }, - get(path) { - // Get metadata from "nodes" object store - const tx = db.transaction(['nodes', 'content'], 'readonly'); - const request = tx.objectStore('nodes').get(path); - return new Promise((resolve, reject) => { - request.onsuccess = event => { - /** @type {IIndexedDBNodeData} */ - const data = request.result; - if (!data) { - return resolve(null); - } - /** @type {ICustomStorageNode} */ - const node = data.metadata; - - // Node exists, get content from "content" object store - const contentReq = tx.objectStore('content').get(path); - contentReq.onsuccess = e => { - node.value = contentReq.result; - resolve(node); - }; - contentReq.onerror = e => reject(e); - } - request.onerror = e => { - console.error(`IndexedDB get error`, e); - reject(e); - } - }); - }, - set(path, node) { - /** @type {ICustomStorageNode} */ - const copy = {}; - const value = node.value; - Object.assign(copy, node); - delete copy.value; - /** @type {ICustomStorageNodeMetaData} */ - const metadata = copy; - /** @type {IIndexedDBNodeData} */ - const obj = { - path, - metadata + async getTransaction(target) { + const context = { + debug: true, + db } - return new Promise((resolve, reject) => { - const tx = db.transaction(['nodes', 'content'], 'readwrite'); - // Insert into "nodes" object store first - const request = tx.objectStore('nodes').put(obj); - request.onerror = e => reject(e); - request.onsuccess = e => { - // Now add to "content" object store - const contentReq = tx.objectStore('content').put(value, path); - contentReq.onsuccess = e => resolve(); - contentReq.onerror = e => { - tx.abort(); // rollback transaction - reject(e); - } - }; - }); - }, - remove(path) { - const tx = db.transaction(['nodes','content'], 'readwrite'); - return new Promise((resolve, reject) => { - // First, remove from "content" object store - const r1 = tx.objectStore('content').delete(path); - r1.onerror = e => reject(e); - r1.onsuccess = e => { - // Now, remove from "nodes" data store - const r2 = tx.objectStore('nodes').delete(path); - r2.onerror = e => { - tx.abort(); // rollback transaction - reject(e); - }; - r2.onsuccess = e => resolve(); - } - }); - }, - childrenOf(path, include, checkCallback, addCallback) { - // Use cursor to loop from path on - const pathInfo = CustomStorageHelpers.PathInfo.get(path); - const lockStores = include.value ? ['nodes','content'] : 'nodes'; - const tx = db.transaction(lockStores, 'readonly'); - const store = tx.objectStore('nodes'); - const query = IDBKeyRange.lowerBound(path, true); - return new Promise((resolve, reject) => { - /** @type {IDBRequest|IDBRequest} */ - const cursor = include.metadata ? store.openCursor(query) : store.openKeyCursor(query); - cursor.onerror = e => reject(e); - cursor.onsuccess = async e => { - /** type {string} */ - const otherPath = cursor.result ? cursor.result.key : null; - let keepGoing = true; - if (otherPath === null) { - // No more results - keepGoing = false; - } - else if (!pathInfo.isAncestorOf(otherPath)) { - // Paths are sorted, no more children to be expected! - keepGoing = false; - } - else if (pathInfo.isParentOf(otherPath) && checkCallback(otherPath)) { - /** @type {ICustomStorageNode|ICustomStorageNodeMetaData} */ - let node; - if (include.metadata) { - /** @type {IDBRequest} */ - const valueCursor = cursor; - /** @type {IIndexedDBNodeData} */ - const data = valueCursor.result.value; - node = data.metadata; - if (include.value) { - // Load value! - const req = tx.objectStore('content').get(otherPath); - await new Promise((resolve, reject) => { - req.onerror = e => reject(e); - req.onsuccess = e => { - node.value = req.result; - resolve(); - } - }); - } - } - keepGoing = addCallback(otherPath, node); - } - if (keepGoing) { - try { cursor.result.continue(); } - catch(err) { - // We reached the end of the cursor? - keepGoing = false; - } - } - if (!keepGoing) { - resolve(); - } - }; - }); - }, - descendantsOf(path, include, checkCallback, addCallback) { - // Use cursor to loop from path on - // NOTE: Implementation is almost identical to childrenOf, consider merging them - const pathInfo = CustomStorageHelpers.PathInfo.get(path); - const lockStores = include.value ? ['nodes','content'] : 'nodes'; - const tx = db.transaction(lockStores, 'readonly'); - const store = tx.objectStore('nodes'); - const query = IDBKeyRange.lowerBound(path, true); - return new Promise((resolve, reject) => { - /** @type {IDBRequest|IDBRequest} */ - const cursor = include.metadata ? store.openCursor(query) : store.openKeyCursor(query); - cursor.onerror = e => reject(e); - cursor.onsuccess = async e => { - /** @type {string} */ - const otherPath = cursor.result ? cursor.result.key : null; - let keepGoing = true; - if (otherPath === null) { - // No more results - keepGoing = false; - } - else if (!pathInfo.isAncestorOf(otherPath)) { - // Paths are sorted, no more ancestors to be expected! - keepGoing = false; - } - else if (checkCallback(otherPath)) { - /** @type {ICustomStorageNode|ICustomStorageNodeMetaData} */ - let node; - if (include.metadata) { - /** @type {IDBRequest} */ - const valueCursor = cursor; - /** @type {IIndexedDBNodeData} */ - const data = valueCursor.result.value; - node = data.metadata; - if (include.value) { - // Load value! - const req = tx.objectStore('content').get(otherPath); - await new Promise((resolve, reject) => { - req.onerror = e => reject(e); - req.onsuccess = e => { - node.value = req.result; - resolve(); - } - }); - } - } - keepGoing = addCallback(otherPath, node); - } - if (keepGoing) { - try { cursor.result.continue(); } - catch(err) { - // We reached the end of the cursor? - keepGoing = false; - } - } - if (!keepGoing) { - resolve(); - } - }; - }); - } + const transaction = new IndexedDBStorageTransaction(context, target); + await transaction.start(); + return transaction; + } }); - return new AceBase(dbname, { logLevel: settings.logLevel, storage: storageSettings }); } @@ -3784,94 +3601,369 @@ class BrowserAceBase extends AceBase { // Determine whether to use localStorage or sessionStorage const localStorage = settings.provider ? settings.provider : settings.temp ? window.localStorage : window.sessionStorage; - // Helper functions to prefix all localStorage keys with dbname - // to allows multiple db's in localStorage - const storageKeysPrefix = `${dbname}.acebase::`; // Prefix all localStorage keys with dbname - function getPathFromStorageKey(key) { - return key.slice(storageKeysPrefix.length); - } - function getStorageKeyForPath(path) { - return `${storageKeysPrefix}${path}`; - } - // Setup our CustomStorageSettings const storageSettings = new CustomStorageSettings({ name: 'LocalStorage', + locking: true, ready() { + // LocalStorage is always ready return Promise.resolve(); }, - get(path) { - // Gets value from localStorage, wrapped in Promise - return new Promise(resolve => { - const json = localStorage.getItem(getStorageKeyForPath(path)); - const val = JSON.parse(json); - resolve(val); - }); - }, - set(path, val) { - // Sets value in localStorage, wrapped in Promise - return new Promise(resolve => { - const json = JSON.stringify(val); - localStorage.setItem(getStorageKeyForPath(path), json); - resolve(); - }); - }, - remove(path) { - // Removes a value from localStorage, wrapped in Promise - return new Promise(resolve => { - localStorage.removeItem(getStorageKeyForPath(path)); - resolve(); - }); - }, - childrenOf(path, include, checkCallback, addCallback) { - // Gets all child paths - // Cannot query localStorage, so loop through all stored keys to find children - return new Promise(resolve => { - const pathInfo = CustomStorageHelpers.PathInfo.get(path); - for (let i = 0; i < localStorage.length; i++) { - const key = localStorage.key(i); - if (!key.startsWith(storageKeysPrefix)) { continue; } - let otherPath = getPathFromStorageKey(key); - if (pathInfo.isParentOf(otherPath) && checkCallback(otherPath)) { - let node; - if (include.metadata || include.value) { - const json = localStorage.getItem(key); - node = JSON.parse(json); - } - const keepGoing = addCallback(otherPath, node); - if (!keepGoing) { break; } - } - } - resolve(); - }); - }, - descendantsOf(path, include, checkCallback, addCallback) { - // Gets all descendant paths - // Cannot query localStorage, so loop through all stored keys to find descendants - return new Promise(resolve => { - const pathInfo = CustomStorageHelpers.PathInfo.get(path); - for (let i = 0; i < localStorage.length; i++) { - const key = localStorage.key(i); - if (!key.startsWith(storageKeysPrefix)) { continue; } - let otherPath = getPathFromStorageKey(key); - if (pathInfo.isAncestorOf(otherPath) && checkCallback(otherPath)) { - let node; - if (include.metadata || include.value) { - const json = localStorage.getItem(key); - node = JSON.parse(json); - } - const keepGoing = addCallback(otherPath, node); - if (!keepGoing) { break; } - } - } - resolve(); - }); + getTransaction(target) { + // Create an instance of our transaction class + const context = { + debug: true, + dbname, + localStorage + } + const transaction = new LocalStorageTransaction(context, target); + return Promise.resolve(transaction); } }); return new AceBase(dbname, { logLevel: settings.logLevel, storage: storageSettings }); } } +class IndexedDBStorageTransaction extends CustomStorageTransaction { + + /** + * @param {{debug: boolean, db: typeof IndexedDB }} context + * @param {{path: string, write: boolean}} target + * @param {NodeLocker} nodeLocker + */ + constructor(context, target) { + super(target); + this.context = context; + } + + start() { + // Start transaction + return new Promise((resolve, reject) => { + this._tx = this.context.db.transaction(['nodes', 'content'], this.target.write ? 'readwrite' : 'readonly'); + resolve(); + }) + } + + commit() { + const tx = this._tx; + tx.commit && tx.commit(); + } + + rollback(err) { + const tx = this._tx; + tx.abort && tx.abort(); + } + + get(path) { + // Get metadata from "nodes" object store + const tx = this._tx; + const request = tx.objectStore('nodes').get(path); + return new Promise((resolve, reject) => { + request.onsuccess = event => { + /** @type {IIndexedDBNodeData} */ + const data = request.result; + if (!data) { + return resolve(null); + } + /** @type {ICustomStorageNode} */ + const node = data.metadata; + + // Node exists, get content from "content" object store + const contentReq = tx.objectStore('content').get(path); + contentReq.onsuccess = e => { + node.value = contentReq.result; + resolve(node); + }; + contentReq.onerror = e => reject(e); + } + request.onerror = e => { + console.error(`IndexedDB get error`, e); + reject(e); + } + }); + } + + set(path, node) { + /** @type {ICustomStorageNode} */ + const copy = {}; + const value = node.value; + Object.assign(copy, node); + delete copy.value; + /** @type {ICustomStorageNodeMetaData} */ + const metadata = copy; + /** @type {IIndexedDBNodeData} */ + const obj = { + path, + metadata + } + return new Promise((resolve, reject) => { + const tx = this._tx; + // Insert into "nodes" object store first + const request = tx.objectStore('nodes').put(obj); + request.onerror = e => reject(e); + request.onsuccess = e => { + // Now add to "content" object store + const contentReq = tx.objectStore('content').put(value, path); + contentReq.onsuccess = e => resolve(); + contentReq.onerror = e => { + tx.abort(); // rollback transaction + reject(e); + } + }; + }); + } + + remove(path) { + const tx = this._tx; + return new Promise((resolve, reject) => { + // First, remove from "content" object store + const r1 = tx.objectStore('content').delete(path); + r1.onerror = e => reject(e); + r1.onsuccess = e => { + // Now, remove from "nodes" data store + const r2 = tx.objectStore('nodes').delete(path); + r2.onerror = e => { + tx.abort(); // rollback transaction + reject(e); + }; + r2.onsuccess = e => resolve(); + } + }); + } + + childrenOf(path, include, checkCallback, addCallback) { + // Use cursor to loop from path on + const pathInfo = CustomStorageHelpers.PathInfo.get(path); + const tx = this._tx; + const store = tx.objectStore('nodes'); + const query = IDBKeyRange.lowerBound(path, true); + return new Promise((resolve, reject) => { + /** @type {IDBRequest|IDBRequest} */ + const cursor = include.metadata ? store.openCursor(query) : store.openKeyCursor(query); + cursor.onerror = e => reject(e); + cursor.onsuccess = async e => { + /** type {string} */ + const otherPath = cursor.result ? cursor.result.key : null; + let keepGoing = true; + if (otherPath === null) { + // No more results + keepGoing = false; + } + else if (!pathInfo.isAncestorOf(otherPath)) { + // Paths are sorted, no more children to be expected! + keepGoing = false; + } + else if (pathInfo.isParentOf(otherPath) && checkCallback(otherPath)) { + /** @type {ICustomStorageNode|ICustomStorageNodeMetaData} */ + let node; + if (include.metadata) { + /** @type {IDBRequest} */ + const valueCursor = cursor; + /** @type {IIndexedDBNodeData} */ + const data = valueCursor.result.value; + node = data.metadata; + if (include.value) { + // Load value! + const req = tx.objectStore('content').get(otherPath); + await new Promise((resolve, reject) => { + req.onerror = e => reject(e); + req.onsuccess = e => { + node.value = req.result; + resolve(); + } + }); + } + } + keepGoing = addCallback(otherPath, node); + } + if (keepGoing) { + try { cursor.result.continue(); } + catch(err) { + // We reached the end of the cursor? + keepGoing = false; + } + } + if (!keepGoing) { + resolve(); + } + }; + }); + } + + descendantsOf(path, include, checkCallback, addCallback) { + // Use cursor to loop from path on + // NOTE: Implementation is almost identical to childrenOf, consider merging them + const pathInfo = CustomStorageHelpers.PathInfo.get(path); + const tx = this._tx; + const store = tx.objectStore('nodes'); + const query = IDBKeyRange.lowerBound(path, true); + return new Promise((resolve, reject) => { + /** @type {IDBRequest|IDBRequest} */ + const cursor = include.metadata ? store.openCursor(query) : store.openKeyCursor(query); + cursor.onerror = e => reject(e); + cursor.onsuccess = async e => { + /** @type {string} */ + const otherPath = cursor.result ? cursor.result.key : null; + let keepGoing = true; + if (otherPath === null) { + // No more results + keepGoing = false; + } + else if (!pathInfo.isAncestorOf(otherPath)) { + // Paths are sorted, no more ancestors to be expected! + keepGoing = false; + } + else if (checkCallback(otherPath)) { + /** @type {ICustomStorageNode|ICustomStorageNodeMetaData} */ + let node; + if (include.metadata) { + /** @type {IDBRequest} */ + const valueCursor = cursor; + /** @type {IIndexedDBNodeData} */ + const data = valueCursor.result.value; + node = data.metadata; + if (include.value) { + // Load value! + const req = tx.objectStore('content').get(otherPath); + await new Promise((resolve, reject) => { + req.onerror = e => reject(e); + req.onsuccess = e => { + node.value = req.result; + resolve(); + } + }); + } + } + keepGoing = addCallback(otherPath, node); + } + if (keepGoing) { + try { cursor.result.continue(); } + catch(err) { + // We reached the end of the cursor? + keepGoing = false; + } + } + if (!keepGoing) { + resolve(); + } + }; + }); + } + +} + +// Setup CustomStorageTransaction for browser's LocalStorage +class LocalStorageTransaction extends CustomStorageTransaction { + + /** + * @param {{debug: boolean, dbname: string, localStorage: typeof window.localStorage}} context + * @param {{path: string, write: boolean}} target + */ + constructor(context, target) { + super(target); + this.context = context; + this._storageKeysPrefix = `${this.context.dbname}.acebase::`; + } + + commit() { + // All changes have already been committed + return Promise.resolve(); + } + + rollback(err) { + // Not able to rollback changes, because we did not keep track + return Promise.resolve(); + } + + get(path) { + // Gets value from localStorage, wrapped in Promise + return new Promise(resolve => { + const json = this.context.localStorage.getItem(this.getStorageKeyForPath(path)); + const val = JSON.parse(json); + resolve(val); + }); + } + + set(path, val) { + // Sets value in localStorage, wrapped in Promise + return new Promise(resolve => { + const json = JSON.stringify(val); + this.context.localStorage.setItem(this.getStorageKeyForPath(path), json); + resolve(); + }); + } + + remove(path) { + // Removes a value from localStorage, wrapped in Promise + return new Promise(resolve => { + this.context.localStorage.removeItem(this.getStorageKeyForPath(path)); + resolve(); + }); + } + + childrenOf(path, include, checkCallback, addCallback) { + // Streams all child paths + // Cannot query localStorage, so loop through all stored keys to find children + return new Promise(resolve => { + const pathInfo = CustomStorageHelpers.PathInfo.get(path); + for (let i = 0; i < this.context.localStorage.length; i++) { + const key = this.context.localStorage.key(i); + if (!key.startsWith(this._storageKeysPrefix)) { continue; } + let otherPath = this.getPathFromStorageKey(key); + if (pathInfo.isParentOf(otherPath) && checkCallback(otherPath)) { + let node; + if (include.metadata || include.value) { + const json = this.context.localStorage.getItem(key); + node = JSON.parse(json); + } + const keepGoing = addCallback(otherPath, node); + if (!keepGoing) { break; } + } + } + resolve(); + }); + } + + descendantsOf(path, include, checkCallback, addCallback) { + // Streams all descendant paths + // Cannot query localStorage, so loop through all stored keys to find descendants + return new Promise(resolve => { + const pathInfo = CustomStorageHelpers.PathInfo.get(path); + for (let i = 0; i < this.context.localStorage.length; i++) { + const key = this.context.localStorage.key(i); + if (!key.startsWith(this._storageKeysPrefix)) { continue; } + let otherPath = this.getPathFromStorageKey(key); + if (pathInfo.isAncestorOf(otherPath) && checkCallback(otherPath)) { + let node; + if (include.metadata || include.value) { + const json = this.context.localStorage.getItem(key); + node = JSON.parse(json); + } + const keepGoing = addCallback(otherPath, node); + if (!keepGoing) { break; } + } + } + resolve(); + }); + } + + /** + * Helper function to get the path from a localStorage key + * @param {string} key + */ + getPathFromStorageKey(key) { + return key.slice(this._storageKeysPrefix.length); + } + + /** + * Helper function to get the localStorage key for a path + * @param {string} path + */ + getStorageKeyForPath(path) { + return `${this._storageKeysPrefix}${path}`; + } +} + module.exports = { BrowserAceBase }; },{"./acebase-local":31,"./storage-custom":38}],31:[function(require,module,exports){ /** @@ -4840,7 +4932,7 @@ const { DataReference, DataSnapshot, EventSubscription, PathReference, TypeMappi const { AceBaseLocalSettings } = require('./acebase-local'); const { BrowserAceBase } = require('./acebase-browser'); const { LocalStorageSettings } = require('./storage-localstorage'); -const { CustomStorageSettings, CustomStorageHelpers } = require('./storage-custom'); +const { CustomStorageSettings, CustomStorageTransaction, CustomStorageHelpers } = require('./storage-custom'); const acebase = { AceBase: BrowserAceBase, @@ -4853,6 +4945,7 @@ const acebase = { TypeMappingOptions, LocalStorageSettings, CustomStorageSettings, + CustomStorageTransaction, CustomStorageHelpers }; @@ -5546,6 +5639,7 @@ module.exports = { },{"./node-info":34,"./node-value-types":36,"./storage":40,"colors":22}],38:[function(require,module,exports){ const { debug, ID, PathReference, PathInfo, ascii85 } = require('acebase-core'); const { NodeInfo } = require('./node-info'); +const { NodeLocker } = require('./node-lock'); const { VALUE_TYPES } = require('./node-value-types'); const { Storage, StorageSettings, NodeNotFoundError } = require('./storage'); @@ -5577,34 +5671,122 @@ class ICustomStorageNode extends ICustomStorageNodeMetaData { /** Enables get/set/remove operations to be wrapped in transactions to improve performance and reliability. */ class CustomStorageTransaction { + + /** + * @param {{ path: string, write: boolean }} target Which path the transaction is taking place on, and whether it is a read or read/write lock. If your storage backend does not support transactions, is synchronous, or if you are able to lock resources based on path: use storage.nodeLocker to ensure threadsafe transactions + */ + constructor(target) { + this.target = { + get originalPath() { return target.path; }, + path: target.path, + get write() { return target.write; } + }; + /** @type {string} Transaction ID */ + this.id = ID.generate(); + } + /** * @param {string} path * @returns {Promise} */ get(path) { throw new Error(`CustomStorageTransaction.get must be overridden by subclass`); } + /** * @param {string} path * @param {ICustomStorageNode} node * @returns {Promise} */ set(path, node) { throw new Error(`CustomStorageTransaction.set must be overridden by subclass`); } + /** * @param {string} path * @returns {Promise} */ remove(path) { throw new Error(`CustomStorageTransaction.remove must be overridden by subclass`); } + /** - * @param {string} reason + * + * @param {string} path Parent path to load children of + * @param {object} include + * @param {boolean} include.metadata Whether metadata needs to be loaded + * @param {boolean} include.value Whether value needs to be loaded + * @param {(childPath: string) => boolean} checkCallback callback method to precheck if child needs to be added, perform before loading metadata/value if possible + * @param {(childPath: string, node?: ICustomStorageNodeMetaData|ICustomStorageNode) => boolean} addCallback callback method that adds the child node. Returns whether or not to keep calling with more children + * @returns {Promise} Returns a promise that resolves when there are no more children to be streamed + */ + childrenOf(path, include, checkCallback, addCallback) { throw new Error(`CustomStorageTransaction.childrenOf must be overridden by subclass`); } + + /** + * + * @param {string} path Parent path to load descendants of + * @param {object} include + * @param {boolean} include.metadata Whether metadata needs to be loaded + * @param {boolean} include.value Whether value needs to be loaded + * @param {(childPath: string) => boolean} checkCallback callback method to precheck if descendant needs to be added, perform before loading metadata/value if possible + * @param {(childPath: string, node?: ICustomStorageNodeMetaData|ICustomStorageNode) => boolean} addCallback callback method that adds the descendant node. Returns whether or not to keep calling with more children + * @returns {Promise} Returns a promise that resolves when there are no more descendants to be streamed + */ + descendantsOf(path, include, checkCallback, addCallback) { throw new Error(`CustomStorageTransaction.descendantsOf must be overridden by subclass`); } + + /** + * Default implementation of getMultiple that executes .get for each given path. Override for custom logic + * @param {string[]} paths + * @returns {Promise>} Returns promise with a Map of paths to nodes + */ + getMultiple(paths) { + const map = new Map(); + return Promise.all(paths.map(path => this.get(path).then(val => map.set(path, val)))) + .then(done => map); + } + + /** + * Default implementation of setMultiple that executes .set for each given path. Override for custom logic + * @param {Array<{ path: string, node: ICustomStorageNode }>} nodes + */ + async setMultiple(nodes) { + await Promise.all(paths.map(({ path, node }) => this.set(path, node))); + } + + /** + * Default implementation of removeMultiple that executes .remove for each given path. Override for custom logic + * @param {string[]} paths + */ + async removeMultiple(paths) { + await Promise.all(paths.map(path => this.remove(path))); + } + + /** + * @param {Error} reason * @returns {Promise} */ rollback(reason) { throw new Error(`CustomStorageTransaction.rollback must be overridden by subclass`); } + /** * @returns {Promise} */ commit() { throw new Error(`CustomStorageTransaction.rollback must be overridden by subclass`); } - constructor() { - /** @type {string} Transaction ID */ - this.id = ID.generate(); + + /** + * Moves the transaction path to the parent node. If node locking is used, it will request a new lock + * Used internally, must not be overridden unless custom locking mechanism is required + * @param {string} targetPath; + */ + async moveToParentPath(targetPath) { + const currentPath = (this._lock && this._lock.path) || this.target.path; + if (currentPath === targetPath) { + return targetPath; // Already on the right path + } + const pathInfo = CustomStorageHelpers.PathInfo.get(targetPath); + if (pathInfo.isParentOf(currentPath)) { + if (this._lock) { + this._lock = await this._lock.moveToParent(); + } + } + else { + throw new Error(`Locking issue. Locked path "${this._lock.path}" is not a child/descendant of "${targetPath}"`); + } + this.target.path = targetPath; + return targetPath; } } @@ -5613,59 +5795,67 @@ class CustomStorageTransaction { * to get, set and remove data and you're done. */ class CustomStorageSettings extends StorageSettings { + /** * * @param {object} settings - * @param {string} settings.name Name of the custom storage adapter + * @param {string} [settings.name] Name of the custom storage adapter + * @param {boolean} [settings.locking=true] Whether default node locking should be used. Set to false if your storage backend disallows multiple simultanious write transactions (eg IndexedDB). Set to true if your storage backend does not support transactions (eg LocalStorage) or allows multiple simultanious write transactions (eg AceBase binary). * @param {() => Promise} settings.ready Function that returns a Promise that resolves once your data store backend is ready for use - * @param {(path: string) => Promise} settings.get Function that gets the node with given path from your custom data store, must return null if it doesn't exist - * @param {(path: string, value: ICustomStorageNode) => Promise} settings.set Function that inserts or updates a node with given path in your custom data store - * @param {(path: string) => Promise} settings.remove Function that removes the node with given path from your custom data store - * @param {(path: string, include: { value: boolean, metadata: boolean }, checkCallback: (childPath: string) => boolean, addCallback: (childPath: string, node?: ICustomStorageNodeMetaData|ICustomStorageNode) => boolean) => Promise} settings.childrenOf Function that streams all stored nodes that are direct children of the given path. For path "parent/path", results must include paths such as "parent/path/key" AND "parent/path[0]". 👉🏻 You can use CustomStorageHelpers for logic. Keep calling the add callback for each node until it returns false. - * @param {(path: string, include: { value: boolean, metadata: boolean }, checkCallback: (childPath: string) => boolean, addCallback: (descPath: string, node?: ICustomStorageNodeMetaData|ICustomStorageNode) => boolean) => Promise} settings.descendantsOf Function that streams all stored nodes that are descendants of the given path. For path "parent/path", results must include paths such as "parent/path/key", "parent/path/key/subkey", "parent/path[0]", "parent/path[12]/key" etc. 👉🏻 You can use CustomStorageHelpers for logic. Keep calling the add callback for each node until it returns false. - * @param {(paths: string[]) => Promise>} [settings.getMultiple] (optional, not used yet) Function that gets multiple nodes (metadata AND value) from your custom data store at once. Must return a Promise that resolves with Map - * @param {(paths: string[]) => Promise} [settings.removeMultiple] (optional) Function that removes multiple nodes from your custom data store at once + * @param {(target: { path: string, write: boolean }, nodeLocker: NodeLocker) => Promise} settings.getTransaction Function that starts a transaction for read/write operations on a specific path and/or child paths */ - constructor(settings) { + constructor(settings) { super(settings); settings = settings || {}; if (typeof settings.ready !== 'function') { throw new Error(`ready must be a function`); } - if (typeof settings.get !== 'function') { - throw new Error(`get must be a function`); - } - if (typeof settings.set !== 'function') { - throw new Error(`set must be a function`); - } - if (typeof settings.remove !== 'function') { - throw new Error(`remove must be a function`); - } - if (typeof settings.childrenOf !== 'function') { - throw new Error(`childrenOf must be a function`); - } - if (typeof settings.descendantsOf !== 'function') { - throw new Error(`descendantsOf must be a function`); + if (typeof settings.getTransaction !== 'function') { + throw new Error(`getTransaction must be a function`); } this.name = settings.name; this.info = `${this.name || 'CustomStorage'} realtime database`; + this.locking = settings.locking !== false; this.ready = settings.ready; - this.get = settings.get; - this.getMultiple = settings.getMultiple - || (paths => { - const map = new Map(); - return Promise.all(paths.map(path => this.get(path).then(val => map.set(path, val)))) - .then(done => map); - }); - this.set = settings.set; - this.remove = settings.remove; - this.removeMultiple = settings.removeMultiple - || (paths => { - return Promise.all(paths.map(path => this.remove(path))) - .then(done => true); - }); - this.childrenOf = settings.childrenOf; - this.descendantsOf = settings.descendantsOf; + + // Hijack getTransaction to add locking + const useLocking = this.locking; + const nodeLocker = useLocking ? new NodeLocker() : null; + this.getTransaction = async ({ path, write }) => { + // console.log(`${write ? 'WRITE' : 'READ'} transaction requested for path "${path}"`) + const transaction = await settings.getTransaction({ path, write }); + console.assert(typeof transaction.id === 'string', `transaction id not set`); + // console.log(`Got transaction ${transaction.id} for ${write ? 'WRITE' : 'READ'} on path "${path}"`); + + // Hijack rollback and commit + const rollback = transaction.rollback; + const commit = transaction.commit; + transaction.commit = async () => { + // console.log(`COMMIT ${transaction.id} for ${write ? 'WRITE' : 'READ'} on path "${path}"`); + const ret = await commit.call(transaction); + // console.log(`COMMIT DONE ${transaction.id} for ${write ? 'WRITE' : 'READ'} on path "${path}"`); + if (useLocking) { + await transaction._lock.release('commit'); + } + return ret; + } + transaction.rollback = async (reason) => { + // const reasonText = reason instanceof Error ? reason.message : reason.toString(); + // console.error(`ROLLBACK ${transaction.id} for ${write ? 'WRITE' : 'READ'} on path "${path}":`, reason); + const ret = await rollback.call(transaction, reason); + // console.log(`ROLLBACK DONE ${transaction.id} for ${write ? 'WRITE' : 'READ'} on path "${path}"`); + if (useLocking) { + await transaction._lock.release('rollback'); + } + return ret; + } + + if (useLocking) { + // Lock the path before continuing + transaction._lock = await nodeLocker.lock(path, transaction.id, write, `${this.name}::getTransaction`); + } + return transaction; + } } }; @@ -5772,8 +5962,7 @@ class CustomStorage extends Storage { this._init(); } - - _init() { + async _init() { /** @type {CustomStorageSettings} */ this._customImplementation = this.settings; this.debug.log(`Database "${this.name}" details:`.intro); @@ -5783,34 +5972,28 @@ class CustomStorage extends Storage { this.debug.log(`- Autoremove undefined props: ${this.settings.removeVoidProperties}`); // Create root node if it's not there yet - return this._customImplementation.ready() - .then(ready => this.getNodeInfo('')) - .then(info => { - if (!info.exists) { - return this._writeNode('', {}); - } - }) - .then(() => { - return this.indexes.supported && this.indexes.load(); - }) - .then(() => { - this.emit('ready'); - }); + await this._customImplementation.ready(); + const transaction = await this._customImplementation.getTransaction({ path: '', write: true }); + const info = await this.getNodeInfo('', { transaction }); + if (!info.exists) { + await this._writeNode('', {}, { transaction }); + } + await transaction.commit(); + if (this.indexes.supported) { + await this.indexes.load(); + } + this.emit('ready'); } /** * * @param {string} path - * @param {object} info - * @param {number} info.type - * @param {any} info.value - * @param {string} info.revision - * @param {number} info.revision_nr - * @param {number} info.created - * @param {number} info.modified + * @param {ICustomStorageNode} node + * @param {object} options + * @param {CustomStorageTransaction} options.transaction * @returns {Promise} */ - _storeNode(path, info) { + _storeNode(path, node, options) { // serialize the value to store const getTypedChildValue = val => { if (val === null) { @@ -5835,34 +6018,34 @@ class CustomStorage extends Storage { } const unprocessed = `Caller should have pre-processed the value by converting it to a string`; - if (info.type === VALUE_TYPES.ARRAY && info.value instanceof Array) { + if (node.type === VALUE_TYPES.ARRAY && node.value instanceof Array) { // Convert array to object with numeric properties // NOTE: caller should have done this already console.warn(`Unprocessed array. ${unprocessed}`); const obj = {}; - for (let i = 0; i < info.value.length; i++) { - obj[i] = info.value[i]; + for (let i = 0; i < node.value.length; i++) { + obj[i] = node.value[i]; } - info.value = obj; + node.value = obj; } - if (info.type === VALUE_TYPES.BINARY && typeof info.value !== 'string') { + if (node.type === VALUE_TYPES.BINARY && typeof node.value !== 'string') { console.warn(`Unprocessed binary value. ${unprocessed}`); - info.value = ascii85.encode(info.value); + node.value = ascii85.encode(node.value); } - if (info.type === VALUE_TYPES.REFERENCE && info.value instanceof PathReference) { + if (node.type === VALUE_TYPES.REFERENCE && node.value instanceof PathReference) { console.warn(`Unprocessed path reference. ${unprocessed}`); - info.value = info.value.path; + node.value = node.value.path; } - if ([VALUE_TYPES.OBJECT, VALUE_TYPES.ARRAY].includes(info.type)) { - const original = info.value; - info.value = {}; + if ([VALUE_TYPES.OBJECT, VALUE_TYPES.ARRAY].includes(node.type)) { + const original = node.value; + node.value = {}; // If original is an array, it'll automatically be converted to an object now Object.keys(original).forEach(key => { - info.value[key] = getTypedChildValue(original[key]); + node.value[key] = getTypedChildValue(original[key]); }); } - return this._customImplementation.set(path, info); + return options.transaction.set(path, node); } /** @@ -5928,9 +6111,14 @@ class CustomStorage extends Storage { } } - async _readNode(path) { + /** + * @param {string} path + * @param {object} options + * @param {CustomStorageTransaction} options.transaction + */ + async _readNode(path, options) { // deserialize a stored value (always an object with "type", "value", "revision", "revision_nr", "created", "modified") - let node = await this._customImplementation.get(path); + let node = await options.transaction.get(path); if (node === null) { return null; } if (typeof node !== 'object') { throw new Error(`CustomStorage get function must return an ICustomStorageNode object. Use JSON.parse if your set function stored it as a string`); @@ -5980,11 +6168,13 @@ class CustomStorage extends Storage { * Creates or updates a node in its own record. DOES NOT CHECK if path exists in parent node, or if parent paths exist! Calling code needs to do this * @param {string} path * @param {any} value - * @param {object} [options] - * @param {CustomStorageTransaction} [options.transaction] TODO: implement + * @param {object} options] + * @param {CustomStorageTransaction} options.transaction + * @param {boolean} [options.merge=false] + * @param {string} [options.revision] * @returns {Promise} */ - async _writeNode(path, value, options = { merge: false, revision: null, transaction: null }) { + async _writeNode(path, value, options) { if (this.valueFitsInline(value) && path !== '') { throw new Error(`invalid value to store in its own node`); } @@ -5992,9 +6182,11 @@ class CustomStorage extends Storage { throw new Error(`Invalid root node value. Must be an object`); } + const transaction = options.transaction; + // Get info about current node at path - const currentRow = await this._readNode(path); - const newRevision = (options && options.revision) || ID.generate(); + const currentRow = await this._readNode(path, { transaction }); + const revision = options.revision || ID.generate(); let mainNode = { type: VALUE_TYPES.OBJECT, value: {} @@ -6104,7 +6296,7 @@ class CustomStorage extends Storage { keys.push(key); return true; // Keep streaming } - await this._customImplementation.childrenOf(path, { metadata: false, value: false }, includeChildCheck, addChildPath); + await transaction.childrenOf(path, { metadata: false, value: false }, includeChildCheck, addChildPath); children.current = children.current.concat(keys); if (newIsObjectOrArray) { @@ -6128,7 +6320,7 @@ class CustomStorage extends Storage { const writePromises = Object.keys(childNodeValues).map(key => { const childPath = pathInfo.childPath(key); // PathInfo.getChildPath(path, key); const childValue = childNodeValues[key]; - return this._writeNode(childPath, childValue, { revision: newRevision, merge: false }); + return this._writeNode(childPath, childValue, { transaction, revision, merge: false }); }); // Delete all child nodes that were stored in their own record, but are being removed @@ -6137,7 +6329,7 @@ class CustomStorage extends Storage { const deleteDedicatedKeys = changes.delete.concat(movingNodes); const deletePromises = deleteDedicatedKeys.map(key => { const childPath = pathInfo.childPath(key); //PathInfo.getChildPath(path, key); - return this._deleteNode(childPath); + return this._deleteNode(childPath, { transaction }); }); const promises = writePromises.concat(deletePromises); @@ -6145,6 +6337,7 @@ class CustomStorage extends Storage { } // Update main node + // TODO: Check if revision should change? return await this._storeNode(path, { type: mainNode.type, value: mainNode.value, @@ -6152,6 +6345,8 @@ class CustomStorage extends Storage { revision_nr: currentRow.revision_nr + 1, created: currentRow.created, modified: Date.now() + }, { + transaction }); } else { @@ -6162,17 +6357,19 @@ class CustomStorage extends Storage { const promises = Object.keys(childNodeValues).map(key => { const childPath = PathInfo.getChildPath(path, key); const childValue = childNodeValues[key]; - return this._writeNode(childPath, childValue, { revision: newRevision, merge: false }); + return this._writeNode(childPath, childValue, { transaction, revision, merge: false }); }); // Create current node const p = this._storeNode(path, { type: mainNode.type, value: mainNode.value, - revision: newRevision, + revision, revision_nr: 1, created: Date.now(), modified: Date.now() + }, { + transaction }); promises.push(p); return Promise.all(promises); @@ -6182,8 +6379,10 @@ class CustomStorage extends Storage { /** * Deletes (dedicated) node and all subnodes without checking for existence. Use with care - all removed nodes will lose their revision stats! DOES NOT REMOVE INLINE CHILD NODES! * @param {string} path + * @param {object} options + * @param {CustomStorageTransaction} options.transaction */ - async _deleteNode(path) { + async _deleteNode(path, options) { const pathInfo = PathInfo.get(path); this.debug.log(`Node "/${path}" is being deleted`.cyan); @@ -6204,19 +6403,23 @@ class CustomStorage extends Storage { deletePaths.push(descPath); return true; }; - await this._customImplementation.descendantsOf(path, { metadata: false, value: false }, includeDescendantCheck, addDescendant); + const transaction = options.transaction; + await transaction.descendantsOf(path, { metadata: false, value: false }, includeDescendantCheck, addDescendant); this.debug.log(`Nodes ${deletePaths.map(p => `"/${p}"`).join(',')} are being deleted`.cyan); - return this._customImplementation.removeMultiple(deletePaths); + return transaction.removeMultiple(deletePaths); } /** * Enumerates all children of a given Node for reflection purposes * @param {string} path + * @param {object} [options] + * @param {CustomStorageTransaction} [options.transaction] * @param {string[]|number[]} [options.keyFilter] */ - getChildren(path, options = { keyFilter: undefined, tid: undefined }) { + getChildren(path, options) { // return generator + options = options || {}; var callback; //, resolve, reject; const generator = { /** @@ -6229,128 +6432,159 @@ class CustomStorage extends Storage { return start(); } }; - const start = () => { - let lock, canceled = false; - const tid = (options && options.tid) || ID.generate(); - return this.nodeLocker.lock(path, tid, false, 'getChildren') - .then(async l => { - lock = l; + const start = async () => { + // let lock; + const transaction = options.transaction || await this._customImplementation.getTransaction({ path, write: false }); + // return this.nodeLocker.lock(path, transaction.id, false, 'getChildren') + // .then(async l => { + // lock = l; + try { + let canceled = false; + await (async () => { + let node = await this._readNode(path, { transaction }); + if (!node) { throw new NodeNotFoundError(`Node "/${path}" does not exist`); } + // node = JSON.parse(node); - let node = await this._readNode(path); - if (!node) { throw new NodeNotFoundError(`Node "/${path}" does not exist`); } - // node = JSON.parse(node); - - if (![VALUE_TYPES.OBJECT, VALUE_TYPES.ARRAY].includes(node.type)) { - // No children - return; - } - const isArray = node.type === VALUE_TYPES.ARRAY; - const value = node.value; - let keys = Object.keys(value); - if (options.keyFilter) { - keys = keys.filter(key => options.keyFilter.includes(key)); - } - const pathInfo = PathInfo.get(path); - keys.length > 0 && keys.every(key => { - let child = this._getTypeFromStoredValue(value[key]); - - const info = new CustomStorageNodeInfo({ - path: pathInfo.childPath(key), - key: isArray ? null : key, - index: isArray ? key : null, - type: child.type, - address: null, - exists: true, - value: child.value, - revision: node.revision, - revision_nr: node.revision_nr, - created: node.created, - modified: node.modified - }); - - canceled = callback(info) === false; - return !canceled; // stop .every loop if canceled - }); - if (canceled) { - return; - } - - // Go on... get other children - let checkExecuted = false; - const includeChildCheck = (childPath) => { - checkExecuted = true; - if (!pathInfo.isParentOf(childPath)) { - // Double check failed - throw new Error(`"${childPath}" is not a child of "${path}" - childrenOf must only check and return paths that are children`); + if (![VALUE_TYPES.OBJECT, VALUE_TYPES.ARRAY].includes(node.type)) { + // No children + return; } + const isArray = node.type === VALUE_TYPES.ARRAY; + const value = node.value; + let keys = Object.keys(value); if (options.keyFilter) { - const key = PathInfo.get(childPath).key; - return options.keyFilter.includes(key); + keys = keys.filter(key => options.keyFilter.includes(key)); } - return true; - }; + const pathInfo = PathInfo.get(path); + keys.length > 0 && keys.every(key => { + let child = this._getTypeFromStoredValue(value[key]); - /** - * - * @param {string} childPath - * @param {ICustomStorageNodeMetaData} node - */ - const addChildNode = (childPath, node) => { - if (!checkExecuted) { - throw new Error(`${this._customImplementation.info} childrenOf did not call checkCallback before addCallback`); - } - const key = PathInfo.get(childPath).key; - const info = new CustomStorageNodeInfo({ - path: childPath, - type: node.type, - key: isArray ? null : key, - index: isArray ? key : null, - address: new CustomStorageNodeAddress(childPath), - exists: true, - value: null, // not loaded - revision: node.revision, - revision_nr: node.revision_nr, - created: new Date(node.created), - modified: new Date(node.modified) + const info = new CustomStorageNodeInfo({ + path: pathInfo.childPath(key), + key: isArray ? null : key, + index: isArray ? key : null, + type: child.type, + address: null, + exists: true, + value: child.value, + revision: node.revision, + revision_nr: node.revision_nr, + created: node.created, + modified: node.modified + }); + + canceled = callback(info) === false; + return !canceled; // stop .every loop if canceled }); + if (canceled) { + return; + } - canceled = callback(info) === false; - return !canceled; - }; - return this._customImplementation.childrenOf(path, { metadata: true, value: false }, includeChildCheck, addChildNode); - }) - .then(() => { - lock.release(); + // Go on... get other children + let checkExecuted = false; + const includeChildCheck = (childPath) => { + checkExecuted = true; + if (!pathInfo.isParentOf(childPath)) { + // Double check failed + throw new Error(`"${childPath}" is not a child of "${path}" - childrenOf must only check and return paths that are children`); + } + if (options.keyFilter) { + const key = PathInfo.get(childPath).key; + return options.keyFilter.includes(key); + } + return true; + }; + + /** + * + * @param {string} childPath + * @param {ICustomStorageNodeMetaData} node + */ + const addChildNode = (childPath, node) => { + if (!checkExecuted) { + throw new Error(`${this._customImplementation.info} childrenOf did not call checkCallback before addCallback`); + } + const key = PathInfo.get(childPath).key; + const info = new CustomStorageNodeInfo({ + path: childPath, + type: node.type, + key: isArray ? null : key, + index: isArray ? key : null, + address: new CustomStorageNodeAddress(childPath), + exists: true, + value: null, // not loaded + revision: node.revision, + revision_nr: node.revision_nr, + created: new Date(node.created), + modified: new Date(node.modified) + }); + + canceled = callback(info) === false; + return !canceled; + }; + await transaction.childrenOf(path, { metadata: true, value: false }, includeChildCheck, addChildNode); + })(); + if (!options.transaction) { + // transaction was created by us, commit + await transaction.commit(); + } return canceled; - }) - .catch(err => { - lock.release(); + } + catch (err) { + if (!options.transaction) { + // transaction was created by us, rollback + await transaction.rollback(err); + } throw err; - }); + } + + // }) + // .then(() => { + // lock.release(); + // return canceled; + // }) + // .catch(err => { + // lock.release(); + // throw err; + // }); }; // start() return generator; } - getNode(path, options = { include: undefined, exclude: undefined, child_objects: true, tid: undefined }) { + /** + * + * @param {string} path + * @param {object} [options] + * @param {string[]} [options.include] + * @param {string[]} [options.exclude] + * @param {boolean} [options.child_objects=true] + * @param {CustomStorageTransaction} [options.transaction] + * @returns {Promise} + */ + async getNode(path, options) { // path = path.replace(/'/g, ''); // prevent sql injection, remove single quotes - const tid = (options && options.tid )|| ID.generate(); - let lock; - return this.nodeLocker.lock(path, tid, false, 'getNode') - .then(async l => { - lock = l; - - // Get path, path/* and path[* - const filtered = options && (options.include || options.exclude || options.child_objects === false); - const pathInfo = PathInfo.get(path); - const targetNode = await this._readNode(path); - if (!targetNode) { - // Lookup parent node - if (path === '') { return { value: null }; } // path is root. There is no parent. - return lock.moveToParent() - .then(async parentLock => { - lock = parentLock; - let parentNode = await this._readNode(pathInfo.parentPath); + options = options || {}; + const transaction = options.transaction || await this._customImplementation.getTransaction({ path, write: false }); + // let lock; + // return this.nodeLocker.lock(path, tid, false, 'getNode') + // .then(async l => { + // lock = l; + try { + const node = await (async () => { + // Get path, path/* and path[* + const filtered = options.include || options.exclude || options.child_objects === false; + const pathInfo = PathInfo.get(path); + const targetNode = await this._readNode(path, { transaction }); + if (!targetNode) { + // Lookup parent node + if (path === '') { return { value: null }; } // path is root. There is no parent. + const lockPath = await transaction.moveToParentPath(pathInfo.parentPath); + console.assert(lockPath === pathInfo.parentPath, `transaction.moveToParentPath() did not move to the right parent path of "${path}"`) + // return lock.moveToParent() + // .then(async parentLock => { + // lock = parentLock; + let parentNode = await this._readNode(pathInfo.parentPath, { transaction }); if (parentNode && [VALUE_TYPES.OBJECT, VALUE_TYPES.ARRAY].includes(parentNode.type) && pathInfo.key in parentNode) { const childValueInfo = this._getTypeFromStoredValue(parentNode.value[pathInfo.key]); return { @@ -6363,198 +6597,215 @@ class CustomStorage extends Storage { }; } return { value: null }; - }); - } - - const includeCheck = options.include - ? new RegExp('^' + options.include.map(p => '(?:' + p.replace(/\*/g, '[^/\\[]+') + ')').join('|') + '(?:$|[/\\[])') - : null; - const excludeCheck = options.exclude - ? new RegExp('^' + options.exclude.map(p => '(?:' + p.replace(/\*/g, '[^/\\[]+') + ')').join('|') + '(?:$|[/\\[])') - : null; - - let checkExecuted = false; - const includeDescendantCheck = (descPath) => { - checkExecuted = true; - if (!pathInfo.isAncestorOf(descPath)) { - // Double check failed - throw new Error(`"${descPath}" is not a descendant of "${path}" - descendantsOf must only check and return paths that are descendants`); + // }); } - if (!filtered) { return true; } - // Apply include & exclude filters - let checkPath = descPath.slice(path.length); - if (checkPath[0] === '/') { checkPath = checkPath.slice(1); } - let include = (includeCheck ? includeCheck.test(checkPath) : true) - && (excludeCheck ? !excludeCheck.test(checkPath) : true); + const includeCheck = options.include + ? new RegExp('^' + options.include.map(p => '(?:' + p.replace(/\*/g, '[^/\\[]+') + ')').join('|') + '(?:$|[/\\[])') + : null; + const excludeCheck = options.exclude + ? new RegExp('^' + options.exclude.map(p => '(?:' + p.replace(/\*/g, '[^/\\[]+') + ')').join('|') + '(?:$|[/\\[])') + : null; - // Apply child_objects filter - if (include - && options.child_objects === false - && (pathInfo.isParentOf(descPath) && [VALUE_TYPES.OBJECT, VALUE_TYPES.ARRAY].includes(childNode.type) - || PathInfo.getPathKeys(descPath).length > pathInfo.pathKeys.length + 1) - ) { - include = false; + let checkExecuted = false; + const includeDescendantCheck = (descPath) => { + checkExecuted = true; + if (!pathInfo.isAncestorOf(descPath)) { + // Double check failed + throw new Error(`"${descPath}" is not a descendant of "${path}" - descendantsOf must only check and return paths that are descendants`); + } + if (!filtered) { return true; } + + // Apply include & exclude filters + let checkPath = descPath.slice(path.length); + if (checkPath[0] === '/') { checkPath = checkPath.slice(1); } + let include = (includeCheck ? includeCheck.test(checkPath) : true) + && (excludeCheck ? !excludeCheck.test(checkPath) : true); + + // Apply child_objects filter + if (include + && options.child_objects === false + && (pathInfo.isParentOf(descPath) && [VALUE_TYPES.OBJECT, VALUE_TYPES.ARRAY].includes(childNode.type) + || PathInfo.getPathKeys(descPath).length > pathInfo.pathKeys.length + 1) + ) { + include = false; + } + return include; } - return include; - } - const descRows = []; - /** - * - * @param {string} descPath - * @param {ICustomStorageNode} node - */ - const addDescendant = (descPath, node) => { - if (!checkExecuted) { - throw new Error(`${this._customImplementation.info} descendantsOf did not call checkCallback before addCallback`); + const descRows = []; + /** + * + * @param {string} descPath + * @param {ICustomStorageNode} node + */ + const addDescendant = (descPath, node) => { + if (!checkExecuted) { + throw new Error(`${this._customImplementation.info} descendantsOf did not call checkCallback before addCallback`); + } + + // Process the value + this._processReadNodeValue(node); + + // Add node + node.path = descPath; + descRows.push(node); + + return true; // Keep streaming + }; + + await transaction.descendantsOf(path, { metadata: true, value: true }, includeDescendantCheck, addDescendant); + + this.debug.log(`Read node "/${path}" and ${filtered ? '(filtered) ' : ''}descendants from ${descRows.length + 1} records`.magenta); + + const result = targetNode; + + const objectToArray = obj => { + // Convert object value to array + const arr = []; + Object.keys(obj).forEach(key => { + let index = parseInt(key); + arr[index] = obj[index]; + }); + return arr; + }; + + if (targetNode.type === VALUE_TYPES.ARRAY) { + result.value = objectToArray(result.value); } - - // Process the value - this._processReadNodeValue(node); - - // Add node - node.path = descPath; - descRows.push(node); - return true; // Keep streaming - }; - - await this._customImplementation.descendantsOf(path, { metadata: true, value: true }, includeDescendantCheck, addDescendant); - - this.debug.log(`Read node "/${path}" and ${filtered ? '(filtered) ' : ''}children from ${descRows.length + 1} records`.magenta); - - const result = targetNode; - - const objectToArray = obj => { - // Convert object value to array - const arr = []; - Object.keys(obj).forEach(key => { - let index = parseInt(key); - arr[index] = obj[index]; - }); - return arr; - }; - - if (targetNode.type === VALUE_TYPES.ARRAY) { - result.value = objectToArray(result.value); - } - - if (targetNode.type === VALUE_TYPES.OBJECT || targetNode.type === VALUE_TYPES.ARRAY) { - // target node is an object or array - // merge with other found (child) nodes - const targetPathKeys = PathInfo.getPathKeys(path); - let value = targetNode.value; - for (let i = 0; i < descRows.length; i++) { - const otherNode = descRows[i]; - const pathKeys = PathInfo.getPathKeys(otherNode.path); - const trailKeys = pathKeys.slice(targetPathKeys.length); - let parent = value; - for (let j = 0 ; j < trailKeys.length; j++) { - console.assert(typeof parent === 'object', 'parent must be an object/array to have children!!'); - const key = trailKeys[j]; - const isLast = j === trailKeys.length-1; - const nodeType = isLast - ? otherNode.type - : typeof trailKeys[j+1] === 'number' - ? VALUE_TYPES.ARRAY - : VALUE_TYPES.OBJECT; - let nodeValue; - if (!isLast) { - nodeValue = nodeType === VALUE_TYPES.OBJECT ? {} : []; - } - else { - nodeValue = otherNode.value; - if (nodeType === VALUE_TYPES.ARRAY) { - nodeValue = objectToArray(nodeValue); + if (targetNode.type === VALUE_TYPES.OBJECT || targetNode.type === VALUE_TYPES.ARRAY) { + // target node is an object or array + // merge with other found (child) nodes + const targetPathKeys = PathInfo.getPathKeys(path); + let value = targetNode.value; + for (let i = 0; i < descRows.length; i++) { + const otherNode = descRows[i]; + const pathKeys = PathInfo.getPathKeys(otherNode.path); + const trailKeys = pathKeys.slice(targetPathKeys.length); + let parent = value; + for (let j = 0 ; j < trailKeys.length; j++) { + console.assert(typeof parent === 'object', 'parent must be an object/array to have children!!'); + const key = trailKeys[j]; + const isLast = j === trailKeys.length-1; + const nodeType = isLast + ? otherNode.type + : typeof trailKeys[j+1] === 'number' + ? VALUE_TYPES.ARRAY + : VALUE_TYPES.OBJECT; + let nodeValue; + if (!isLast) { + nodeValue = nodeType === VALUE_TYPES.OBJECT ? {} : []; } + else { + nodeValue = otherNode.value; + if (nodeType === VALUE_TYPES.ARRAY) { + nodeValue = objectToArray(nodeValue); + } + } + if (key in parent) { + // Merge with parent + console.assert(typeof parent[key] === typeof nodeValue && [VALUE_TYPES.OBJECT, VALUE_TYPES.ARRAY].includes(nodeType), 'Merging child values can only be done if existing and current values are both an array or object'); + Object.keys(nodeValue).forEach(childKey => { + console.assert(!(childKey in parent[key]), 'child key is in parent value already?! HOW?!'); + parent[key][childKey] = nodeValue[childKey]; + }); + } + else { + parent[key] = nodeValue; + } + parent = parent[key]; } - if (key in parent) { - // Merge with parent - console.assert(typeof parent[key] === typeof nodeValue && [VALUE_TYPES.OBJECT, VALUE_TYPES.ARRAY].includes(nodeType), 'Merging child values can only be done if existing and current values are both an array or object'); - Object.keys(nodeValue).forEach(childKey => { - console.assert(!(childKey in parent[key]), 'child key is in parent value already?! HOW?!'); - parent[key][childKey] = nodeValue[childKey]; + } + } + else if (descRows.length > 0) { + throw new Error(`multiple records found for non-object value!`); + } + + // Post process filters to remove any data that got though because they were + // not stored in dedicated records. This will happen with smaller values because + // they are stored inline in their parent node. + // eg: + // { number: 1, small_string: 'small string', bool: true, obj: {}, arr: [] } + // All properties of this object are stored inline, + // if exclude: ['obj'], or child_objects: false was passed, these will still + // have to be removed from the value + + if (options.child_objects === false) { + Object.keys(result.value).forEach(key => { + if (typeof result.value[key] === 'object' && result.value[key].constructor === Object) { + // This can only happen if the object was empty + console.assert(Object.keys(result.value[key]).length === 0); + delete result.value[key]; + } + }) + } + + if (options.exclude) { + const process = (obj, keys) => { + if (typeof obj !== 'object') { return; } + const key = keys[0]; + if (key === '*') { + Object.keys(obj).forEach(k => { + process(obj[k], keys.slice(1)); }); } - else { - parent[key] = nodeValue; + else if (keys.length > 1) { + key in obj && process(obj[key], keys.slice(1)); } - parent = parent[key]; - } + else { + delete obj[key]; + } + }; + options.exclude.forEach(path => { + const checkKeys = PathInfo.getPathKeys(path); + process(result.value, checkKeys); + }); } + + return result; + })(); + if (!options.transaction) { + // transaction was created by us, commit + await transaction.commit(); + } + return node; + } + catch (err) { + if (!options.transaction) { + // transaction was created by us, rollback + await transaction.rollback(err); } - else if (descRows.length > 0) { - throw new Error(`multiple records found for non-object value!`); - } - - // Post process filters to remove any data that got though because they were - // not stored in dedicated records. This will happen with smaller values because - // they are stored inline in their parent node. - // eg: - // { number: 1, small_string: 'small string', bool: true, obj: {}, arr: [] } - // All properties of this object are stored inline, - // if exclude: ['obj'], or child_objects: false was passed, these will still - // have to be removed from the value - - if (options.child_objects === false) { - Object.keys(result.value).forEach(key => { - if (typeof result.value[key] === 'object' && result.value[key].constructor === Object) { - // This can only happen if the object was empty - console.assert(Object.keys(result.value[key]).length === 0); - delete result.value[key]; - } - }) - } - - if (options.exclude) { - const process = (obj, keys) => { - if (typeof obj !== 'object') { return; } - const key = keys[0]; - if (key === '*') { - Object.keys(obj).forEach(k => { - process(obj[k], keys.slice(1)); - }); - } - else if (keys.length > 1) { - key in obj && process(obj[key], keys.slice(1)); - } - else { - delete obj[key]; - } - }; - options.exclude.forEach(path => { - const checkKeys = PathInfo.getPathKeys(path); - process(result.value, checkKeys); - }); - } - return result; - }) - .then(result => { - lock.release(); - return result; - }) - .catch(err => { - lock.release(); throw err; - }); + } + // }) + // .then(result => { + // lock.release(); + // return result; + // }) + // .catch(err => { + // lock.release(); + // throw err; + // }); } /** * * @param {string} path - * @param {*} options + * @param {object} [options] + * @param {CustomStorageTransaction} [options.transaction] * @returns {Promise} */ - getNodeInfo(path, options = { tid: undefined }) { + async getNodeInfo(path, options) { + options = options || {}; const pathInfo = PathInfo.get(path); - const tid = (options && options.tid) || ID.generate(); - let lock; - return this.nodeLocker.lock(path, tid, false, 'getNodeInfo') - .then(async l => { - lock = l; - - const node = await this._readNode(path); + // let lock; + const transaction = options.transaction || await this._customImplementation.getTransaction({ path, write: true }); + // return this.nodeLocker.lock(path, transaction.id, false, 'getNodeInfo') + // .then(async l => { + // lock = l; + try { + const node = await this._readNode(path, { transaction }); const info = new CustomStorageNodeInfo({ path, key: typeof pathInfo.key === 'string' ? pathInfo.key : null, @@ -6568,15 +6819,16 @@ class CustomStorage extends Storage { revision_nr: node ? node.revision_nr : null }); - if (node || path === '') { - return info; - } + if (!node && path !== '') { + // Try parent node - // Try parent node - return lock.moveToParent() - .then(async parentLock => { - lock = parentLock; - const parent = await this._readNode(pathInfo.parentPath); + // return lock.moveToParent() + // .then(async parentLock => { + // lock = parentLock; + + const lockPath = await transaction.moveToParentPath(pathInfo.parentPath); + console.assert(lockPath === pathInfo.parentPath, `transaction.moveToParentPath() did not move to the right parent path of "${path}"`) + const parent = await this._readNode(pathInfo.parentPath, { transaction }); if (parent && [VALUE_TYPES.OBJECT, VALUE_TYPES.ARRAY].includes(parent.type) && pathInfo.key in parent.value) { // Stored in parent node info.exists = true; @@ -6592,146 +6844,236 @@ class CustomStorage extends Storage { // Parent doesn't exist, so the node we're looking for cannot exist either info.address = null; } - return info; - }) - }) - .then(info => { - lock.release(); + } + + if (!options.transaction) { + // transaction was created by us, commit + await transaction.commit(); + } return info; - }) - .catch(err => { - lock && lock.release(); + // }) + } + catch (err) { + if (!options.transaction) { + // transaction was created by us, rollback + await transaction.rollback(err); + } throw err; - }); + } + // }) + // .then(info => { + // lock.release(); + // return info; + // }) + // .catch(err => { + // lock && lock.release(); + // throw err; + // }); } // TODO: Move to Storage base class? - removeNode(path, options = { tid: undefined }) { + /** + * + * @param {string} path + * @param {object} [options] + * @param {CustomStorageTransaction} [options.transaction] + * @returns {Promise} + */ + async removeNode(path, options) { if (path === '') { return Promise.reject(new Error(`Cannot remove the root node`)); } + options = options || {}; const pathInfo = PathInfo.get(path); - const tid = (options && options.tid) || ID.generate(); - return this.nodeLocker.lock(pathInfo.parentPath, tid, true, 'removeNode') - .then(lock => { - return this.updateNode(pathInfo.parentPath, { [pathInfo.key]: null }, { tid }) - .then(result => { - lock.release(); - return result; - }) - .catch(err => { - lock.release(); - throw err; - }); - }); + const transaction = options.transaction || this._customImplementation.getTransaction({ path, write: true }); + // return this.nodeLocker.lock(pathInfo.parentPath, transaction.id, true, 'removeNode') + // .then(lock => { + try { + await this.updateNode(pathInfo.parentPath, { [pathInfo.key]: null }, { transaction }); + if (!options.transaction) { + // transaction was created by us, commit + await transaction.commit(); + } + } + catch (err) { + if (!options.transaction) { + // transaction was created by us, rollback + await transaction.rollback(err); + } + throw err; + } + // .then(result => { + // lock.release(); + // return result; + // }) + // .catch(err => { + // lock.release(); + // throw err; + // }); + // }); } // TODO: Move to Storage base class? - setNode(path, value, options = { assert_revision: undefined, tid: undefined }) { + /** + * + * @param {string} path + * @param {any} value + * @param {object} [options] + * @param {string} [options.assert_revision] + * @param {CustomStorageTransaction} [options.transaction] + * @returns {Promise} + */ + async setNode(path, value, options) { const pathInfo = PathInfo.get(path); - let lock; - const tid = (options && options.tid) || ID.generate(); - return this.nodeLocker.lock(path, tid, true, 'setNode') - .then(l => { - lock = l; + // let lock; + options = options || {}; + const transaction = options.transaction || await this._customImplementation.getTransaction({ path, write: true }); + // return this.nodeLocker.lock(path, transaction.id, true, 'setNode') + // .then(l => { + // lock = l; + try { if (path === '') { if (value === null || typeof value !== 'object' || value instanceof Array || value instanceof ArrayBuffer || ('buffer' in value && value.buffer instanceof ArrayBuffer)) { - return Promise.reject(new Error(`Invalid value for root node: ${value}`)); + throw new Error(`Invalid value for root node: ${value}`); } - - return this._writeNodeWithTracking('', value, { merge: false, tid }) + await this._writeNodeWithTracking('', value, { merge: false, transaction }) } - - if (options && typeof options.assert_revision !== 'undefined') { - return this.getNodeInfo(path, { tid: lock.tid }) - .then(info => { - if (info.revision !== options.assert_revision) { - throw new NodeRevisionError(`revision '${info.revision}' does not match requested revision '${options.assert_revision}'`); - } - if (info.address && info.address.path === path && !this.valueFitsInline(value)) { - // Overwrite node - return this._writeNodeWithTracking(path, value, { merge: false, tid }); - } - else { - // Update parent node - return lock.moveToParent() - .then(parentLock => { - lock = parentLock; - return this._writeNodeWithTracking(pathInfo.parentPath, { [pathInfo.key]: value }, { merge: true, tid }); - }); - } - }) + else if ( typeof options.assert_revision !== 'undefined') { + const info = await this.getNodeInfo(path, { transaction }) + // .then(info => { + if (info.revision !== options.assert_revision) { + throw new NodeRevisionError(`revision '${info.revision}' does not match requested revision '${options.assert_revision}'`); + } + if (info.address && info.address.path === path && !this.valueFitsInline(value)) { + // Overwrite node + await this._writeNodeWithTracking(path, value, { merge: false, transaction }); + } + else { + // Update parent node + // return lock.moveToParent() + // .then(parentLock => { + // lock = parentLock; + const lockPath = await transaction.moveToParentPath(pathInfo.parentPath); + console.assert(lockPath === pathInfo.parentPath, `transaction.moveToParentPath() did not move to the right parent path of "${path}"`) + await this._writeNodeWithTracking(pathInfo.parentPath, { [pathInfo.key]: value }, { merge: true, transaction }); + // }); + } + // }) } else { // Delegate operation to update on parent node - return lock.moveToParent() - .then(parentLock => { - lock = parentLock; - return this.updateNode(pathInfo.parentPath, { [pathInfo.key]: value }, { tid }); - }); + // return lock.moveToParent() + // .then(parentLock => { + // lock = parentLock; + const lockPath = await transaction.moveToParentPath(pathInfo.parentPath); + console.assert(lockPath === pathInfo.parentPath, `transaction.moveToParentPath() did not move to the right parent path of "${path}"`) + await this.updateNode(pathInfo.parentPath, { [pathInfo.key]: value }, { transaction }); + // }); + } + if (!options.transaction) { + // transaction was created by us, commit + await transaction.commit(); + } + } + catch (err) { + if (!options.transaction) { + // transaction was created by us, rollback + await transaction.rollback(err); } - }) - .then(result => { - lock.release(); - return result; - }) - .catch(err => { - lock.release(); throw err; - }); + } + // }) + // .then(result => { + // lock.release(); + // return result; + // }) + // .catch(err => { + // lock.release(); + // throw err; + // }); } // TODO: Move to Storage base class? - updateNode(path, updates, options = { tid: undefined }) { + /** + * + * @param {string} path + * @param {*} updates + * @param {object} [options] + * @param {CustomStorageTransaction} [options.transaction] + */ + async updateNode(path, updates, options) { if (typeof updates !== 'object') { // || Object.keys(updates).length === 0 - return Promise.reject(new Error(`invalid updates argument`)); //. Must be a non-empty object or array + throw new Error(`invalid updates argument`); //. Must be a non-empty object or array } - const tid = (options && options.tid) || ID.generate(); - let lock; - return this.nodeLocker.lock(path, tid, true, 'updateNode') - .then(l => { - lock = l; + options = options || {}; + const transaction = options.transaction || await this._customImplementation.getTransaction({ path, write: true }); + // const tid = (options && options.tid) || ID.generate(); + // let lock; + // return this.nodeLocker.lock(path, tid, true, 'updateNode') + // .then(l => { + // lock = l; + + try { // Get info about current node - return this.getNodeInfo(path, { tid: lock.tid }); - }) - .then(nodeInfo => { + const nodeInfo = await this.getNodeInfo(path, { transaction }); + + // }) + // .then(nodeInfo => { + const pathInfo = PathInfo.get(path); if (nodeInfo.exists && nodeInfo.address && nodeInfo.address.path === path) { // Node exists and is stored in its own record. // Update it - return this._writeNodeWithTracking(path, updates, { merge: true, tid }); + await this._writeNodeWithTracking(path, updates, { transaction, merge: true }); } else if (nodeInfo.exists) { // Node exists, but is stored in its parent node. const pathInfo = PathInfo.get(path); - return lock.moveToParent() - .then(parentLock => { - lock = parentLock; - return this._writeNodeWithTracking(pathInfo.parentPath, { [pathInfo.key]: value }, { merge: true, tid }); - }); + // return lock.moveToParent() + // .then(parentLock => { + // lock = parentLock; + const lockPath = await transaction.moveToParentPath(pathInfo.parentPath); + console.assert(lockPath === pathInfo.parentPath, `transaction.moveToParentPath() did not move to the right parent path of "${path}"`) + await this._writeNodeWithTracking(pathInfo.parentPath, { [pathInfo.key]: value }, { transaction, merge: true }); + // }); } else { // The node does not exist, it's parent doesn't have it either. Update the parent instead - return lock.moveToParent() - .then(parentLock => { - lock = parentLock; - return this.updateNode(pathInfo.parentPath, { [pathInfo.key]: updates }, { tid }); - }); + // return lock.moveToParent() + // .then(parentLock => { + // lock = parentLock; + const lockPath = await transaction.moveToParentPath(pathInfo.parentPath); + console.assert(lockPath === pathInfo.parentPath, `transaction.moveToParentPath() did not move to the right parent path of "${path}"`) + await this.updateNode(pathInfo.parentPath, { [pathInfo.key]: updates }, { transaction }); + // }); + } + if (!options.transaction) { + // transaction was created by us, commit + await transaction.commit(); + } + } + catch (err) { + if (!options.transaction) { + // transaction was created by us, rollback + await transaction.rollback(err); } - }) - .then(result => { - lock.release(); - return result; - }) - .catch(err => { - lock.release(); throw err; - }); + } + + // }) + // .then(result => { + // lock.release(); + // return result; + // }) + // .catch(err => { + // lock.release(); + // throw err; + // }); } } @@ -6742,10 +7084,11 @@ module.exports = { CustomStorage, CustomStorageSettings, CustomStorageHelpers, + CustomStorageTransaction, ICustomStorageNodeMetaData, ICustomStorageNode } -},{"./node-info":34,"./node-value-types":36,"./storage":40,"acebase-core":11}],39:[function(require,module,exports){ +},{"./node-info":34,"./node-lock":35,"./node-value-types":36,"./storage":40,"acebase-core":11}],39:[function(require,module,exports){ const { debug, ID, PathReference, PathInfo, ascii85 } = require('acebase-core'); const { NodeInfo } = require('./node-info'); const { VALUE_TYPES } = require('./node-value-types'); @@ -8364,10 +8707,12 @@ class Storage extends EventEmitter { * @param {object} [options] * @returns {Promise} */ - _writeNodeWithTracking(path, value, options = { merge: false, tid: undefined, _customWriteFunction: undefined, waitForIndexUpdates: true }) { - if (!options || !options.tid) { throw new Error(`_writeNodeWithTracking MUST be executed with a tid!`); } + _writeNodeWithTracking(path, value, options = { merge: false, transaction: undefined, tid: undefined, _customWriteFunction: undefined, waitForIndexUpdates: true }) { + options = options || {}; + if (!options.tid && !options.transaction) { throw new Error(`_writeNodeWithTracking MUST be executed with a tid OR transaction!`); } options.merge = options.merge === true; const tid = options.tid; + const transaction = options.transaction; // Is anyone interested in the values changing on this path? let topEventData = null; @@ -8466,13 +8811,13 @@ class Storage extends EventEmitter { } } - return this.getNodeInfo(topEventPath, { tid }) + return this.getNodeInfo(topEventPath, { transaction, tid }) .then(eventNodeInfo => { if (!eventNodeInfo.exists) { // Node doesn't exist return null; } - let valueOptions = { tid }; + let valueOptions = { transaction, tid }; // if (!hasValueSubscribers && options.merge === true) { // // Only load current value for properties being updated // valueOptions.include = Object.keys(value); diff --git a/dist/browser.min.js b/dist/browser.min.js index 8222023..a7d7613 100644 --- a/dist/browser.min.js +++ b/dist/browser.min.js @@ -1 +1 @@ -(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.acebase=f()}})((function(){var define,module,exports;return function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,(function(r){var n=e[i][1][r];return o(n||r)}),p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i=7&&stringLength<=10)return true;return false};cuid.fingerprint=fingerprint;module.exports=cuid},{"./lib/fingerprint.js":2,"./lib/pad.js":3}],2:[function(require,module,exports){var pad=require("./pad.js");var env=typeof window==="object"?window:self;var globalCount=Object.keys(env).length;var mimeTypesLength=navigator.mimeTypes?navigator.mimeTypes.length:0;var clientId=pad((mimeTypesLength+navigator.userAgent.length).toString(36)+globalCount.toString(36),4);module.exports=function fingerprint(){return clientId}},{"./pad.js":3}],3:[function(require,module,exports){module.exports=function pad(num,size){var s="000000000"+num;return s.substr(s.length-size)}},{}],4:[function(require,module,exports){const{EventEmitter:EventEmitter}=require("events");const{DataReference:DataReference,DataReferenceQuery:DataReferenceQuery}=require("./data-reference");const{TypeMappings:TypeMappings}=require("./type-mappings");class AceBaseSettings{constructor(options){this.logLevel=options.logLevel||"log"}}class AceBaseBase extends EventEmitter{constructor(dbname,options){super();if(!options){options={}}this.once("ready",()=>{this._ready=true});this.types=new TypeMappings(this)}ready(callback=undefined){if(this._ready===true){callback&&callback();return Promise.resolve()}else{let resolve;const promise=new Promise(res=>resolve=res);this.on("ready",()=>{resolve();callback&&callback()});return promise}}get isReady(){return this._ready===true}ref(path){return new DataReference(this,path)}get root(){return this.ref("")}query(path){const ref=new DataReference(this,path);return new DataReferenceQuery(ref)}get indexes(){return{get:()=>{return this.api.getIndexes()},create:(path,key,options)=>{return this.api.createIndex(path,key,options)}}}}module.exports={AceBaseBase:AceBaseBase,AceBaseSettings:AceBaseSettings}},{"./data-reference":7,"./type-mappings":16,events:42}],5:[function(require,module,exports){class Api{stats(options=undefined){}subscribe(path,event,callback){}unsubscribe(path,event,callback){}update(path,updates){}set(path,value){}get(path,options){}exists(path){}query(path,query,options){}createIndex(path,key){}getIndexes(){}}module.exports={Api:Api}},{}],6:[function(require,module,exports){const c=function(input,length,result){var i,j,n,b=[0,0,0,0,0];for(i=0;i";return ret},decode:function(input){if(!input.startsWith("<~")||!input.endsWith("~>")){throw new Error("Invalid input string")}input=input.substr(2,input.length-4);var n=input.length,r=[],b=[0,0,0,0,0],i,j,t,x,y,d;for(i=0;i>>=8;y=t&255;t>>>=8;r.push(t>>>8,t&255,y,x);for(j=d;j<5;++j,r.pop());i+=4}const data=new Uint8Array(r);return data.buffer.slice(data.byteOffset,data.byteOffset+data.byteLength)}};module.exports=ascii85},{}],7:[function(require,module,exports){const{DataSnapshot:DataSnapshot}=require("./data-snapshot");const{EventStream:EventStream,EventPublisher:EventPublisher}=require("./subscription");const{ID:ID}=require("./id");const debug=require("./debug");const{PathInfo:PathInfo}=require("./path-info");class DataRetrievalOptions{constructor(options){if(!options){options={}}if(typeof options.include!=="undefined"&&!(options.include instanceof Array)){throw new TypeError(`options.include must be an array`)}if(typeof options.exclude!=="undefined"&&!(options.exclude instanceof Array)){throw new TypeError(`options.exclude must be an array`)}if(typeof options.child_objects!=="undefined"&&typeof options.child_objects!=="boolean"){throw new TypeError(`options.child_objects must be a boolean`)}if(typeof options.allow_cache!=="undefined"&&typeof options.allow_cache!=="boolean"){throw new TypeError(`options.allow_cache must be a boolean`)}this.include=options.include||undefined;this.exclude=options.exclude||undefined;this.child_objects=typeof options.child_objects==="boolean"?options.child_objects:undefined;this.allow_cache=typeof options.allow_cache==="boolean"?options.allow_cache:undefined}}class QueryDataRetrievalOptions extends DataRetrievalOptions{constructor(options){super(options);if(typeof options.snapshots!=="undefined"&&typeof options.snapshots!=="boolean"){throw new TypeError(`options.snapshots must be an array`)}this.snapshots=typeof options.snapshots==="boolean"?options.snapshots:undefined}}const _private=Symbol("private");class DataReference{constructor(db,path,vars){if(!path){path=""}path=path.replace(/^\/|\/$/g,"");const pathInfo=PathInfo.get(path);const key=pathInfo.key;const callbacks=[];this[_private]={get path(){return path},get key(){return key},get callbacks(){return callbacks},vars:vars||{}};this.db=db}get path(){return this[_private].path}get key(){return this[_private].key}get parent(){let currentPath=PathInfo.fillVariables2(this.path,this.vars);const info=PathInfo.get(currentPath);if(info.parentPath===null){return null}return new DataReference(this.db,info.parentPath)}get vars(){return this[_private].vars}child(childPath){childPath=childPath.replace(/^\/|\/$/g,"");const currentPath=PathInfo.fillVariables2(this.path,this.vars);const targetPath=PathInfo.getChildPath(currentPath,childPath);return new DataReference(this.db,targetPath)}set(value,onComplete=undefined){if(this.isWildcardPath){throw new Error(`Cannot set the value of a path with wildcards and/or variables`)}if(this.parent===null){throw new Error(`Cannot set the root object. Use update, or set individual child properties`)}if(typeof value==="undefined"){throw new TypeError(`Cannot store value undefined`)}value=this.db.types.serialize(this.path,value);return this.db.api.set(this.path,value).then(res=>{if(typeof onComplete==="function"){try{onComplete(null,this)}catch(err){console.error(`Error in onComplete callback:`,err)}}}).catch(err=>{if(typeof onComplete==="function"){try{onComplete(err)}catch(err){console.error(`Error in onComplete callback:`,err)}}else{throw err}}).then(()=>{return this})}update(updates,onComplete=undefined){if(this.isWildcardPath){throw new Error(`Cannot update the value of a path with wildcards and/or variables`)}let promise;if(typeof updates!=="object"||updates instanceof Array||updates instanceof ArrayBuffer||updates instanceof Date){promise=this.set(updates)}else{updates=this.db.types.serialize(this.path,updates);promise=this.db.api.update(this.path,updates)}return promise.then(()=>{if(typeof onComplete==="function"){try{onComplete(null,this)}catch(err){console.error(`Error in onComplete callback:`,err)}}}).catch(err=>{if(typeof onComplete==="function"){try{onComplete(err)}catch(err){console.error(`Error in onComplete callback:`,err)}}else{throw err}}).then(()=>{return this})}transaction(callback){if(this.isWildcardPath){throw new Error(`Cannot start a transaction on a path with wildcards and/or variables`)}let cb=currentValue=>{currentValue=this.db.types.deserialize(this.path,currentValue);const snap=new DataSnapshot(this,currentValue);let newValue;try{newValue=callback(snap)}catch(err){return}if(newValue instanceof Promise){return newValue.then(val=>{return this.db.types.serialize(this.path,val)})}else{return this.db.types.serialize(this.path,newValue)}};return this.db.api.transaction(this.path,cb).then(result=>{return this})}on(event,callback,cancelCallbackOrContext,context){if(this.path===""&&["value","notify_value","child_changed","notify_child_changed"].includes(event)){console.warn(`WARNING: Listening for value and child_changed events on the root node is a bad practice`)}const cancelCallback=typeof cancelCallbackOrContext==="function"&&cancelCallbackOrContext;context=typeof cancelCallbackOrContext==="object"?cancelCallbackOrContext:context;const useCallback=typeof callback==="function";let eventPublisher=null;const eventStream=new EventStream(publisher=>{eventPublisher=publisher});let cb={subscr:eventStream,original:callback,ours:(err,path,newValue,oldValue)=>{if(err){debug.error(`Error getting data for event ${event} on path "${path}"`,err);return}let ref=this.db.ref(path);ref[_private].vars=PathInfo.extractVariables(this.path,path);let callbackObject;if(event.startsWith("notify_")){callbackObject=ref}else{const isRemoved=event==="child_removed";const val=this.db.types.deserialize(path,isRemoved?oldValue:newValue);const snap=new DataSnapshot(ref,val,isRemoved);callbackObject=snap}useCallback&&callback.call(context||null,callbackObject);let keep=eventPublisher.publish(callbackObject);if(!keep&&!useCallback){let callbacks=this[_private].callbacks;callbacks.splice(callbacks.indexOf(cb),1);this.db.api.unsubscribe(this.path,event,cb.ours)}}};this[_private].callbacks.push(cb);let authorized=this.db.api.subscribe(this.path,event,cb.ours);const allSubscriptionsStoppedCallback=()=>{let callbacks=this[_private].callbacks;callbacks.splice(callbacks.indexOf(cb),1);this.db.api.unsubscribe(this.path,event,cb.ours)};if(authorized instanceof Promise){authorized.then(()=>{eventPublisher.start(allSubscriptionsStoppedCallback)}).catch(err=>{let callbacks=this[_private].callbacks;callbacks.splice(callbacks.indexOf(cb),1);this.db.api.unsubscribe(this.path,event,cb.ours);eventPublisher.cancel(err.message);cancelCallback&&cancelCallback(err.message)})}else{eventPublisher.start(allSubscriptionsStoppedCallback)}if(callback&&!this.isWildcardPath){if(event==="value"){this.get(snap=>{eventPublisher.publish(snap);useCallback&&callback(snap)})}else if(event==="child_added"){this.get(snap=>{const val=snap.val();if(val===null||typeof val!=="object"){return}Object.keys(val).forEach(key=>{let childSnap=new DataSnapshot(this.child(key),val[key]);eventPublisher.publish(childSnap);useCallback&&callback(childSnap)})})}else if(event==="notify_child_added"){const step=100;let limit=step,skip=0;const more=()=>{this.db.api.reflect(this.path,"children",{limit:limit,skip:skip}).then(children=>{children.list.forEach(child=>{const childRef=this.child(child.key);eventPublisher.publish(childRef);useCallback&&callback(childRef)});if(children.more){skip+=step;more()}})};more()}}return eventStream}off(event=undefined,callback=undefined){const callbacks=this[_private].callbacks;if(callback){const cb=callbacks.find(cb=>cb.original===callback);if(!cb){debug.error(`Can't find specified callback to unsubscribe from (path: "${this.path}", event: ${event}, callback: ${callback})`);return}callbacks.splice(callbacks.indexOf(cb),1);callback=cb.ours;cb.subscr.unsubscribe(callback)}else{callbacks.splice(0,callbacks.length).forEach(cb=>{cb.subscr.unsubscribe()})}this.db.api.unsubscribe(this.path,event,callback);return this}get(optionsOrCallback=undefined,callback=undefined){if(this.isWildcardPath){throw new Error(`Cannot get the value of a path with wildcards and/or variables. Use .query() instead`)}callback=typeof optionsOrCallback==="function"?optionsOrCallback:typeof callback==="function"?callback:undefined;const options=typeof optionsOrCallback==="object"?optionsOrCallback:new DataRetrievalOptions({allow_cache:true});if(typeof options.allow_cache==="undefined"){options.allow_cache=true}const promise=this.db.api.get(this.path,options).then(value=>{value=this.db.types.deserialize(this.path,value);const snapshot=new DataSnapshot(this,value);return snapshot});if(callback){promise.then(callback);return}else{return promise}}once(event,options){if(event==="value"&&!this.isWildcardPath){return this.get(options)}return new Promise((resolve,reject)=>{const callback=snap=>{this.off(event,snap);resolve(snap)};this.on(event,callback)})}push(value=undefined,onComplete=undefined){if(this.isWildcardPath){throw new Error(`Cannot push to a path with wildcards and/or variables`)}const id=ID.generate();const ref=this.child(id);ref.__pushed=true;if(typeof value!=="undefined"){return ref.set(value,onComplete).then(res=>ref)}else{return ref}}remove(){if(this.isWildcardPath){throw new Error(`Cannot remove a path with wildcards and/or variables. Use query().remove instead`)}if(this.parent===null){throw new Error(`Cannot remove the top node`)}return this.set(null)}exists(){if(this.isWildcardPath){throw new Error(`Cannot push to a path with wildcards and/or variables`)}return this.db.api.exists(this.path)}get isWildcardPath(){return this.path.indexOf("*")>=0||this.path.indexOf("$")>=0}query(){return new DataReferenceQuery(this)}reflect(type,args){if(this.pathHasVariables){throw new Error(`Cannot reflect on a path with wildcards and/or variables`)}return this.db.api.reflect(this.path,type,args)}export(stream,options={format:"json"}){return this.db.api.export(this.path,stream,options)}}class DataReferenceQuery{constructor(ref){this.ref=ref;this[_private]={filters:[],skip:0,take:0,order:[]}}filter(key,op,compare){if((op==="in"||op==="!in")&&(!(compare instanceof Array)||compare.length===0)){throw new Error(`${op} filter for ${key} must supply an Array compare argument containing at least 1 value`)}if((op==="between"||op==="!between")&&(!(compare instanceof Array)||compare.length!==2)){throw new Error(`${op} filter for ${key} must supply an Array compare argument containing 2 values`)}if((op==="matches"||op==="!matches")&&!(compare instanceof RegExp)){throw new Error(`${op} filter for ${key} must supply a RegExp compare argument`)}if((op==="contains"||op==="!contains")&&(typeof compare==="object"&&!(compare instanceof Array)&&!(compare instanceof Date)||compare instanceof Array&&compare.length===0)){throw new Error(`${op} filter for ${key} must supply a simple value or (non-zero length) array compare argument`)}this[_private].filters.push({key:key,op:op,compare:compare});return this}where(key,op,compare){return this.filter(key,op,compare)}take(n){this[_private].take=n;return this}skip(n){this[_private].skip=n;return this}sort(key,ascending=true){if(typeof key!=="string"){throw`key must be a string`}this[_private].order.push({key:key,ascending:ascending});return this}order(key,ascending=true){return this.sort(key,ascending)}get(optionsOrCallback=undefined,callback=undefined){callback=typeof optionsOrCallback==="function"?optionsOrCallback:typeof callback==="function"?callback:undefined;const options=typeof optionsOrCallback==="object"?optionsOrCallback:new QueryDataRetrievalOptions({snapshots:true,allow_cache:true});if(typeof options.snapshots==="undefined"){options.snapshots=true}if(typeof options.allow_cache==="undefined"){options.allow_cache=true}options.eventHandler=ev=>{if(!this._events||!this._events[ev.name]){return false}const listeners=this._events[ev.name];if(typeof listeners!=="object"||listeners.length===0){return false}if(["add","change","remove"].includes(ev.name)){const ref=new DataReference(this.ref.db,ev.path);const eventData={name:ev.name};if(options.snapshots&&ev.name!=="remove"){const val=db.types.deserialize(ev.path,ev.value);eventData.snapshot=new DataSnapshot(ref,val,false)}else{eventData.ref=ref}ev=eventData}listeners.forEach(callback=>{try{callback(ev)}catch(e){}})};options.monitor={add:false,change:false,remove:false};if(this._events){if(this._events["add"]&&this._events["add"].length>0){options.monitor.add=true}if(this._events["change"]&&this._events["change"].length>0){options.monitor.change=true}if(this._events["remove"]&&this._events["remove"].length>0){options.monitor.remove=true}}const db=this.ref.db;return db.api.query(this.ref.path,this[_private],options).catch(err=>{throw new Error(err)}).then(results=>{results.forEach((result,index)=>{if(options.snapshots){const val=db.types.deserialize(result.path,result.val);results[index]=new DataSnapshot(db.ref(result.path),val)}else{results[index]=db.ref(result)}});if(options.snapshots){return DataSnapshotsArray.from(results)}else{return DataReferencesArray.from(results)}}).then(results=>{callback&&callback(results);return results})}getRefs(callback=undefined){return this.get({snapshots:false},callback)}remove(callback){return this.get({snapshots:false}).then(refs=>{const promises=[];return Promise.all(refs.map(ref=>ref.remove())).then(()=>{callback&&callback()})})}on(event,callback){if(!this._events){this._events={}}if(!this._events[event]){this._events[event]=[]}this._events[event].push(callback);return this}off(event,callback){if(!this._events){return this}if(typeof event==="undefined"){this._events={};return this}if(!this._events[event]){return this}if(typeof callback==="undefined"){delete this._events[event];return this}const index=!this._events[event].indexOf(callback);if(!~index){return this}this._events[event].splice(index,1);return this}}class DataSnapshotsArray extends Array{static from(snaps){const arr=new DataSnapshotsArray(snaps.length);snaps.forEach((snap,i)=>arr[i]=snap);return arr}getValues(){return this.map(snap=>snap.val())}}class DataReferencesArray extends Array{static from(refs){const arr=new DataReferencesArray(refs.length);refs.forEach((ref,i)=>arr[i]=ref);return arr}getPaths(){return this.map(ref=>ref.path)}}module.exports={DataReference:DataReference,DataReferenceQuery:DataReferenceQuery,DataRetrievalOptions:DataRetrievalOptions,QueryDataRetrievalOptions:QueryDataRetrievalOptions}},{"./data-snapshot":8,"./debug":9,"./id":10,"./path-info":12,"./subscription":14}],8:[function(require,module,exports){const{DataReference:DataReference}=require("./data-reference");const{getPathKeys:getPathKeys}=require("./path-info");const getChild=(snapshot,path)=>{if(!snapshot.exists()){return null}let child=snapshot.val();getPathKeys(path).every(key=>{child=child[key];return typeof child!=="undefined"});return child||null};const getChildren=snapshot=>{if(!snapshot.exists()){return[]}let value=snapshot.val();if(value instanceof Array){return new Array(value.length).map((v,i)=>i)}if(typeof value==="object"){return Object.keys(value)}return[]};class DataSnapshot{constructor(ref,value,isRemoved=false){this.ref=ref;this.val=()=>{return value};this.exists=()=>{if(isRemoved){return false}return value!==null&&typeof value!=="undefined"}}child(path){let child=getChild(this,path);return new DataSnapshot(this.ref.child(path),child)}hasChild(path){return getChild(this,path)!==null}hasChildren(){return getChildren(this).length>0}numChildren(){return getChildren(this).length}forEach(callback){const value=this.val();return getChildren(this).every((key,i)=>{const snap=new DataSnapshot(this.ref.child(key),value[key]);return callback(snap)})}get key(){return this.ref.key}}module.exports={DataSnapshot:DataSnapshot}},{"./data-reference":7,"./path-info":12}],9:[function(require,module,exports){class DebugLogger{constructor(level="log",prefix=""){this.prefix=prefix;this.setLevel(level)}setLevel(level){const prefix=this.prefix?this.prefix:"";this.level=level;this.verbose=["verbose"].includes(level)?console.log.bind(console,prefix):()=>{};this.log=["verbose","log"].includes(level)?console.log.bind(console,prefix):()=>{};this.warn=["verbose","log","warn"].includes(level)?console.warn.bind(console,prefix):()=>{};this.error=["verbose","log","warn","error"].includes(level)?console.error.bind(console,prefix):()=>{};this.write=console.log.bind(console)}}module.exports=DebugLogger},{}],10:[function(require,module,exports){const cuid=require("cuid");class ID{static generate(){return cuid().slice(1)}}module.exports={ID:ID}},{cuid:1}],11:[function(require,module,exports){const{AceBaseBase:AceBaseBase,AceBaseSettings:AceBaseSettings}=require("./acebase-base");const{Api:Api}=require("./api");const{DataReference:DataReference,DataReferenceQuery:DataReferenceQuery,DataRetrievalOptions:DataRetrievalOptions,QueryDataRetrievalOptions:QueryDataRetrievalOptions}=require("./data-reference");const{DataSnapshot:DataSnapshot}=require("./data-snapshot");const DebugLogger=require("./debug");const{ID:ID}=require("./id");const{PathReference:PathReference}=require("./path-reference");const{EventStream:EventStream,EventPublisher:EventPublisher,EventSubscription:EventSubscription}=require("./subscription");const Transport=require("./transport");const{TypeMappings:TypeMappings,TypeMappingOptions:TypeMappingOptions}=require("./type-mappings");const Utils=require("./utils");const{PathInfo:PathInfo}=require("./path-info");const ascii85=require("./ascii85");module.exports={AceBaseBase:AceBaseBase,AceBaseSettings:AceBaseSettings,Api:Api,DataReference:DataReference,DataReferenceQuery:DataReferenceQuery,DataRetrievalOptions:DataRetrievalOptions,QueryDataRetrievalOptions:QueryDataRetrievalOptions,DataSnapshot:DataSnapshot,DebugLogger:DebugLogger,ID:ID,PathReference:PathReference,EventStream:EventStream,EventPublisher:EventPublisher,EventSubscription:EventSubscription,Transport:Transport,TypeMappings:TypeMappings,TypeMappingOptions:TypeMappingOptions,Utils:Utils,PathInfo:PathInfo,ascii85:ascii85}},{"./acebase-base":4,"./api":5,"./ascii85":6,"./data-reference":7,"./data-snapshot":8,"./debug":9,"./id":10,"./path-info":12,"./path-reference":13,"./subscription":14,"./transport":15,"./type-mappings":16,"./utils":17}],12:[function(require,module,exports){function getPathKeys(path){if(path.length===0){return[]}let keys=path.replace(/\[/g,"/[").split("/");keys.forEach((key,index)=>{if(key.startsWith("[")){keys[index]=parseInt(key.substr(1,key.length-2))}});return keys}function getPathInfo(path){if(path.length===0){return{parent:null,key:""}}const i=Math.max(path.lastIndexOf("/"),path.lastIndexOf("["));const parentPath=i<0?"":path.substr(0,i);let key=i<0?path:path.substr(i);if(key.startsWith("[")){key=parseInt(key.substr(1,key.length-2))}else if(key.startsWith("/")){key=key.substr(1)}if(parentPath===path){parentPath=null}return{parent:parentPath,key:key}}function getChildPath(path,key){if(path.length===0){if(typeof key==="number"){throw new TypeError("Cannot add array index to root path!")}return key}if(typeof key==="number"){return`${path}[${key}]`}return`${path}/${key}`}class PathInfo{static get(path){return new PathInfo(path)}static getChildPath(path,childKey){return getChildPath(path,childKey)}static getPathKeys(path){return getPathKeys(path)}constructor(path){this.path=path}get key(){return getPathInfo(this.path).key}get parentPath(){return getPathInfo(this.path).parent}childPath(childKey){return getChildPath(`${this.path}`,childKey)}get pathKeys(){return getPathKeys(this.path)}static extractVariables(varPath,fullPath){if(!varPath.includes("*")&&!varPath.includes("$")){return[]}const keys=getPathKeys(varPath);const pathKeys=getPathKeys(fullPath);let count=0;const variables={get length(){return count}};keys.forEach((key,index)=>{const pathKey=pathKeys[index];if(key==="*"){variables[count++]=pathKey}else if(typeof key==="string"&&key[0]==="$"){variables[count++]=pathKey;variables[key]=pathKey;const varName=key.slice(1);if(typeof variables[varName]==="undefined"){variables[varName]=pathKey}}});return variables}static fillVariables(varPath,fullPath){if(varPath.indexOf("*")<0&&varPath.indexOf("$")<0){return varPath}const keys=getPathKeys(varPath);const pathKeys=getPathKeys(fullPath);let merged=keys.map((key,index)=>{if(key===pathKeys[index]||index>=pathKeys.length){return key}else if(typeof key==="string"&&(key==="*"||key[0]==="$")){return pathKeys[index]}else{throw new Error(`Path "${fullPath}" cannot be used to fill variables of path "${this.path}" because they do not match`)}});let mergedPath="";merged.forEach(key=>{if(typeof key==="number"){mergedPath+=`[${key}]`}else{if(mergedPath.length>0){mergedPath+="/"}mergedPath+=key}});return mergedPath}static fillVariables2(varPath,vars){if(typeof vars!=="object"||Object.keys(vars).length===0){return varPath}let pathKeys=getPathKeys(varPath);let n=0;const targetPath=pathKeys.reduce((path,key)=>{if(key==="*"||key.startsWith("$")){key=vars[n++]}if(typeof key==="number"){return`${path}[${key}]`}else{return`${path}/${key}`}},"");return targetPath}equals(otherPath){if(this.path===otherPath){return true}const keys=getPathKeys(this.path);const otherKeys=getPathKeys(otherPath);if(keys.length!==otherKeys.length){return false}return keys.every((key,index)=>{const otherKey=otherKeys[index];return otherKey===key||typeof otherKey==="string"&&(otherKey==="*"||otherKey[0]==="$")||typeof key==="string"&&(key==="*"||key[0]==="$")})}isAncestorOf(descendantPath){if(descendantPath===""||this.path===descendantPath){return false}if(this.path===""){return true}const ancestorKeys=getPathKeys(this.path);const descendantKeys=getPathKeys(descendantPath);if(ancestorKeys.length>=descendantKeys.length){return false}return ancestorKeys.every((key,index)=>{const otherKey=descendantKeys[index];return otherKey===key||typeof otherKey==="string"&&(otherKey==="*"||otherKey[0]==="$")||typeof key==="string"&&(key==="*"||key[0]==="$")})}isDescendantOf(ancestorPath){if(this.path===""||this.path===ancestorPath){return false}if(ancestorPath===""){return true}const ancestorKeys=getPathKeys(ancestorPath);const descendantKeys=getPathKeys(this.path);if(ancestorKeys.length>=descendantKeys.length){return false}return ancestorKeys.every((key,index)=>{const otherKey=descendantKeys[index];return otherKey===key||typeof otherKey==="string"&&(otherKey==="*"||otherKey[0]==="$")||typeof key==="string"&&(key==="*"||key[0]==="$")})}isChildOf(otherPath){if(this.path===""){return false}const parentInfo=PathInfo.get(this.parentPath);return parentInfo.equals(otherPath)}isParentOf(otherPath){if(otherPath===""){return false}const parentInfo=PathInfo.get(PathInfo.get(otherPath).parentPath);return parentInfo.equals(this.path)}}module.exports={getPathInfo:getPathInfo,getChildPath:getChildPath,getPathKeys:getPathKeys,PathInfo:PathInfo}},{}],13:[function(require,module,exports){class PathReference{constructor(path){this.path=path}}module.exports={PathReference:PathReference}},{}],14:[function(require,module,exports){class EventSubscription{constructor(stop){this.stop=stop;this._internal={state:"init",cancelReason:undefined,activatePromises:[]}}activated(callback=undefined){if(callback){this._internal.activatePromises.push({callback:callback});if(this._internal.state==="active"){callback(true)}else if(this._internal.state==="canceled"){callback(false,this._internal.cancelReason)}}return new Promise((resolve,reject)=>{if(this._internal.state==="active"){return resolve()}else if(this._internal.state==="canceled"&&!callback){return reject(new Error(this._internal.cancelReason))}this._internal.activatePromises.push({resolve:resolve,reject:callback?()=>{}:reject})})}_setActivationState(activated,cancelReason){this._internal.cancelReason=cancelReason;this._internal.state=activated?"active":"canceled";while(this._internal.activatePromises.length>0){const p=this._internal.activatePromises.shift();if(activated){p.callback&&p.callback(true);p.resolve&&p.resolve()}else{p.callback&&p.callback(false,cancelReason);p.reject&&p.reject(cancelReason)}}}}class EventPublisher{constructor(publish,start,cancel){this.publish=publish;this.start=start;this.cancel=cancel}}class EventStream{constructor(eventPublisherCallback){const subscribers=[];let noMoreSubscribersCallback;let activationState;const _stoppedState="stopped (no more subscribers)";this.subscribe=(callback,activationCallback)=>{if(typeof callback!=="function"){throw new TypeError("callback must be a function")}else if(activationState===_stoppedState){throw new Error("stream can't be used anymore because all subscribers were stopped")}const sub={callback:callback,activationCallback:function(activated,cancelReason){activationCallback&&activationCallback(activated,cancelReason);this.subscription._setActivationState(activated,cancelReason)},subscription:new EventSubscription((function stop(){subscribers.splice(subscribers.indexOf(this),1);checkActiveSubscribers()}))};subscribers.push(sub);if(typeof activationState!=="undefined"){if(activationState===true){activationCallback&&activationCallback(true);sub.subscription._setActivationState(true)}else if(typeof activationState==="string"){activationCallback&&activationCallback(false,activationState);sub.subscription._setActivationState(false,activationState)}}return sub.subscription};const checkActiveSubscribers=()=>{if(subscribers.length===0){noMoreSubscribersCallback&&noMoreSubscribersCallback();activationState=_stoppedState}};this.unsubscribe=(callback=undefined)=>{const remove=callback?subscribers.filter(sub=>sub.callback===callback):subscribers;remove.forEach(sub=>{const i=subscribers.indexOf(sub);subscribers.splice(i,1)});checkActiveSubscribers()};this.stop=()=>{subscribers.splice(0);checkActiveSubscribers()};const publish=val=>{subscribers.forEach(sub=>{try{sub.callback(val)}catch(err){debug.error(`Error running subscriber callback: ${err.message}`)}});return subscribers.length>0};const start=allSubscriptionsStoppedCallback=>{activationState=true;noMoreSubscribersCallback=allSubscriptionsStoppedCallback;subscribers.forEach(sub=>{sub.activationCallback&&sub.activationCallback(true)})};const cancel=reason=>{activationState=reason;subscribers.forEach(sub=>{sub.activationCallback&&sub.activationCallback(false,reason||new Error("unknown reason"))});subscribers.splice()};const publisher=new EventPublisher(publish,start,cancel);eventPublisherCallback(publisher)}}module.exports={EventStream:EventStream,EventPublisher:EventPublisher,EventSubscription:EventSubscription}},{}],15:[function(require,module,exports){const{PathReference:PathReference}=require("./path-reference");const{cloneObject:cloneObject}=require("./utils");const ascii85=require("./ascii85");module.exports={deserialize(data){if(data.map===null||typeof data.map==="undefined"){return data.val}const deserializeValue=(type,val)=>{if(type==="date"){return new Date(val)}else if(type==="binary"){return ascii85.decode(val)}else if(type==="reference"){return new PathReference(val)}else if(type==="regexp"){return new RegExp(val.pattern,val.flags)}return val};if(typeof data.map==="string"){return deserializeValue(data.map,data.val)}Object.keys(data.map).forEach(path=>{const type=data.map[path];const keys=path.replace(/\[/g,"/[").split("/");keys.forEach((key,index)=>{if(key.startsWith("[")){keys[index]=parseInt(key.substr(1,key.length-2))}});let parent=data;let key="val";let val=data.val;keys.forEach(k=>{key=k;parent=val;val=val[key]});parent[key]=deserializeValue(type,val)});return data.val},serialize(obj){if(obj===null||typeof obj!=="object"||obj instanceof Date||obj instanceof ArrayBuffer||obj instanceof PathReference){const ser=this.serialize({value:obj});return{map:ser.map.value,val:ser.val.value}}obj=cloneObject(obj);const process=(obj,mappings,prefix)=>{Object.keys(obj).forEach(key=>{const val=obj[key];const path=prefix.length===0?key:`${prefix}/${key}`;if(val instanceof Date){obj[key]=val.toISOString();mappings[path]="date"}else if(val instanceof ArrayBuffer){obj[key]=ascii85.encode(val);mappings[path]="binary"}else if(val instanceof PathReference){obj[key]=val.path;mappings[path]="reference"}else if(val instanceof RegExp){obj[key]={pattern:val.source,flags:val.flags};mappings[path]="regexp"}else if(typeof val==="object"&&val!==null){process(val,mappings,path)}})};const mappings={};process(obj,mappings,"");return{map:mappings,val:obj}}}},{"./ascii85":6,"./path-reference":13,"./utils":17}],16:[function(require,module,exports){const{cloneObject:cloneObject}=require("./utils");const{PathInfo:PathInfo}=require("./path-info");const{AceBaseBase:AceBaseBase}=require("./acebase-base");const{DataReference:DataReference}=require("./data-reference");const{DataSnapshot:DataSnapshot}=require("./data-snapshot");const get=(mappings,path)=>{path=path.replace(/^\/|\/$/g,"");const keys=PathInfo.getPathKeys(path);const mappedPath=Object.keys(mappings).find(mpath=>{const mkeys=PathInfo.getPathKeys(mpath);if(mkeys.length!==keys.length){return false}return mkeys.every((mkey,index)=>{if(mkey==="*"||mkey[0]==="$"){return true}return mkey===keys[index]})});const mapping=mappings[mappedPath];return mapping};const map=(mappings,path)=>{const targetPath=PathInfo.get(path).parentPath;if(targetPath===null){return}return get(mappings,targetPath)};const mapDeep=(mappings,entryPath)=>{entryPath=entryPath.replace(/^\/|\/$/g,"");const pathInfo=PathInfo.get(entryPath);const startPath=pathInfo.parentPath;const keys=startPath?PathInfo.getPathKeys(startPath):[];const matches=Object.keys(mappings).reduce((m,mpath)=>{const mkeys=PathInfo.getPathKeys(mpath);if(mkeys.length{if(index>=keys.length){return false}else if(mkey==="*"||mkey[0]==="$"||mkey===keys[index]){return true}else{isMatch=false;return false}})}if(isMatch){const mapping=mappings[mpath];m.push({path:mpath,type:mapping})}return m},[]);return matches};const process=(db,mappings,path,obj,action)=>{if(obj===null||typeof obj!=="object"){return obj}const keys=PathInfo.getPathKeys(path);const m=mapDeep(mappings,path);const changes=[];m.sort((a,b)=>PathInfo.getPathKeys(a.path).length>PathInfo.getPathKeys(b.path).length?-1:1);m.forEach(mapping=>{const mkeys=PathInfo.getPathKeys(mapping.path);mkeys.push("*");const mTrailKeys=mkeys.slice(keys.length);if(mTrailKeys.length===0){const vars=PathInfo.extractVariables(mapping.path,path);const ref=new DataReference(db,path,vars);if(action==="serialize"){obj=mapping.type.serialize(obj,ref)}else if(action==="deserialize"){const snap=new DataSnapshot(ref,obj);obj=mapping.type.deserialize(snap)}return}const process=(parentPath,parent,keys)=>{if(obj===null||typeof obj!=="object"){return obj}const key=keys[0];let children=[];if(key==="*"||key[0]==="$"){if(parent instanceof Array){children=parent.map((val,index)=>({key:index,val:val}))}else{children=Object.keys(parent).map(k=>({key:k,val:parent[k]}))}}else{const child=parent[key];if(typeof child==="object"){children.push({key:key,val:child})}}children.forEach(child=>{const childPath=PathInfo.getChildPath(parentPath,child.key);const vars=PathInfo.extractVariables(mapping.path,childPath);const ref=new DataReference(db,childPath,vars);if(keys.length===1){if(action==="serialize"){changes.push({parent:parent,key:child.key,original:parent[child.key]});parent[child.key]=mapping.type.serialize(child.val,ref)}else if(action==="deserialize"){const snap=new DataSnapshot(ref,child.val);parent[child.key]=mapping.type.deserialize(snap)}}else{process(childPath,child.val,keys.slice(1))}})};process(path,obj,mTrailKeys)});if(action==="serialize"){obj=cloneObject(obj);if(changes.length>0){changes.forEach(change=>{change.parent[change.key]=change.original})}}return obj};class TypeMappingOptions{constructor(options){if(!options){options={}}this.serializer=options.serializer;this.creator=options.creator}}const _mappings=Symbol("mappings");class TypeMappings{constructor(db){this.db=db;this[_mappings]={}}get mappings(){return this[_mappings]}map(path){return map(this[_mappings],path)}bind(path,type,options={}){if(typeof path!=="string"){throw new TypeError("path must be a string")}if(typeof type!=="function"){throw new TypeError("constructor must be a function")}if(typeof options.serializer==="undefined"){}else if(typeof options.serializer==="string"){if(typeof type.prototype[options.serializer]==="function"){options.serializer=type.prototype[options.serializer]}else{throw new TypeError(`${type.name}.prototype.${options.serializer} is not a function, cannot use it as serializer`)}}else if(typeof options.serializer!=="function"){throw new TypeError(`serializer for class ${type.name} must be a function, or the name of a prototype method`)}if(typeof options.creator==="undefined"){if(typeof type.create==="function"){options.creator=type.create}}else if(typeof options.creator==="string"){if(typeof type[options.creator]==="function"){options.creator=type[options.creator]}else{throw new TypeError(`${type.name}.${options.creator} is not a function, cannot use it as creator`)}}else if(typeof options.creator!=="function"){throw new TypeError(`creator for class ${type.name} must be a function, or the name of a static method`)}path=path.replace(/^\/|\/$/g,"");this[_mappings][path]={db:this.db,type:type,creator:options.creator,serializer:options.serializer,deserialize(snap){let obj;if(this.creator){obj=this.creator.call(this.type,snap)}else{obj=new this.type(snap)}return obj},serialize(obj,ref){if(this.serializer){obj=this.serializer.call(obj,ref,obj)}else if(obj&&typeof obj.serialize==="function"){obj=obj.serialize(ref,obj)}return obj}}}serialize(path,obj){return process(this.db,this[_mappings],path,obj,"serialize")}deserialize(path,obj){return process(this.db,this[_mappings],path,obj,"deserialize")}}module.exports={TypeMappings:TypeMappings,TypeMappingOptions:TypeMappingOptions}},{"./acebase-base":4,"./data-reference":7,"./data-snapshot":8,"./path-info":12,"./utils":17}],17:[function(require,module,exports){(function(Buffer){const{PathReference:PathReference}=require("./path-reference");function numberToBytes(number){const bytes=new Uint8Array(8);const view=new DataView(bytes.buffer);view.setFloat64(0,number);return new Array(...bytes)}function bytesToNumber(bytes){if(bytes.length<8){throw new TypeError("must be 8 bytes")}const bin=new Uint8Array(bytes);const view=new DataView(bin.buffer);const nr=view.getFloat64(0);return nr}function encodeString(str){if(typeof TextEncoder!=="undefined"){const encoder=new TextEncoder;return encoder.encode(str)}else if(typeof Buffer==="function"){const buf=Buffer.from(str,"utf-8");return new Uint8Array(buf.buffer,buf.byteOffset,buf.byteLength)}else{let arr=[];for(let i=0;i128){if((code&55296)===55296){const nextCode=str.charCodeAt(i+1);if((nextCode&56320)!==56320){throw new Error("follow-up utf-16 character does not start with 0xDC00")}i++;const p1=code&1023;const p2=nextCode&1023;code=65536|p1<<10|p2}if(code<2048){const b1=192|code>>6&31;const b2=128|code&63;arr.push(b1,b2)}else if(code<65536){const b1=224|code>>12&15;const b2=128|code>>6&63;const b3=128|code&63;arr.push(b1,b2,b3)}else if(code<2097152){const b1=240|code>>18&7;const b2=128|code>>12&63;const b3=128|code>>6&63;const b4=128|code&63;arr.push(b1,b2,b3,b4)}else{throw new Error(`Cannot convert character ${str.charAt(i)} (code ${code}) to utf-8`)}}else{arr.push(code<128?code:63)}}return new Uint8Array(arr)}}function decodeString(buffer){if(typeof TextDecoder!=="undefined"){const decoder=new TextDecoder;if(buffer instanceof Uint8Array){return decoder.decode(buffer)}const buf=Uint8Array.from(buffer);return decoder.decode(buf)}else if(typeof Buffer==="function"){if(buffer instanceof Buffer){return buffer.toString("utf-8")}else if(buffer instanceof Array){const typedArray=Uint8Array.from(buffer);const buf=Buffer.from(typedArray.buffer,typedArray.byteOffset,typedArray.byteOffset+typedArray.byteLength);return buf.toString("utf-8")}else if("buffer"in buffer&&buffer["buffer"]instanceof ArrayBuffer){const buf=Buffer.from(buffer["buffer"],buffer.byteOffset,buffer.byteOffset+buffer.byteLength);return buf.toString("utf-8")}else{throw new Error(`Unsupported buffer argument`)}}else{if(!(buffer instanceof Uint8Array)&&"buffer"in buffer&&buffer["buffer"]instanceof ArrayBuffer){buffer=new Uint8Array(buffer["buffer"],buffer.byteOffset,buffer.byteLength)}if(buffer instanceof Buffer||buffer instanceof Array||buffer instanceof Uint8Array){let str="";for(let i=0;i128){if((code&240)===240){const b1=code,b2=buffer[i+1],b3=buffer[i+2],b4=buffer[i+3];code=(b1&7)<<18|(b2&63)<<12|(b3&63)<<6|b4&63;i+=3}else if((code&224)===224){const b1=code,b2=buffer[i+1],b3=buffer[i+2];code=(b1&15)<<12|(b2&63)<<6|b3&63;i+=2}else if((code&192)===192){const b1=code,b2=buffer[i+1];code=(b1&31)<<6|b2&63;i++}else{throw new Error(`invalid utf-8 data`)}}if(code>=65536){code^=65536;const p1=55296|code>>10;const p2=56320|code&1023;str+=String.fromCharCode(p1);str+=String.fromCharCode(p2)}else{str+=String.fromCharCode(code)}}return str}else{throw new Error(`Unsupported buffer argument`)}}}function concatTypedArrays(a,b){const c=new a.constructor(a.length+b.length);c.set(a);c.set(b,a.length);return c}function cloneObject(original,stack){const{DataSnapshot:DataSnapshot}=require("./data-snapshot");if(original instanceof DataSnapshot){throw new TypeError(`Object to clone is a DataSnapshot (path "${original.ref.path}")`)}const checkAndFixTypedArray=obj=>{if(obj!==null&&typeof obj==="object"&&typeof obj.constructor==="function"&&typeof obj.constructor.name==="string"&&["Buffer","Uint8Array","Int8Array","Uint16Array","Int16Array","Uint32Array","Int32Array","BigUint64Array","BigInt64Array"].includes(obj.constructor.name)){obj=obj.buffer.slice(obj.byteOffset,obj.byteOffset+obj.byteLength)}return obj};original=checkAndFixTypedArray(original);if(typeof original!=="object"||original===null||original instanceof Date||original instanceof ArrayBuffer||original instanceof PathReference||original instanceof RegExp){return original}const cloneValue=val=>{if(stack.indexOf(val)>=0){throw new ReferenceError(`object contains a circular reference`)}val=checkAndFixTypedArray(val);if(val===null||val instanceof Date||val instanceof ArrayBuffer||val instanceof PathReference||val instanceof RegExp){return val}else if(val instanceof Array){stack.push(val);val=val.map(item=>cloneValue(item));stack.pop();return val}else if(typeof val==="object"){stack.push(val);val=cloneObject(val,stack);stack.pop();return val}else{return val}};if(typeof stack==="undefined"){stack=[original]}const clone={};Object.keys(original).forEach(key=>{let val=original[key];if(typeof val==="function"){return}clone[key]=cloneValue(val)});return clone}function compareValues(oldVal,newVal){const voids=[undefined,null];if(oldVal===newVal){return"identical"}else if(voids.indexOf(oldVal)>=0&&voids.indexOf(newVal)<0){return"added"}else if(voids.indexOf(oldVal)<0&&voids.indexOf(newVal)>=0){return"removed"}else if(typeof oldVal!==typeof newVal){return"changed"}else if(typeof oldVal==="object"){const isArray=oldVal instanceof Array;const oldKeys=isArray?Object.keys(oldVal).map(v=>parseInt(v)):Object.keys(oldVal);const newKeys=isArray?Object.keys(newVal).map(v=>parseInt(v)):Object.keys(newVal);const removedKeys=oldKeys.filter(key=>newKeys.indexOf(key)<0);const addedKeys=newKeys.filter(key=>oldKeys.indexOf(key)<0);const changedKeys=newKeys.reduce((changed,key)=>{if(oldKeys.indexOf(key)>=0){const val1=oldVal[key];const val2=newVal[key];const c=compareValues(val1,val2);if(c!=="identical"){changed.push({key:key,change:c})}}return changed},[]);if(addedKeys.length===0&&removedKeys.length===0&&changedKeys.length===0){return"identical"}else{return{added:addedKeys,removed:removedKeys,changed:changedKeys}}}else if(oldVal!==newVal){return"changed"}return"identical"}const getChildValues=(childKey,oldValue,newValue)=>{oldValue=oldValue===null?null:oldValue[childKey];if(typeof oldValue==="undefined"){oldValue=null}newValue=newValue===null?null:newValue[childKey];if(typeof newValue==="undefined"){newValue=null}return{oldValue:oldValue,newValue:newValue}};module.exports={numberToBytes:numberToBytes,bytesToNumber:bytesToNumber,concatTypedArrays:concatTypedArrays,cloneObject:cloneObject,compareValues:compareValues,getChildValues:getChildValues,encodeString:encodeString,decodeString:decodeString}}).call(this,require("buffer").Buffer)},{"./data-snapshot":8,"./path-reference":13,buffer:41}],18:[function(require,module,exports){var colors={};module["exports"]=colors;colors.themes={};var util=require("util");var ansiStyles=colors.styles=require("./styles");var defineProps=Object.defineProperties;var newLineRegex=new RegExp(/[\r\n]+/g);colors.supportsColor=require("./system/supports-colors").supportsColor;if(typeof colors.enabled==="undefined"){colors.enabled=colors.supportsColor()!==false}colors.enable=function(){colors.enabled=true};colors.disable=function(){colors.enabled=false};colors.stripColors=colors.strip=function(str){return(""+str).replace(/\x1B\[\d+m/g,"")};var stylize=colors.stylize=function stylize(str,style){if(!colors.enabled){return str+""}return ansiStyles[style].open+str+ansiStyles[style].close};var matchOperatorsRe=/[|\\{}()[\]^$+*?.]/g;var escapeStringRegexp=function(str){if(typeof str!=="string"){throw new TypeError("Expected a string")}return str.replace(matchOperatorsRe,"\\$&")};function build(_styles){var builder=function builder(){return applyStyle.apply(builder,arguments)};builder._styles=_styles;builder.__proto__=proto;return builder}var styles=function(){var ret={};ansiStyles.grey=ansiStyles.gray;Object.keys(ansiStyles).forEach((function(key){ansiStyles[key].closeRe=new RegExp(escapeStringRegexp(ansiStyles[key].close),"g");ret[key]={get:function(){return build(this._styles.concat(key))}}}));return ret}();var proto=defineProps((function colors(){}),styles);function applyStyle(){var args=Array.prototype.slice.call(arguments);var str=args.map((function(arg){if(arg!=undefined&&arg.constructor===String){return arg}else{return util.inspect(arg)}})).join(" ");if(!colors.enabled||!str){return str}var newLinesPresent=str.indexOf("\n")!=-1;var nestedStyles=this._styles;var i=nestedStyles.length;while(i--){var code=ansiStyles[nestedStyles[i]];str=code.open+str.replace(code.closeRe,code.open)+code.close;if(newLinesPresent){str=str.replace(newLineRegex,(function(match){return code.close+match+code.open}))}}return str}colors.setTheme=function(theme){if(typeof theme==="string"){console.log("colors.setTheme now only accepts an object, not a string. "+"If you are trying to set a theme from a file, it is now your (the "+"caller's) responsibility to require the file. The old syntax "+"looked like colors.setTheme(__dirname + "+"'/../themes/generic-logging.js'); The new syntax looks like "+"colors.setTheme(require(__dirname + "+"'/../themes/generic-logging.js'));");return}for(var style in theme){(function(style){colors[style]=function(str){if(typeof theme[style]==="object"){var out=str;for(var i in theme[style]){out=colors[theme[style][i]](out)}return out}return colors[theme[style]](str)}})(style)}};function init(){var ret={};Object.keys(styles).forEach((function(name){ret[name]={get:function(){return build([name])}}}));return ret}var sequencer=function sequencer(map,str){var exploded=str.split("");exploded=exploded.map(map);return exploded.join("")};colors.trap=require("./custom/trap");colors.zalgo=require("./custom/zalgo");colors.maps={};colors.maps.america=require("./maps/america")(colors);colors.maps.zebra=require("./maps/zebra")(colors);colors.maps.rainbow=require("./maps/rainbow")(colors);colors.maps.random=require("./maps/random")(colors);for(var map in colors.maps){(function(map){colors[map]=function(str){return sequencer(colors.maps[map],str)}})(map)}defineProps(colors,init())},{"./custom/trap":19,"./custom/zalgo":20,"./maps/america":23,"./maps/rainbow":24,"./maps/random":25,"./maps/zebra":26,"./styles":27,"./system/supports-colors":29,util:47}],19:[function(require,module,exports){module["exports"]=function runTheTrap(text,options){var result="";text=text||"Run the trap, drop the bass";text=text.split("");var trap={a:["@","Ą","Ⱥ","Ʌ","Δ","Λ","Д"],b:["ß","Ɓ","Ƀ","ɮ","β","฿"],c:["©","Ȼ","Ͼ"],d:["Ð","Ɗ","Ԁ","ԁ","Ԃ","ԃ"],e:["Ë","ĕ","Ǝ","ɘ","Σ","ξ","Ҽ","੬"],f:["Ӻ"],g:["ɢ"],h:["Ħ","ƕ","Ң","Һ","Ӈ","Ԋ"],i:["༏"],j:["Ĵ"],k:["ĸ","Ҡ","Ӄ","Ԟ"],l:["Ĺ"],m:["ʍ","Ӎ","ӎ","Ԡ","ԡ","൩"],n:["Ñ","ŋ","Ɲ","Ͷ","Π","Ҋ"],o:["Ø","õ","ø","Ǿ","ʘ","Ѻ","ם","۝","๏"],p:["Ƿ","Ҏ"],q:["্"],r:["®","Ʀ","Ȑ","Ɍ","ʀ","Я"],s:["§","Ϟ","ϟ","Ϩ"],t:["Ł","Ŧ","ͳ"],u:["Ʊ","Ս"],v:["ט"],w:["Ш","Ѡ","Ѽ","൰"],x:["Ҳ","Ӿ","Ӽ","ӽ"],y:["¥","Ұ","Ӌ"],z:["Ƶ","ɀ"]};text.forEach((function(c){c=c.toLowerCase();var chars=trap[c]||[" "];var rand=Math.floor(Math.random()*chars.length);if(typeof trap[c]!=="undefined"){result+=trap[c][rand]}else{result+=c}}));return result}},{}],20:[function(require,module,exports){module["exports"]=function zalgo(text,options){text=text||" he is here ";var soul={up:["̍","̎","̄","̅","̿","̑","̆","̐","͒","͗","͑","̇","̈","̊","͂","̓","̈","͊","͋","͌","̃","̂","̌","͐","̀","́","̋","̏","̒","̓","̔","̽","̉","ͣ","ͤ","ͥ","ͦ","ͧ","ͨ","ͩ","ͪ","ͫ","ͬ","ͭ","ͮ","ͯ","̾","͛","͆","̚"],down:["̖","̗","̘","̙","̜","̝","̞","̟","̠","̤","̥","̦","̩","̪","̫","̬","̭","̮","̯","̰","̱","̲","̳","̹","̺","̻","̼","ͅ","͇","͈","͉","͍","͎","͓","͔","͕","͖","͙","͚","̣"],mid:["̕","̛","̀","́","͘","̡","̢","̧","̨","̴","̵","̶","͜","͝","͞","͟","͠","͢","̸","̷","͡"," ҉"]};var all=[].concat(soul.up,soul.down,soul.mid);function randomNumber(range){var r=Math.floor(Math.random()*range);return r}function isChar(character){var bool=false;all.filter((function(i){bool=i===character}));return bool}function heComes(text,options){var result="";var counts;var l;options=options||{};options["up"]=typeof options["up"]!=="undefined"?options["up"]:true;options["mid"]=typeof options["mid"]!=="undefined"?options["mid"]:true;options["down"]=typeof options["down"]!=="undefined"?options["down"]:true;options["size"]=typeof options["size"]!=="undefined"?options["size"]:"maxi";text=text.split("");for(l in text){if(isChar(l)){continue}result=result+text[l];counts={up:0,down:0,mid:0};switch(options.size){case"mini":counts.up=randomNumber(8);counts.mid=randomNumber(2);counts.down=randomNumber(8);break;case"maxi":counts.up=randomNumber(16)+3;counts.mid=randomNumber(4)+1;counts.down=randomNumber(64)+3;break;default:counts.up=randomNumber(8)+1;counts.mid=randomNumber(6)/2;counts.down=randomNumber(8)+1;break}var arr=["up","mid","down"];for(var d in arr){var index=arr[d];for(var i=0;i<=counts[index];i++){if(options[index]){result=result+soul[index][randomNumber(soul[index].length)]}}}}return result}return heComes(text,options)}},{}],21:[function(require,module,exports){var colors=require("./colors");module["exports"]=function(){var addProperty=function(color,func){String.prototype.__defineGetter__(color,func)};addProperty("strip",(function(){return colors.strip(this)}));addProperty("stripColors",(function(){return colors.strip(this)}));addProperty("trap",(function(){return colors.trap(this)}));addProperty("zalgo",(function(){return colors.zalgo(this)}));addProperty("zebra",(function(){return colors.zebra(this)}));addProperty("rainbow",(function(){return colors.rainbow(this)}));addProperty("random",(function(){return colors.random(this)}));addProperty("america",(function(){return colors.america(this)}));var x=Object.keys(colors.styles);x.forEach((function(style){addProperty(style,(function(){return colors.stylize(this,style)}))}));function applyTheme(theme){var stringPrototypeBlacklist=["__defineGetter__","__defineSetter__","__lookupGetter__","__lookupSetter__","charAt","constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf","charCodeAt","indexOf","lastIndexOf","length","localeCompare","match","repeat","replace","search","slice","split","substring","toLocaleLowerCase","toLocaleUpperCase","toLowerCase","toUpperCase","trim","trimLeft","trimRight"];Object.keys(theme).forEach((function(prop){if(stringPrototypeBlacklist.indexOf(prop)!==-1){console.log("warn: ".red+("String.prototype"+prop).magenta+" is probably something you don't want to override. "+"Ignoring style name")}else{if(typeof theme[prop]==="string"){colors[prop]=colors[theme[prop]]}else{var tmp=colors[theme[prop][0]];for(var t=1;t=2,has16m:level>=3}}function supportsColor(stream){if(forceColor===false){return 0}if(hasFlag("color=16m")||hasFlag("color=full")||hasFlag("color=truecolor")){return 3}if(hasFlag("color=256")){return 2}if(stream&&!stream.isTTY&&forceColor!==true){return 0}var min=forceColor?1:0;if(process.platform==="win32"){var osRelease=os.release().split(".");if(Number(process.versions.node.split(".")[0])>=8&&Number(osRelease[0])>=10&&Number(osRelease[2])>=10586){return Number(osRelease[2])>=14931?3:2}return 1}if("CI"in env){if(["TRAVIS","CIRCLECI","APPVEYOR","GITLAB_CI"].some((function(sign){return sign in env}))||env.CI_NAME==="codeship"){return 1}return min}if("TEAMCITY_VERSION"in env){return/^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(env.TEAMCITY_VERSION)?1:0}if("TERM_PROGRAM"in env){var version=parseInt((env.TERM_PROGRAM_VERSION||"").split(".")[0],10);switch(env.TERM_PROGRAM){case"iTerm.app":return version>=3?3:2;case"Hyper":return 3;case"Apple_Terminal":return 2}}if(/-256(color)?$/i.test(env.TERM)){return 2}if(/^screen|^xterm|^vt100|^rxvt|color|ansi|cygwin|linux/i.test(env.TERM)){return 1}if("COLORTERM"in env){return 1}if(env.TERM==="dumb"){return min}return min}function getSupportLevel(stream){var level=supportsColor(stream);return translateLevel(level)}module.exports={supportsColor:getSupportLevel,stdout:getSupportLevel(process.stdout),stderr:getSupportLevel(process.stderr)}}).call(this,require("_process"))},{"./has-flag.js":28,_process:45,os:44}],30:[function(require,module,exports){const{AceBase:AceBase,AceBaseLocalSettings:AceBaseLocalSettings}=require("./acebase-local");const{CustomStorageSettings:CustomStorageSettings,CustomStorageHelpers:CustomStorageHelpers,ICustomStorageNode:ICustomStorageNode,ICustomStorageNodeMetaData:ICustomStorageNodeMetaData}=require("./storage-custom");const deprecatedConstructorError=`Using AceBase constructor in the browser to use localStorage is deprecated!\nSwitch to:\nIndexedDB implementation (FASTER, MORE RELIABLE):\n let db = AceBase.WithIndexedDB(name, settings)\nOr, new LocalStorage implementation:\n let db = AceBase.WithLocalStorage(name, settings)\nOr, write your own CustomStorage adapter:\n let myCustomStorage = new CustomStorageSettings({ ... });\n let db = new AceBase(name, { storage: myCustomStorage })`;class BrowserAceBase extends AceBase{constructor(name,settings){if(typeof settings!=="object"||typeof settings.storage!=="object"){throw new Error(deprecatedConstructorError)}super(name,settings)}static WithIndexedDB(dbname,settings){settings=settings||{};if(!settings.logLevel){settings.logLevel="error"}const IndexedDB=window.indexedDB||window.mozIndexedDB||window.webkitIndexedDB||window.msIndexedDB;let request=IndexedDB.open(`${dbname}.acebase`,1);let readyResolve,readyReject,readyPromise=new Promise((rs,rj)=>{readyResolve=rs;readyReject=rj});request.onupgradeneeded=e=>{let db=request.result;db.createObjectStore("nodes",{keyPath:"path"});db.createObjectStore("content")};let db;request.onsuccess=e=>{db=request.result;readyResolve()};request.onerror=e=>{readyReject(e)};const storageSettings=new CustomStorageSettings({name:"IndexedDB",ready(){return readyPromise},get(path){const tx=db.transaction(["nodes","content"],"readonly");const request=tx.objectStore("nodes").get(path);return new Promise((resolve,reject)=>{request.onsuccess=event=>{const data=request.result;if(!data){return resolve(null)}const node=data.metadata;const contentReq=tx.objectStore("content").get(path);contentReq.onsuccess=e=>{node.value=contentReq.result;resolve(node)};contentReq.onerror=e=>reject(e)};request.onerror=e=>{console.error(`IndexedDB get error`,e);reject(e)}})},set(path,node){const copy={};const value=node.value;Object.assign(copy,node);delete copy.value;const metadata=copy;const obj={path:path,metadata:metadata};return new Promise((resolve,reject)=>{const tx=db.transaction(["nodes","content"],"readwrite");const request=tx.objectStore("nodes").put(obj);request.onerror=e=>reject(e);request.onsuccess=e=>{const contentReq=tx.objectStore("content").put(value,path);contentReq.onsuccess=e=>resolve();contentReq.onerror=e=>{tx.abort();reject(e)}}})},remove(path){const tx=db.transaction(["nodes","content"],"readwrite");return new Promise((resolve,reject)=>{const r1=tx.objectStore("content").delete(path);r1.onerror=e=>reject(e);r1.onsuccess=e=>{const r2=tx.objectStore("nodes").delete(path);r2.onerror=e=>{tx.abort();reject(e)};r2.onsuccess=e=>resolve()}})},childrenOf(path,include,checkCallback,addCallback){const pathInfo=CustomStorageHelpers.PathInfo.get(path);const lockStores=include.value?["nodes","content"]:"nodes";const tx=db.transaction(lockStores,"readonly");const store=tx.objectStore("nodes");const query=IDBKeyRange.lowerBound(path,true);return new Promise((resolve,reject)=>{const cursor=include.metadata?store.openCursor(query):store.openKeyCursor(query);cursor.onerror=e=>reject(e);cursor.onsuccess=async e=>{const otherPath=cursor.result?cursor.result.key:null;let keepGoing=true;if(otherPath===null){keepGoing=false}else if(!pathInfo.isAncestorOf(otherPath)){keepGoing=false}else if(pathInfo.isParentOf(otherPath)&&checkCallback(otherPath)){let node;if(include.metadata){const valueCursor=cursor;const data=valueCursor.result.value;node=data.metadata;if(include.value){const req=tx.objectStore("content").get(otherPath);await new Promise((resolve,reject)=>{req.onerror=e=>reject(e);req.onsuccess=e=>{node.value=req.result;resolve()}})}}keepGoing=addCallback(otherPath,node)}if(keepGoing){try{cursor.result.continue()}catch(err){keepGoing=false}}if(!keepGoing){resolve()}}})},descendantsOf(path,include,checkCallback,addCallback){const pathInfo=CustomStorageHelpers.PathInfo.get(path);const lockStores=include.value?["nodes","content"]:"nodes";const tx=db.transaction(lockStores,"readonly");const store=tx.objectStore("nodes");const query=IDBKeyRange.lowerBound(path,true);return new Promise((resolve,reject)=>{const cursor=include.metadata?store.openCursor(query):store.openKeyCursor(query);cursor.onerror=e=>reject(e);cursor.onsuccess=async e=>{const otherPath=cursor.result?cursor.result.key:null;let keepGoing=true;if(otherPath===null){keepGoing=false}else if(!pathInfo.isAncestorOf(otherPath)){keepGoing=false}else if(checkCallback(otherPath)){let node;if(include.metadata){const valueCursor=cursor;const data=valueCursor.result.value;node=data.metadata;if(include.value){const req=tx.objectStore("content").get(otherPath);await new Promise((resolve,reject)=>{req.onerror=e=>reject(e);req.onsuccess=e=>{node.value=req.result;resolve()}})}}keepGoing=addCallback(otherPath,node)}if(keepGoing){try{cursor.result.continue()}catch(err){keepGoing=false}}if(!keepGoing){resolve()}}})}});return new AceBase(dbname,{logLevel:settings.logLevel,storage:storageSettings})}static WithLocalStorage(dbname,settings){settings=settings||{};if(!settings.logLevel){settings.logLevel="error"}const localStorage=settings.provider?settings.provider:settings.temp?window.localStorage:window.sessionStorage;const storageKeysPrefix=`${dbname}.acebase::`;function getPathFromStorageKey(key){return key.slice(storageKeysPrefix.length)}function getStorageKeyForPath(path){return`${storageKeysPrefix}${path}`}const storageSettings=new CustomStorageSettings({name:"LocalStorage",ready(){return Promise.resolve()},get(path){return new Promise(resolve=>{const json=localStorage.getItem(getStorageKeyForPath(path));const val=JSON.parse(json);resolve(val)})},set(path,val){return new Promise(resolve=>{const json=JSON.stringify(val);localStorage.setItem(getStorageKeyForPath(path),json);resolve()})},remove(path){return new Promise(resolve=>{localStorage.removeItem(getStorageKeyForPath(path));resolve()})},childrenOf(path,include,checkCallback,addCallback){return new Promise(resolve=>{const pathInfo=CustomStorageHelpers.PathInfo.get(path);for(let i=0;i{const pathInfo=CustomStorageHelpers.PathInfo.get(path);for(let i=0;i{this.emit("ready")})}}module.exports={AceBase:AceBase,AceBaseLocalSettings:AceBaseLocalSettings}},{"./api-local":32,"./storage":40,"acebase-core":11}],32:[function(require,module,exports){const{Api:Api,Utils:Utils}=require("acebase-core");const{AceBase:AceBase}=require("./acebase-local");const{StorageSettings:StorageSettings}=require("./storage");const{AceBaseStorage:AceBaseStorage,AceBaseStorageSettings:AceBaseStorageSettings}=require("./storage-acebase");const{SQLiteStorage:SQLiteStorage,SQLiteStorageSettings:SQLiteStorageSettings}=require("./storage-sqlite");const{MSSQLStorage:MSSQLStorage,MSSQLStorageSettings:MSSQLStorageSettings}=require("./storage-mssql");const{LocalStorage:LocalStorage,LocalStorageSettings:LocalStorageSettings}=require("./storage-localstorage");const{CustomStorage:CustomStorage,CustomStorageSettings:CustomStorageSettings}=require("./storage-custom");const{Node:Node}=require("./node");const{DataIndex:DataIndex}=require("./data-index");class LocalApi extends Api{constructor(dbname="default",settings,readyCallback){super();this.db=settings.db;if(typeof settings.storage==="object"){settings.storage.logLevel=settings.logLevel;if(SQLiteStorageSettings&&(settings.storage instanceof SQLiteStorageSettings||settings.storage.type==="sqlite")){this.storage=new SQLiteStorage(dbname,settings.storage)}else if(MSSQLStorageSettings&&(settings.storage instanceof MSSQLStorageSettings||settings.storage.type==="mssql")){this.storage=new MSSQLStorage(dbname,settings.storage)}else if(LocalStorageSettings&&(settings.storage instanceof LocalStorageSettings||settings.storage.type==="localstorage")){this.storage=new LocalStorage(dbname,settings.storage)}else if(CustomStorageSettings&&(settings.storage instanceof CustomStorageSettings||settings.storage.type==="custom")){this.storage=new CustomStorage(dbname,settings.storage)}else{const storageSettings=settings.storage instanceof AceBaseStorageSettings?settings.storage:new AceBaseStorageSettings(settings.storage);this.storage=new AceBaseStorage(dbname,storageSettings)}}else{settings.storage=new AceBaseStorageSettings({logLevel:settings.logLevel});this.storage=new AceBaseStorage(dbname,settings.storage)}this.storage.on("ready",readyCallback)}stats(options){return Promise.resolve(this.storage.stats)}subscribe(path,event,callback){this.storage.subscriptions.add(path,event,callback)}unsubscribe(path,event=undefined,callback=undefined){this.storage.subscriptions.remove(path,event,callback)}set(path,value,flags=undefined){return Node.update(this.storage,path,value,{merge:false})}update(path,updates,flags=undefined){return Node.update(this.storage,path,updates,{merge:true})}get(path,options){return Node.getValue(this.storage,path,options)}transaction(path,callback){return Node.transaction(this.storage,path,callback)}exists(path){return Node.exists(this.storage,path)}query2(path,query,options={snapshots:false,include:undefined,exclude:undefined,child_objects:undefined}){}query(path,query,options={snapshots:false,include:undefined,exclude:undefined,child_objects:undefined,eventHandler:event=>{}}){if(typeof options!=="object"){options={}}if(typeof options.snapshots==="undefined"){options.snapshots=false}const sortMatches=matches=>{matches.sort((a,b)=>{const compare=i=>{const o=query.order[i];let left=a.val[o.key];let right=b.val[o.key];if(typeof left==="undefined"&&typeof right!=="undefined"){return o.ascending?-1:1}if(typeof left!=="undefined"&&typeof right==="undefined"){return o.ascending?1:-1}if(typeof left==="undefined"&&typeof right==="undefined"){return 0}if(left==right){if(iright){return o.ascending?1:-1}};return compare(0)})};const loadResultsData=(preResults,options)=>{if(preResults.length===0){return Promise.resolve([])}const maxBatchSize=50;let batches=[];const items=preResults.map((result,index)=>({path:result.path,index:index}));while(items.length>0){let batchItems=items.splice(0,maxBatchSize);batches.push(batchItems)}const results=[];const nextBatch=()=>{const batch=batches.shift();return Promise.all(batch.map(item=>{const{path:path,index:index}=item;return Node.getValue(this.storage,path,options).then(val=>{if(val===null){this.storage.debug.warn(`Indexed result "/${path}" does not have a record!`);return}const result={path:path,val:val};if(stepsExecuted.sorted){results[index]=result}else{results.push(result);if(!stepsExecuted.skipped&&results.length>query.skip+query.take){sortMatches(results);results.pop()}}})})).then(()=>{if(batches.length>0){return nextBatch()}})};return nextBatch().then(()=>{return results})};const isWildcardPath=path.indexOf("*")>=0||path.indexOf("$")>=0;const availableIndexes=this.storage.indexes.get(path);const usingIndexes=[];query.filters.forEach(filter=>{if(filter.index){return}const indexesOnKey=availableIndexes.filter(index=>index.key===filter.key).filter(index=>{return index.validOperators.includes(filter.op)});if(indexesOnKey.length>=1){const otherFilterKeys=query.filters.filter(f=>f!==filter).map(f=>f.key);const sortKeys=query.order.map(o=>o.key).filter(key=>key!==filter.key);const beneficialIndexes=indexesOnKey.map(index=>{const availableKeys=index.includeKeys.concat(index.key);const forOtherFilters=availableKeys.filter(key=>otherFilterKeys.indexOf(key)>=0);const forSorting=availableKeys.filter(key=>sortKeys.indexOf(key)>=0);const forBoth=forOtherFilters.concat(forSorting.filter(index=>forOtherFilters.indexOf(index)<0));const points={filters:forOtherFilters.length,sorting:forSorting.length*(query.take>0?forSorting.length:1),both:forBoth.length*forBoth.length,get total(){return this.filters+this.sorting+this.both}};return{index:index,points:points.total,filterKeys:forOtherFilters,sortKeys:forSorting}});beneficialIndexes.sort((a,b)=>a.points>b.points?-1:1);const bestBenificialIndex=beneficialIndexes[0];filter.index=bestBenificialIndex.index;bestBenificialIndex.filterKeys.forEach(key=>{query.filters.filter(f=>f!==filter&&f.key===key).forEach(f=>{if(!DataIndex.validOperators.includes(f.op)){return}f.indexUsage="filter";f.index=bestBenificialIndex.index})});bestBenificialIndex.sortKeys.forEach(key=>{query.order.filter(s=>s.key===key).forEach(s=>{s.index=bestBenificialIndex.index})})}if(filter.index){usingIndexes.push({index:filter.index,description:filter.index.description})}});if(query.order.length>0&&query.take>0){query.order.forEach(sort=>{if(sort.index){return}sort.index=availableIndexes.filter(index=>index.key===sort.key).find(index=>index.type==="normal")})}const indexDescriptions=usingIndexes.map(index=>index.description).join(", ");usingIndexes.length>0&&this.storage.debug.log(`Using indexes for query: ${indexDescriptions}`);const tableScanFilters=query.filters.filter(filter=>!filter.index);const specialOpsRegex=/^[a-z]+\:/i;if(tableScanFilters.some(filter=>specialOpsRegex.test(filter.op))){const f=tableScanFilters.find(filter=>specialOpsRegex.test(filter.op));const err=new Error(`query contains operator "${f.op}" which requires a special index that was not found on path "${path}", key "${f.key}"`);return Promise.reject(err)}const allowedTableScanOperators=["<","<=","==","!=",">=",">","like","!like","in","!in","matches","!matches","between","!between","has","!has","contains","!contains","exists","!exists"];for(let i=0;i0){const keys=tableScanFilters.reduce((keys,f)=>{if(keys.indexOf(f.key)<0){keys.push(f.key)}return keys},[]).map(key=>`"${key}"`);const err=new Error(`This wildcard path query on "/${path}" requires index(es) on key(s): ${keys.join(", ")}. Create the index(es) and retry`);return Promise.reject(err)}const indexScanPromises=[];query.filters.forEach(filter=>{if(filter.index&&filter.indexUsage!=="filter"){let promise=filter.index.query(filter.op,filter.compare).then(results=>{options.eventHandler&&options.eventHandler({name:"stats",type:"index_query",source:filter.index.description,stats:results.stats});if(results.hints.length>0){options.eventHandler&&options.eventHandler({name:"hints",type:"index_query",source:filter.index.description,hints:results.hints})}return results});const resultFilters=query.filters.filter(f=>f.index===filter.index&&f.indexUsage==="filter");if(resultFilters.length>0){promise=promise.then(results=>{resultFilters.forEach(filter=>{results=results.filterMetadata(filter.key,filter.op,filter.compare)});return results})}indexScanPromises.push(promise)}});const stepsExecuted={filtered:query.filters.length===0,skipped:query.skip===0,taken:query.take===0,sorted:query.order.length===0,preDataLoaded:false,dataLoaded:false};if(query.filters.length===0&&query.take===0){this.storage.debug.error(`Filterless queries must use .take to limit the results. Defaulting to 100 for query on path "${path}"`);query.take=100}if(query.filters.length===0&&query.order.length>0&&query.order[0].index){const sortIndex=query.order[0].index;this.storage.debug.log(`Using index for sorting: ${sortIndex.description}`);const promise=sortIndex.take(query.skip,query.take,query.order[0].ascending).then(results=>{options.eventHandler&&options.eventHandler({name:"stats",type:"sort_index_take",source:filter.index.description,stats:results.stats});if(results.hints.length>0){options.eventHandler&&options.eventHandler({name:"hints",type:"sort_index_take",source:filter.index.description,hints:results.hints})}return results});indexScanPromises.push(promise);stepsExecuted.skipped=true;stepsExecuted.taken=true;stepsExecuted.sorted=true}return Promise.all(indexScanPromises).then(indexResultSets=>{let indexedResults=[];if(indexResultSets.length===1){const resultSet=indexResultSets[0];indexedResults=resultSet.map(match=>{const result={key:match.key,path:match.path,val:{[resultSet.filterKey]:match.value}};match.metadata&&Object.assign(result.val,match.metadata);return result});stepsExecuted.filtered=true}else if(indexResultSets.length>1){indexResultSets.sort((a,b)=>a.length{const result={key:match.key,path:match.path,val:{[shortestSet.filterKey]:match.value}};const matchedInAllSets=otherSets.every(set=>set.findIndex(m=>match.path===match.path)>=0);if(matchedInAllSets){match.metadata&&Object.assign(result.val,match.metadata);otherSets.forEach(set=>{const otherResult=set.find(r=>r.path===result.path);result.val[set.filterKey]=otherResult.value;otherResult.metadata&&Object.assign(result.val,otherResult.metadata)});results.push(result)}return results},[]);stepsExecuted.filtered=true}if(isWildcardPath||indexScanPromises.length>0&&tableScanFilters.length===0){if(query.order.length===0||query.order.every(o=>o.index)){stepsExecuted.preDataLoaded=true;if(!stepsExecuted.sorted&&query.order.length>0){sortMatches(indexedResults)}stepsExecuted.sorted=true;if(!stepsExecuted.skipped&&query.skip>0){indexedResults=indexedResults.slice(query.skip)}if(!stepsExecuted.taken&&query.take>0){indexedResults=indexedResults.slice(0,query.take)}stepsExecuted.skipped=true;stepsExecuted.taken=true;if(!options.snapshots){return indexedResults}const childOptions={include:options.include,exclude:options.exclude,child_objects:options.child_objects};return loadResultsData(indexedResults,childOptions).then(results=>{stepsExecuted.dataLoaded=true;return results})}if(options.snapshots||!stepsExecuted.sorted){const loadPartialResults=query.order.length>0;const childOptions=loadPartialResults?{include:query.order.map(order=>order.key)}:{include:options.include,exclude:options.exclude,child_objects:options.child_objects};return loadResultsData(indexedResults,childOptions).then(results=>{if(query.order.length>0){sortMatches(results)}stepsExecuted.sorted=true;if(query.skip>0){results=results.slice(query.skip)}if(query.take>0){results=results.slice(0,query.take)}stepsExecuted.skipped=true;stepsExecuted.taken=true;if(options.snapshots&&loadPartialResults){return loadResultsData(results,{include:options.include,exclude:options.exclude,child_objects:options.child_objects})}return results})}else{return indexedResults}}let indexKeyFilter;if(indexedResults.length>0){indexKeyFilter=indexedResults.map(result=>result.key)}const promises=[];let matches=[];let preliminaryStop=false;const loadPartialData=query.order.length>0;const childOptions=loadPartialData?{include:query.order.map(order=>order.key)}:{include:options.include,exclude:options.exclude,child_objects:options.child_objects};return Node.getChildren(this.storage,path,indexKeyFilter).next(child=>{if(child.type===Node.VALUE_TYPES.OBJECT){if(!child.address){return}if(preliminaryStop){return false}const p=Node.matches(this.storage,child.address.path,tableScanFilters).then(isMatch=>{if(!isMatch){return null}const childPath=child.address.path;if(options.snapshots||query.order.length>0){return Node.getValue(this.storage,childPath,childOptions).then(val=>{return{path:childPath,val:val}})}else{return{path:childPath}}}).then(result=>{if(result!==null){matches.push(result);if(query.take>0&&matches.length>query.take+query.skip){if(query.order.length>0){sortMatches(matches)}else{preliminaryStop=true}matches.pop()}}});promises.push(p)}}).catch(reason=>{this.storage.debug.warn(`Error getting child stream: ${reason}`);return[]}).then(()=>{return Promise.all(promises).then(()=>{stepsExecuted.preDataLoaded=loadPartialData;stepsExecuted.dataLoaded=!loadPartialData;if(query.order.length>0){sortMatches(matches)}stepsExecuted.sorted=true;if(query.skip>0){matches=matches.slice(query.skip)}stepsExecuted.skipped=true;if(query.take>0){matches=matches.slice(0,query.take)}stepsExecuted.taken=true;if(!stepsExecuted.dataLoaded){return loadResultsData(matches,{include:options.include,exclude:options.exclude,child_objects:options.child_objects}).then(results=>{stepsExecuted.dataLoaded=true;return results})}return matches})})}).then(matches=>{if(!stepsExecuted.sorted&&query.order.length>0){sortMatches(matches)}if(!options.snapshots){matches=matches.map(match=>match.path)}if(!stepsExecuted.skipped&&query.skip>0){matches=matches.slice(query.skip)}if(!stepsExecuted.taken&&query.take>0){matches=matches.slice(0,query.take)}if(options.monitor===true){options.monitor={add:true,change:true,remove:true}}if(typeof options.monitor==="object"&&(options.monitor.add||options.monitor.change||options.monitor.remove)){const matchedPaths=options.snapshots?matches.map(match=>match.path):matches.slice();const ref=this.db.ref(path);const removeMatch=path=>{const index=matchedPaths.indexOf(path);if(index<0){return}matchedPaths.splice(index,1)};const addMatch=path=>{if(matchedPaths.includes(path)){return}matchedPaths.push(path)};const stopMonitoring=()=>{this.unsubscribe(ref.path,"notify_child_changed",childChangedCallback);this.unsubscribe(ref.path,"notify_child_added",childAddedCallback);this.unsubscribe(ref.path,"notify_child_removed",childRemovedCallback)};const childChangedCallback=(err,path,newValue,oldValue)=>{const wasMatch=matchedPaths.includes(path);let keepMonitoring=true;const checkKeys=[];query.filters.forEach(f=>!checkKeys.includes(f.key)&&checkKeys.push(f.key));const seenKeys=[];typeof oldValue==="object"&&Object.keys(oldValue).forEach(key=>!seenKeys.includes(key)&&seenKeys.push(key));typeof newValue==="object"&&Object.keys(newValue).forEach(key=>!seenKeys.includes(key)&&seenKeys.push(key));const missingKeys=[];let isMatch=seenKeys.every(key=>{if(!checkKeys.includes(key)){return true}const filters=query.filters.filter(filter=>filter.key===key);return filters.every(filter=>{if(allowedTableScanOperators.includes(filter.op)){return this.storage.test(newValue[key],filter.op,filter.compare)}if(filter.index.constructor.name==="FullTextDataIndex"&&filter.index.localeKey&&!seenKeys.includes(filter.index.localeKey)){missingKeys.push(filter.index.localeKey);return true}return filter.index.test(newValue,filter.op,filter.compare)})});if(isMatch){missingKeys.push(...checkKeys.filter(key=>!seenKeys.includes(key)));let promise=Promise.resolve(true);if(!wasMatch&&missingKeys.length>0){const filterQueue=query.filters.filter(f=>missingKeys.includes(f.key));const simpleFilters=filterQueue.filter(f=>allowedTableScanOperators.includes(f.op));const indexFilters=filterQueue.filter(f=>!allowedTableScanOperators.includes(f.op));const processFilters=()=>{const checkIndexFilters=()=>{const keysToLoad=indexFilters.reduce((keys,filter)=>{if(!keys.includes(filter.key)){keys.push(filter.key)}if(filter.index.constructor.name==="FullTextDataIndex"&&filter.index.localeKey&&!keys.includes(filter.index.localeKey)){keys.push(filter.index.localeKey)}return keys},[]);return Node.getValue(this.storage,path,{include:keysToLoad}).then(val=>{if(val===null){return false}return indexFilters.every(filter=>filter.index.test(val,filter.op,filter.compare))})};if(simpleFilters.length>0){return Node.matches(this.storage,path,simpleFilters).then(isMatch=>{if(isMatch){if(indexFilters.length===0){return true}return checkIndexFilters()}return false})}else{return checkIndexFilters()}};promise=processFilters()}return promise.then(isMatch=>{if(isMatch){if(!wasMatch){addMatch(path)}let gotValue=value=>{if(wasMatch&&options.monitor.change){keepMonitoring=options.eventHandler({name:"change",path:path,value:value})}else if(!wasMatch&&options.monitor.add){keepMonitoring=options.eventHandler({name:"add",path:path,value:value})}if(keepMonitoring===false){stopMonitoring()}};if(options.snapshots){const loadOptions={include:options.include,exclude:options.exclude,child_objects:options.child_objects};return this.storage.getNodeValue(path,loadOptions).then(gotValue)}else{return gotValue(newValue)}}else if(wasMatch){removeMatch(path);if(options.monitor.remove){keepMonitoring=options.eventHandler({name:"remove",path:path,value:oldValue})}}if(keepMonitoring===false){stopMonitoring()}})}else{if(wasMatch){removeMatch(path);if(options.monitor.remove){keepMonitoring=options.eventHandler({name:"remove",path:path,value:oldValue});if(keepMonitoring===false){stopMonitoring()}}}}};const childAddedCallback=(err,path,newValue,oldValue)=>{let isMatch=query.filters.every(filter=>{if(allowedTableScanOperators.includes(filter.op)){return this.storage.test(newValue[filter.key],filter.op,filter.compare)}else{return filter.index.test(newValue,filter.op,filter.compare)}});let keepMonitoring=true;if(isMatch){addMatch(path);if(options.monitor.add){keepMonitoring=options.eventHandler({name:"add",path:path,value:options.snapshots?newValue:null})}}if(keepMonitoring===false){stopMonitoring()}};const childRemovedCallback=(err,path,newValue,oldValue)=>{let keepMonitoring=true;removeMatch(path);if(options.monitor.remove){keepMonitoring=options.eventHandler({name:"remove",path:path,value:options.snapshots?oldValue:null})}if(keepMonitoring===false){stopMonitoring()}};if(options.monitor.add||options.monitor.change||options.monitor.remove){this.subscribe(ref.path,"notify_child_changed",childChangedCallback)}if(options.monitor.remove){this.subscribe(ref.path,"notify_child_removed",childRemovedCallback)}if(options.monitor.add){this.subscribe(ref.path,"notify_child_added",childAddedCallback)}}return matches})}createIndex(path,key,options){return this.storage.indexes.create(path,key,options)}getIndexes(){return Promise.resolve(this.storage.indexes.list())}reflect(path,type,args){const getChildren=(path,limit=50,skip=0)=>{if(typeof limit==="string"){limit=parseInt(limit)}if(typeof skip==="string"){skip=parseInt(skip)}const children=[];let n=0,stop=skip+limit;return Node.getChildren(this.storage,path).next(childInfo=>{n++;if(limit===0||n<=stop&&n>skip){children.push({key:typeof childInfo.key==="string"?childInfo.key:childInfo.index,type:childInfo.valueTypeName,value:childInfo.value,address:typeof childInfo.address==="object"&&"pageNr"in childInfo.address?{pageNr:childInfo.address.pageNr,recordNr:childInfo.address.recordNr}:undefined})}if(limit>0&&n>stop){return false}}).then(()=>{return{more:limit!==0&&n>stop,list:children}})};switch(type){case"children":{return getChildren(path,args.limit,args.skip)}case"info":{const info={key:"",exists:false,type:"unknown",value:undefined,children:{more:false,list:[]}};return Node.getInfo(this.storage,path).then(nodeInfo=>{info.key=nodeInfo.key;info.exists=nodeInfo.exists;info.type=nodeInfo.valueTypeName;info.value=nodeInfo.value;let hasChildren=nodeInfo.exists&&nodeInfo.address&&[Node.VALUE_TYPES.OBJECT,Node.VALUE_TYPES.ARRAY].includes(nodeInfo.type);if(hasChildren){return getChildren(path,args.child_limit,args.child_skip)}}).then(children=>{info.children=children;return info})}}}export(path,stream,options={format:"json"}){return this.storage.exportNode(path,stream,options)}}module.exports={LocalApi:LocalApi}},{"./acebase-local":31,"./data-index":41,"./node":37,"./storage":40,"./storage-acebase":41,"./storage-custom":38,"./storage-localstorage":39,"./storage-mssql":41,"./storage-sqlite":41,"acebase-core":11}],33:[function(require,module,exports){const{DataReference:DataReference,DataSnapshot:DataSnapshot,EventSubscription:EventSubscription,PathReference:PathReference,TypeMappings:TypeMappings,TypeMappingOptions:TypeMappingOptions}=require("acebase-core");const{AceBaseLocalSettings:AceBaseLocalSettings}=require("./acebase-local");const{BrowserAceBase:BrowserAceBase}=require("./acebase-browser");const{LocalStorageSettings:LocalStorageSettings}=require("./storage-localstorage");const{CustomStorageSettings:CustomStorageSettings,CustomStorageHelpers:CustomStorageHelpers}=require("./storage-custom");const acebase={AceBase:BrowserAceBase,AceBaseLocalSettings:AceBaseLocalSettings,DataReference:DataReference,DataSnapshot:DataSnapshot,EventSubscription:EventSubscription,PathReference:PathReference,TypeMappings:TypeMappings,TypeMappingOptions:TypeMappingOptions,LocalStorageSettings:LocalStorageSettings,CustomStorageSettings:CustomStorageSettings,CustomStorageHelpers:CustomStorageHelpers};window.acebase=acebase;window.AceBase=BrowserAceBase;module.exports=acebase},{"./acebase-browser":30,"./acebase-local":31,"./storage-custom":38,"./storage-localstorage":39,"acebase-core":11}],34:[function(require,module,exports){const{VALUE_TYPES:VALUE_TYPES,getValueTypeName:getValueTypeName}=require("./node-value-types");const{PathInfo:PathInfo}=require("acebase-core");class NodeInfo{constructor(info){this.path=info.path;this.type=info.type;this.index=info.index;this.key=info.key;this.exists=info.exists;this.address=info.address;this.value=info.value;if(typeof this.path==="string"&&(typeof this.key==="undefined"&&typeof this.index==="undefined")){let pathInfo=PathInfo.get(this.path);if(typeof pathInfo.key==="number"){this.index=pathInfo.key}else{this.key=pathInfo.key}}if(typeof this.exists==="undefined"){this.exists=true}}get valueType(){return this.type}get valueTypeName(){return getValueTypeName(this.valueType)}toString(){if(!this.exists){return`"${this.path}" doesn't exist`}if(this.address){return`"${this.path}" is ${this.valueTypeName} stored at ${this.address.pageNr},${this.address.recordNr}`}else{return`"${this.path}" is ${this.valueTypeName} with value ${this.value}`}}}module.exports={NodeInfo:NodeInfo}},{"./node-value-types":36,"acebase-core":11}],35:[function(require,module,exports){const{PathInfo:PathInfo}=require("acebase-core");const SECOND=1e3;const MINUTE=6e4;const DEBUG_MODE=false;const LOCK_TIMEOUT=DEBUG_MODE?15*MINUTE:90*SECOND;const LOCK_STATE={PENDING:"pending",LOCKED:"locked",EXPIRED:"expired",DONE:"done"};class NodeLocker{constructor(){this._locks=[]}_allowLock(path,tid,forWriting){const pathInfo=PathInfo.get(path);const existing=this._locks.find(otherLock=>otherLock.tid===tid&&otherLock.state===LOCK_STATE.LOCKED&&(otherLock.path===path||pathInfo.isDescendantOf(otherLock.path))&&(otherLock.forWriting||!forWriting));if(typeof existing==="object"){return{allow:true}}const conflict=this._locks.filter(otherLock=>otherLock.tid!==tid&&otherLock.state===LOCK_STATE.LOCKED).find(otherLock=>{return(forWriting||otherLock.forWriting)&&(path===otherLock.path||pathInfo.isDescendantOf(otherLock.path))});const clashes=typeof conflict!=="undefined";return{allow:!clashes,conflict:conflict}}_processLockQueue(){const pending=this._locks.filter(lock=>lock.state===LOCK_STATE.PENDING&&(lock.waitingFor===null||lock.waitingFor.state!==LOCK_STATE.LOCKED)).sort((a,b)=>{if(a.priority&&!b.priority){return-1}else if(!a.priority&&b.priority){return 1}return a.requested{const check=this._allowLock(lock.path,lock.tid,lock.forWriting);lock.waitingFor=check.conflict||null;if(check.allow){this.lock(lock).then(lock.resolve).catch(lock.reject)}})}lock(path,tid,forWriting=true,comment="",options={withPriority:false,noTimeout:false}){let lock,proceed;if(path instanceof NodeLock){lock=path;lock.comment=`(retry: ${lock.comment})`;proceed=true}else if(this._locks.findIndex(l=>l.tid===tid&&l.state===LOCK_STATE.EXPIRED)>=0){return Promise.reject(new Error(`lock on tid ${tid} has expired, not allowed to continue`))}else{lock=new NodeLock(this,path,tid,forWriting,options.withPriority===true);lock.comment=comment;this._locks.push(lock);const check=this._allowLock(path,tid,forWriting);lock.waitingFor=check.conflict||null;proceed=check.allow}if(proceed){lock.state=LOCK_STATE.LOCKED;if(typeof lock.granted==="number"){}else{lock.granted=Date.now();if(options.noTimeout!==true){lock.expires=Date.now()+LOCK_TIMEOUT;lock.timeout=setTimeout(()=>{if(lock.state!==LOCK_STATE.LOCKED){return}console.error(`lock :: ${lock.forWriting?"write":"read"} lock on path "/${lock.path}" by tid ${lock.tid} took too long, ${lock.comment}`);lock.state=LOCK_STATE.EXPIRED;this._processLockQueue()},LOCK_TIMEOUT)}}return Promise.resolve(lock)}else{console.assert(lock.state===LOCK_STATE.PENDING);const p=new Promise((resolve,reject)=>{lock.resolve=resolve;lock.reject=reject});return p}}unlock(lockOrId,comment,processQueue=true){let lock,i;if(lockOrId instanceof NodeLock){lock=lockOrId;i=this._locks.indexOf(lock)}else{let id=lockOrId;i=this._locks.findIndex(l=>l.id===id);lock=this._locks[i]}if(i<0){const msg=`lock on "/${lock.path}" for tid ${lock.tid} wasn't found; ${comment}`;return Promise.reject(new Error(msg))}lock.state=LOCK_STATE.DONE;clearTimeout(lock.timeout);this._locks.splice(i,1);processQueue&&this._processLockQueue();return Promise.resolve(lock)}list(){return this._locks||[]}isAllowed(path,tid,forWriting){return this._allowLock(path,tid,forWriting).allow}}let lastid=0;class NodeLock{static get LOCK_STATE(){return LOCK_STATE}constructor(locker,path,tid,forWriting,priority=false){this.locker=locker;this.path=path;this.tid=tid;this.forWriting=forWriting;this.priority=priority;this.state=LOCK_STATE.PENDING;this.requested=Date.now();this.granted=undefined;this.expires=undefined;this.comment="";this.waitingFor=null;this.id=++lastid}release(comment){return this.locker.unlock(this,comment||this.comment)}moveToParent(){const parentPath=PathInfo.get(this.path).parentPath;const allowed=this.locker.isAllowed(parentPath,this.tid,this.forWriting);if(allowed){this.waitingFor=null;this.path=parentPath;this.comment=`moved to parent: ${this.comment}`;return Promise.resolve(this)}else{this.locker.unlock(this,`moveLockToParent: ${this.comment}`,false);return this.locker.lock(parentPath,this.tid,this.forWriting,`moved to parent (queued): ${this.comment}`,{withPriority:true}).then(newLock=>{return newLock})}}moveTo(otherPath,forWriting){const allowed=this.locker.isAllowed(otherPath,this.tid,forWriting);if(allowed){this.waitingFor=null;this.path=otherPath;this.forWriting=forWriting;this.comment=`moved to "/${otherPath}": ${this.comment}`;return Promise.resolve(this)}else{this.locker.unlock(this,`moving to "/${otherPath}": ${this.comment}`,false);return this.locker.lock(otherPath,this.tid,forWriting,`moved to "/${otherPath}" (queued): ${this.comment}`,{withPriority:true}).then(newLock=>{return newLock})}}}module.exports={NodeLocker:NodeLocker,NodeLock:NodeLock}},{"acebase-core":11}],36:[function(require,module,exports){const VALUE_TYPES={OBJECT:1,ARRAY:2,NUMBER:3,BOOLEAN:4,STRING:5,DATETIME:6,BINARY:8,REFERENCE:9};function getValueTypeName(valueType){switch(valueType){case VALUE_TYPES.ARRAY:return"array";case VALUE_TYPES.BINARY:return"binary";case VALUE_TYPES.BOOLEAN:return"boolean";case VALUE_TYPES.DATETIME:return"date";case VALUE_TYPES.NUMBER:return"number";case VALUE_TYPES.OBJECT:return"object";case VALUE_TYPES.REFERENCE:return"reference";case VALUE_TYPES.STRING:return"string";default:"unknown"}}module.exports={VALUE_TYPES:VALUE_TYPES,getValueTypeName:getValueTypeName}},{}],37:[function(require,module,exports){const{Storage:Storage}=require("./storage");const{NodeInfo:NodeInfo}=require("./node-info");const{VALUE_TYPES:VALUE_TYPES,getValueTypeName:getValueTypeName}=require("./node-value-types");const colors=require("colors");class Node{static get VALUE_TYPES(){return VALUE_TYPES}static getInfo(storage,path,options={no_cache:false}){if(options&&!options.no_cache){let cachedInfo=storage.nodeCache.find(path);if(cachedInfo){return Promise.resolve(cachedInfo)}}return storage.getNodeInfo(path).then(info=>{if(options&&!options.no_cache){storage.nodeCache.update(info)}return info})}static update(storage,path,value,options={merge:true}){if(options.merge){return storage.updateNode(path,value)}else{return storage.setNode(path,value)}}static exists(storage,path){return storage.getNodeInfo(path).then(nodeInfo=>{return nodeInfo.exists})}static getValue(storage,path,options={include:undefined,exclude:undefined,child_objects:true}){if(!options){options={}}if(typeof options.include!=="undefined"&&!(options.include instanceof Array)){throw new TypeError(`options.include must be an array of key names`)}if(typeof options.exclude!=="undefined"&&!(options.exclude instanceof Array)){throw new TypeError(`options.exclude must be an array of key names`)}if(["undefined","boolean"].indexOf(typeof options.child_objects)<0){throw new TypeError(`options.child_objects must be a boolean`)}return storage.getNodeValue(path,options)}static getChildInfo(storage,path,childKeyOrIndex){let childInfo;return storage.getChildren(path,{keyFilter:[childKeyOrIndex]}).next(info=>{childInfo=info}).then(()=>{return childInfo})}static getChildren(storage,path,keyFilter=undefined){return storage.getChildren(path,{keyFilter:keyFilter})}static remove(storage,path){return storage.removeNode(path)}static set(storage,path,value){return Node.update(storage,path,value,{merge:false})}static transaction(storage,path,callback){return storage.transactNode(path,callback)}static matches(storage,path,criteria,options){return storage.matchNode(path,criteria,options)}}class NodeChange{static get CHANGE_TYPE(){return{UPDATE:"update",DELETE:"delete",INSERT:"insert"}}constructor(keyOrIndex,changeType,oldValue,newValue){this.keyOrIndex=keyOrIndex;this.changeType=changeType;this.oldValue=oldValue;this.newValue=newValue}}class NodeChangeTracker{constructor(path){this.path=path;this._changes=[];this._oldValue=undefined;this._newValue=undefined}addDelete(keyOrIndex,oldValue){this._changes.push(new NodeChange(keyOrIndex,NodeChange.CHANGE_TYPE.DELETE,oldValue,null))}addUpdate(keyOrIndex,oldValue,newValue){this._changes.push(new NodeChange(keyOrIndex,NodeChange.CHANGE_TYPE.UPDATE,oldValue,newValue))}addInsert(keyOrIndex,newValue){this._changes.push(new NodeChange(keyOrIndex,NodeChange.CHANGE_TYPE.INSERT,null,newValue))}add(keyOrIndex,currentValue,newValue){if(currentValue===null){if(newValue===null){throw new Error(`Wrong logic for node change on "${this.nodeInfo.path}/${keyOrIndex}" - both old and new values are null`)}this.addInsert(keyOrIndex,newValue)}else if(newValue===null){this.addDelete(keyOrIndex,currentValue)}else{this.addUpdate(keyOrIndex,currentValue,newValue)}}get updates(){return this._changes.filter(change=>change.changeType===NodeChange.CHANGE_TYPE.UPDATE)}get deletes(){return this._changes.filter(change=>change.changeType===NodeChange.CHANGE_TYPE.DELETE)}get inserts(){return this._changes.filter(change=>change.changeType===NodeChange.CHANGE_TYPE.INSERT)}get all(){return this._changes}get totalChanges(){return this._changes.length}get(keyOrIndex){return this._changes.find(change=>change.keyOrIndex===keyOrIndex)}hasChanged(keyOrIndex){return!!this.get(keyOrIndex)}get newValue(){if(typeof this._newValue==="object"){return this._newValue}if(typeof this._oldValue==="undefined"){throw new TypeError(`oldValue is not set`)}let newValue={};Object.keys(this.oldValue).forEach(key=>newValue[key]=oldValue[key]);this.deletes.forEach(change=>delete newValue[change.key]);this.updates.forEach(change=>newValue[change.key]=change.newValue);this.inserts.forEach(change=>newValue[change.key]=change.newValue);return newValue}set newValue(value){this._newValue=value}get oldValue(){if(typeof this._oldValue==="object"){return this._oldValue}if(typeof this._newValue==="undefined"){throw new TypeError(`newValue is not set`)}let oldValue={};Object.keys(this.newValue).forEach(key=>oldValue[key]=newValue[key]);this.deletes.forEach(change=>oldValue[change.key]=change.oldValue);this.updates.forEach(change=>oldValue[change.key]=change.oldValue);this.inserts.forEach(change=>delete oldValue[change.key]);return oldValue}set oldValue(value){this._oldValue=value}get typeChanged(){return typeof this.oldValue!==typeof this.newValue||this.oldValue instanceof Array&&!(this.newValue instanceof Array)||this.newValue instanceof Array&&!(this.oldValue instanceof Array)}static create(path,oldValue,newValue){const changes=new NodeChangeTracker(path);changes.oldValue=oldValue;changes.newValue=newValue;typeof oldValue==="object"&&Object.keys(oldValue).forEach(key=>{if(typeof newValue==="object"&&key in newValue&&newValue!==null){changes.add(key,oldValue[key],newValue[key])}else{changes.add(key,oldValue[key],null)}});typeof newValue==="object"&&Object.keys(newValue).forEach(key=>{if(typeof oldValue!=="object"||!(key in oldValue)||oldValue[key]===null){changes.add(key,null,newValue[key])}});return changes}}module.exports={Node:Node,NodeInfo:NodeInfo}},{"./node-info":34,"./node-value-types":36,"./storage":40,colors:22}],38:[function(require,module,exports){const{debug:debug,ID:ID,PathReference:PathReference,PathInfo:PathInfo,ascii85:ascii85}=require("acebase-core");const{NodeInfo:NodeInfo}=require("./node-info");const{VALUE_TYPES:VALUE_TYPES}=require("./node-value-types");const{Storage:Storage,StorageSettings:StorageSettings,NodeNotFoundError:NodeNotFoundError}=require("./storage");class ICustomStorageNodeMetaData{constructor(){this.revision="";this.revision_nr=0;this.created=0;this.modified=0;this.type=0}}class ICustomStorageNode extends ICustomStorageNodeMetaData{constructor(){super();this.value=null}}class CustomStorageTransaction{get(path){throw new Error(`CustomStorageTransaction.get must be overridden by subclass`)}set(path,node){throw new Error(`CustomStorageTransaction.set must be overridden by subclass`)}remove(path){throw new Error(`CustomStorageTransaction.remove must be overridden by subclass`)}rollback(reason){throw new Error(`CustomStorageTransaction.rollback must be overridden by subclass`)}commit(){throw new Error(`CustomStorageTransaction.rollback must be overridden by subclass`)}constructor(){this.id=ID.generate()}}class CustomStorageSettings extends StorageSettings{constructor(settings){super(settings);settings=settings||{};if(typeof settings.ready!=="function"){throw new Error(`ready must be a function`)}if(typeof settings.get!=="function"){throw new Error(`get must be a function`)}if(typeof settings.set!=="function"){throw new Error(`set must be a function`)}if(typeof settings.remove!=="function"){throw new Error(`remove must be a function`)}if(typeof settings.childrenOf!=="function"){throw new Error(`childrenOf must be a function`)}if(typeof settings.descendantsOf!=="function"){throw new Error(`descendantsOf must be a function`)}this.name=settings.name;this.info=`${this.name||"CustomStorage"} realtime database`;this.ready=settings.ready;this.get=settings.get;this.getMultiple=settings.getMultiple||(paths=>{const map=new Map;return Promise.all(paths.map(path=>this.get(path).then(val=>map.set(path,val)))).then(done=>map)});this.set=settings.set;this.remove=settings.remove;this.removeMultiple=settings.removeMultiple||(paths=>{return Promise.all(paths.map(path=>this.remove(path))).then(done=>true)});this.childrenOf=settings.childrenOf;this.descendantsOf=settings.descendantsOf}}class CustomStorageNodeAddress{constructor(containerPath){this.path=containerPath}}class CustomStorageNodeInfo extends NodeInfo{constructor(info){super(info);this.address;this.revision=info.revision;this.revision_nr=info.revision_nr;this.created=info.created;this.modified=info.modified}}class CustomStorageHelpers{static ChildPathsSql(path,columnName="path"){const where=path===""?`${columnName} <> '' AND ${columnName} NOT LIKE '%/%'`:`(${columnName} LIKE '${path}/%' OR ${columnName} LIKE '${path}[%') AND ${columnName} NOT LIKE '${path}/%/%' AND ${columnName} NOT LIKE '${path}[%]/%' AND ${columnName} NOT LIKE '${path}[%][%'`;return where}static ChildPathsRegex(path){return new RegExp(`^${path}(?:/[^/[]+|[[0-9]+])$`)}static DescendantPathsSql(path,columnName="path"){const where=path===""?`${columnName} <> ''`:`${columnName} LIKE '${path}/%' OR ${columnName} LIKE '${path}[%'`;return where}static DescendantPathsRegex(path){return new RegExp(`^${path}(?:/[^/[]+|[[0-9]+])`)}static get PathInfo(){return PathInfo}}class CustomStorage extends Storage{constructor(dbname,settings){super(dbname,settings);this._init()}_init(){this._customImplementation=this.settings;this.debug.log(`Database "${this.name}" details:`.intro);this.debug.log(`- Type: CustomStorage`);this.debug.log(`- Path: ${this.settings.path}`);this.debug.log(`- Max inline value size: ${this.settings.maxInlineValueSize}`.intro);this.debug.log(`- Autoremove undefined props: ${this.settings.removeVoidProperties}`);return this._customImplementation.ready().then(ready=>this.getNodeInfo("")).then(info=>{if(!info.exists){return this._writeNode("",{})}}).then(()=>{return this.indexes.supported&&this.indexes.load()}).then(()=>{this.emit("ready")})}_storeNode(path,info){const getTypedChildValue=val=>{if(val===null){throw new Error(`Not allowed to store null values. remove the property`)}else if(["string","number","boolean"].includes(typeof val)){return val}else if(val instanceof Date){return{type:VALUE_TYPES.DATETIME,value:val.getTime()}}else if(val instanceof PathReference){return{type:VALUE_TYPES.REFERENCE,value:child.path}}else if(val instanceof ArrayBuffer){return{type:VALUE_TYPES.BINARY,value:ascii85.encode(val)}}else if(typeof val==="object"){console.assert(Object.keys(val).length===0,"child object stored in parent can only be empty");return val}};const unprocessed=`Caller should have pre-processed the value by converting it to a string`;if(info.type===VALUE_TYPES.ARRAY&&info.value instanceof Array){console.warn(`Unprocessed array. ${unprocessed}`);const obj={};for(let i=0;i{info.value[key]=getTypedChildValue(original[key])})}return this._customImplementation.set(path,info)}_processReadNodeValue(node){const getTypedChildValue=val=>{if(val.type===VALUE_TYPES.BINARY){return ascii85.decode(val.value)}else if(val.type===VALUE_TYPES.DATETIME){return new Date(val.value)}else if(val.type===VALUE_TYPES.REFERENCE){return new PathReference(val.value)}else{throw new Error(`Unhandled child value type ${val.type}`)}};switch(node.type){case VALUE_TYPES.ARRAY:case VALUE_TYPES.OBJECT:{const obj=node.value;Object.keys(obj).forEach(key=>{let item=obj[key];if(typeof item==="object"&&"type"in item){obj[key]=getTypedChildValue(item)}});node.value=obj;break}case VALUE_TYPES.BINARY:{node.value=ascii85.decode(node.value);break}case VALUE_TYPES.REFERENCE:{node.value=new PathReference(node.value);break}case VALUE_TYPES.STRING:{break}default:throw new Error(`Invalid standalone record value type`)}}async _readNode(path){let node=await this._customImplementation.get(path);if(node===null){return null}if(typeof node!=="object"){throw new Error(`CustomStorage get function must return an ICustomStorageNode object. Use JSON.parse if your set function stored it as a string`)}this._processReadNodeValue(node);return node}_getTypeFromStoredValue(val){let type;if(typeof val==="string"){type=VALUE_TYPES.STRING}else if(typeof val==="number"){type=VALUE_TYPES.NUMBER}else if(typeof val==="boolean"){type=VALUE_TYPES.BOOLEAN}else if(val instanceof Array){type=VALUE_TYPES.ARRAY}else if(typeof val==="object"){if("type"in val){type=val.type;val=val.value;if(type===VALUE_TYPES.DATETIME){val=new Date(val)}else if(type===VALUE_TYPES.REFERENCE){val=new PathReference(val)}}else{type=VALUE_TYPES.OBJECT}}else{throw new Error(`Unknown value type`)}return{type:type,value:val}}async _writeNode(path,value,options={merge:false,revision:null,transaction:null}){if(this.valueFitsInline(value)&&path!==""){throw new Error(`invalid value to store in its own node`)}else if(path===""&&(typeof value!=="object"||value instanceof Array)){throw new Error(`Invalid root node value. Must be an object`)}const currentRow=await this._readNode(path);const newRevision=options&&options.revision||ID.generate();let mainNode={type:VALUE_TYPES.OBJECT,value:{}};const childNodeValues={};if(value instanceof Array){mainNode.type=VALUE_TYPES.ARRAY;const obj={};for(let i=0;i{const val=value[key];delete mainNode.value[key];if(val===null){return}else if(typeof val==="undefined"){if(this.settings.removeVoidProperties===true){delete value[key];return}else{throw new Error(`Property "${key}" has invalid value. Cannot store undefined values. Set removeVoidProperties option to true to automatically remove undefined properties`)}}if(this.valueFitsInline(val)){mainNode.value[key]=val}else{childNodeValues[key]=val}})}if(currentRow){this.debug.log(`Node "/${path}" is being ${options.merge?"updated":"overwritten"}`.cyan);if(currentIsObjectOrArray||newIsObjectOrArray){const pathInfo=PathInfo.get(path);const keys=[];let checkExecuted=false;const includeChildCheck=childPath=>{checkExecuted=true;if(!pathInfo.isParentOf(childPath)){throw new Error(`"${childPath}" is not a child of "${path}" - childrenOf must only check and return paths that are children`)}return true};const addChildPath=childPath=>{if(!checkExecuted){throw new Error(`${this._customImplementation.info} childrenOf did not call checkCallback before addCallback`)}const key=PathInfo.get(childPath).key;keys.push(key);return true};await this._customImplementation.childrenOf(path,{metadata:false,value:false},includeChildCheck,addChildPath);children.current=children.current.concat(keys);if(newIsObjectOrArray){if(options&&options.merge){children.new=children.current.slice()}Object.keys(value).forEach(key=>{if(!children.new.includes(key)){children.new.push(key)}})}const changes={insert:children.new.filter(key=>!children.current.includes(key)),update:children.new.filter(key=>children.current.includes(key)),delete:options&&options.merge?Object.keys(value).filter(key=>value[key]===null):children.current.filter(key=>!children.new.includes(key))};const writePromises=Object.keys(childNodeValues).map(key=>{const childPath=pathInfo.childPath(key);const childValue=childNodeValues[key];return this._writeNode(childPath,childValue,{revision:newRevision,merge:false})});const movingNodes=keys.filter(key=>key in mainNode.value);const deleteDedicatedKeys=changes.delete.concat(movingNodes);const deletePromises=deleteDedicatedKeys.map(key=>{const childPath=pathInfo.childPath(key);return this._deleteNode(childPath)});const promises=writePromises.concat(deletePromises);await Promise.all(promises)}return await this._storeNode(path,{type:mainNode.type,value:mainNode.value,revision:currentRow.revision,revision_nr:currentRow.revision_nr+1,created:currentRow.created,modified:Date.now()})}else{this.debug.log(`Node "/${path}" is being created`.cyan);const promises=Object.keys(childNodeValues).map(key=>{const childPath=PathInfo.getChildPath(path,key);const childValue=childNodeValues[key];return this._writeNode(childPath,childValue,{revision:newRevision,merge:false})});const p=this._storeNode(path,{type:mainNode.type,value:mainNode.value,revision:newRevision,revision_nr:1,created:Date.now(),modified:Date.now()});promises.push(p);return Promise.all(promises)}}async _deleteNode(path){const pathInfo=PathInfo.get(path);this.debug.log(`Node "/${path}" is being deleted`.cyan);const deletePaths=[path];let checkExecuted=false;const includeDescendantCheck=descPath=>{checkExecuted=true;if(!pathInfo.isAncestorOf(descPath)){throw new Error(`"${descPath}" is not a descendant of "${path}" - descendantsOf must only check and return paths that are descendants`)}return true};const addDescendant=descPath=>{if(!checkExecuted){throw new Error(`${this._customImplementation.info} descendantsOf did not call checkCallback before addCallback`)}deletePaths.push(descPath);return true};await this._customImplementation.descendantsOf(path,{metadata:false,value:false},includeDescendantCheck,addDescendant);this.debug.log(`Nodes ${deletePaths.map(p=>`"/${p}"`).join(",")} are being deleted`.cyan);return this._customImplementation.removeMultiple(deletePaths)}getChildren(path,options={keyFilter:undefined,tid:undefined}){var callback;const generator={next(valueCallback){callback=valueCallback;return start()}};const start=()=>{let lock,canceled=false;const tid=options&&options.tid||ID.generate();return this.nodeLocker.lock(path,tid,false,"getChildren").then(async l=>{lock=l;let node=await this._readNode(path);if(!node){throw new NodeNotFoundError(`Node "/${path}" does not exist`)}if(![VALUE_TYPES.OBJECT,VALUE_TYPES.ARRAY].includes(node.type)){return}const isArray=node.type===VALUE_TYPES.ARRAY;const value=node.value;let keys=Object.keys(value);if(options.keyFilter){keys=keys.filter(key=>options.keyFilter.includes(key))}const pathInfo=PathInfo.get(path);keys.length>0&&keys.every(key=>{let child=this._getTypeFromStoredValue(value[key]);const info=new CustomStorageNodeInfo({path:pathInfo.childPath(key),key:isArray?null:key,index:isArray?key:null,type:child.type,address:null,exists:true,value:child.value,revision:node.revision,revision_nr:node.revision_nr,created:node.created,modified:node.modified});canceled=callback(info)===false;return!canceled});if(canceled){return}let checkExecuted=false;const includeChildCheck=childPath=>{checkExecuted=true;if(!pathInfo.isParentOf(childPath)){throw new Error(`"${childPath}" is not a child of "${path}" - childrenOf must only check and return paths that are children`)}if(options.keyFilter){const key=PathInfo.get(childPath).key;return options.keyFilter.includes(key)}return true};const addChildNode=(childPath,node)=>{if(!checkExecuted){throw new Error(`${this._customImplementation.info} childrenOf did not call checkCallback before addCallback`)}const key=PathInfo.get(childPath).key;const info=new CustomStorageNodeInfo({path:childPath,type:node.type,key:isArray?null:key,index:isArray?key:null,address:new CustomStorageNodeAddress(childPath),exists:true,value:null,revision:node.revision,revision_nr:node.revision_nr,created:new Date(node.created),modified:new Date(node.modified)});canceled=callback(info)===false;return!canceled};return this._customImplementation.childrenOf(path,{metadata:true,value:false},includeChildCheck,addChildNode)}).then(()=>{lock.release();return canceled}).catch(err=>{lock.release();throw err})};return generator}getNode(path,options={include:undefined,exclude:undefined,child_objects:true,tid:undefined}){const tid=options&&options.tid||ID.generate();let lock;return this.nodeLocker.lock(path,tid,false,"getNode").then(async l=>{lock=l;const filtered=options&&(options.include||options.exclude||options.child_objects===false);const pathInfo=PathInfo.get(path);const targetNode=await this._readNode(path);if(!targetNode){if(path===""){return{value:null}}return lock.moveToParent().then(async parentLock=>{lock=parentLock;let parentNode=await this._readNode(pathInfo.parentPath);if(parentNode&&[VALUE_TYPES.OBJECT,VALUE_TYPES.ARRAY].includes(parentNode.type)&&pathInfo.key in parentNode){const childValueInfo=this._getTypeFromStoredValue(parentNode.value[pathInfo.key]);return{revision:parentNode.revision,revision_nr:parentNode.revision_nr,created:parentNode.created,modified:parentNode.modified,type:childValueInfo.type,value:childValueInfo.value}}return{value:null}})}const includeCheck=options.include?new RegExp("^"+options.include.map(p=>"(?:"+p.replace(/\*/g,"[^/\\[]+")+")").join("|")+"(?:$|[/\\[])"):null;const excludeCheck=options.exclude?new RegExp("^"+options.exclude.map(p=>"(?:"+p.replace(/\*/g,"[^/\\[]+")+")").join("|")+"(?:$|[/\\[])"):null;let checkExecuted=false;const includeDescendantCheck=descPath=>{checkExecuted=true;if(!pathInfo.isAncestorOf(descPath)){throw new Error(`"${descPath}" is not a descendant of "${path}" - descendantsOf must only check and return paths that are descendants`)}if(!filtered){return true}let checkPath=descPath.slice(path.length);if(checkPath[0]==="/"){checkPath=checkPath.slice(1)}let include=(includeCheck?includeCheck.test(checkPath):true)&&(excludeCheck?!excludeCheck.test(checkPath):true);if(include&&options.child_objects===false&&(pathInfo.isParentOf(descPath)&&[VALUE_TYPES.OBJECT,VALUE_TYPES.ARRAY].includes(childNode.type)||PathInfo.getPathKeys(descPath).length>pathInfo.pathKeys.length+1)){include=false}return include};const descRows=[];const addDescendant=(descPath,node)=>{if(!checkExecuted){throw new Error(`${this._customImplementation.info} descendantsOf did not call checkCallback before addCallback`)}this._processReadNodeValue(node);node.path=descPath;descRows.push(node);return true};await this._customImplementation.descendantsOf(path,{metadata:true,value:true},includeDescendantCheck,addDescendant);this.debug.log(`Read node "/${path}" and ${filtered?"(filtered) ":""}children from ${descRows.length+1} records`.magenta);const result=targetNode;const objectToArray=obj=>{const arr=[];Object.keys(obj).forEach(key=>{let index=parseInt(key);arr[index]=obj[index]});return arr};if(targetNode.type===VALUE_TYPES.ARRAY){result.value=objectToArray(result.value)}if(targetNode.type===VALUE_TYPES.OBJECT||targetNode.type===VALUE_TYPES.ARRAY){const targetPathKeys=PathInfo.getPathKeys(path);let value=targetNode.value;for(let i=0;i{console.assert(!(childKey in parent[key]),"child key is in parent value already?! HOW?!");parent[key][childKey]=nodeValue[childKey]})}else{parent[key]=nodeValue}parent=parent[key]}}}else if(descRows.length>0){throw new Error(`multiple records found for non-object value!`)}if(options.child_objects===false){Object.keys(result.value).forEach(key=>{if(typeof result.value[key]==="object"&&result.value[key].constructor===Object){console.assert(Object.keys(result.value[key]).length===0);delete result.value[key]}})}if(options.exclude){const process=(obj,keys)=>{if(typeof obj!=="object"){return}const key=keys[0];if(key==="*"){Object.keys(obj).forEach(k=>{process(obj[k],keys.slice(1))})}else if(keys.length>1){key in obj&&process(obj[key],keys.slice(1))}else{delete obj[key]}};options.exclude.forEach(path=>{const checkKeys=PathInfo.getPathKeys(path);process(result.value,checkKeys)})}return result}).then(result=>{lock.release();return result}).catch(err=>{lock.release();throw err})}getNodeInfo(path,options={tid:undefined}){const pathInfo=PathInfo.get(path);const tid=options&&options.tid||ID.generate();let lock;return this.nodeLocker.lock(path,tid,false,"getNodeInfo").then(async l=>{lock=l;const node=await this._readNode(path);const info=new CustomStorageNodeInfo({path:path,key:typeof pathInfo.key==="string"?pathInfo.key:null,index:typeof pathInfo.key==="number"?pathInfo.key:null,type:node?node.type:0,exists:node!==null,address:node?new CustomStorageNodeAddress(path):null,created:node?new Date(node.created):null,modified:node?new Date(node.modified):null,revision:node?node.revision:null,revision_nr:node?node.revision_nr:null});if(node||path===""){return info}return lock.moveToParent().then(async parentLock=>{lock=parentLock;const parent=await this._readNode(pathInfo.parentPath);if(parent&&[VALUE_TYPES.OBJECT,VALUE_TYPES.ARRAY].includes(parent.type)&&pathInfo.key in parent.value){info.exists=true;info.value=parent.value[pathInfo.key];info.address=null;info.type=parent.type;info.created=new Date(parent.created);info.modified=new Date(parent.modified);info.revision=parent.revision;info.revision_nr=parent.revision_nr}else{info.address=null}return info})}).then(info=>{lock.release();return info}).catch(err=>{lock&&lock.release();throw err})}removeNode(path,options={tid:undefined}){if(path===""){return Promise.reject(new Error(`Cannot remove the root node`))}const pathInfo=PathInfo.get(path);const tid=options&&options.tid||ID.generate();return this.nodeLocker.lock(pathInfo.parentPath,tid,true,"removeNode").then(lock=>{return this.updateNode(pathInfo.parentPath,{[pathInfo.key]:null},{tid:tid}).then(result=>{lock.release();return result}).catch(err=>{lock.release();throw err})})}setNode(path,value,options={assert_revision:undefined,tid:undefined}){const pathInfo=PathInfo.get(path);let lock;const tid=options&&options.tid||ID.generate();return this.nodeLocker.lock(path,tid,true,"setNode").then(l=>{lock=l;if(path===""){if(value===null||typeof value!=="object"||value instanceof Array||value instanceof ArrayBuffer||"buffer"in value&&value.buffer instanceof ArrayBuffer){return Promise.reject(new Error(`Invalid value for root node: ${value}`))}return this._writeNodeWithTracking("",value,{merge:false,tid:tid})}if(options&&typeof options.assert_revision!=="undefined"){return this.getNodeInfo(path,{tid:lock.tid}).then(info=>{if(info.revision!==options.assert_revision){throw new NodeRevisionError(`revision '${info.revision}' does not match requested revision '${options.assert_revision}'`)}if(info.address&&info.address.path===path&&!this.valueFitsInline(value)){return this._writeNodeWithTracking(path,value,{merge:false,tid:tid})}else{return lock.moveToParent().then(parentLock=>{lock=parentLock;return this._writeNodeWithTracking(pathInfo.parentPath,{[pathInfo.key]:value},{merge:true,tid:tid})})}})}else{return lock.moveToParent().then(parentLock=>{lock=parentLock;return this.updateNode(pathInfo.parentPath,{[pathInfo.key]:value},{tid:tid})})}}).then(result=>{lock.release();return result}).catch(err=>{lock.release();throw err})}updateNode(path,updates,options={tid:undefined}){if(typeof updates!=="object"){return Promise.reject(new Error(`invalid updates argument`))}const tid=options&&options.tid||ID.generate();let lock;return this.nodeLocker.lock(path,tid,true,"updateNode").then(l=>{lock=l;return this.getNodeInfo(path,{tid:lock.tid})}).then(nodeInfo=>{const pathInfo=PathInfo.get(path);if(nodeInfo.exists&&nodeInfo.address&&nodeInfo.address.path===path){return this._writeNodeWithTracking(path,updates,{merge:true,tid:tid})}else if(nodeInfo.exists){const pathInfo=PathInfo.get(path);return lock.moveToParent().then(parentLock=>{lock=parentLock;return this._writeNodeWithTracking(pathInfo.parentPath,{[pathInfo.key]:value},{merge:true,tid:tid})})}else{return lock.moveToParent().then(parentLock=>{lock=parentLock;return this.updateNode(pathInfo.parentPath,{[pathInfo.key]:updates},{tid:tid})})}}).then(result=>{lock.release();return result}).catch(err=>{lock.release();throw err})}}module.exports={CustomStorageNodeAddress:CustomStorageNodeAddress,CustomStorageNodeInfo:CustomStorageNodeInfo,CustomStorage:CustomStorage,CustomStorageSettings:CustomStorageSettings,CustomStorageHelpers:CustomStorageHelpers,ICustomStorageNodeMetaData:ICustomStorageNodeMetaData,ICustomStorageNode:ICustomStorageNode}},{"./node-info":34,"./node-value-types":36,"./storage":40,"acebase-core":11}],39:[function(require,module,exports){const{debug:debug,ID:ID,PathReference:PathReference,PathInfo:PathInfo,ascii85:ascii85}=require("acebase-core");const{NodeInfo:NodeInfo}=require("./node-info");const{VALUE_TYPES:VALUE_TYPES}=require("./node-value-types");const{Storage:Storage,StorageSettings:StorageSettings,NodeNotFoundError:NodeNotFoundError}=require("./storage");class LocalStorageSettings extends StorageSettings{constructor(settings){super(settings);settings=settings||{};this.session=settings.session===true;this.provider=typeof settings.provider==="object"?settings.provider:null}}class LocalStorageNodeAddress{constructor(containerPath){this.path=containerPath}}class LocalStorageNodeInfo extends NodeInfo{constructor(info){super(info);this.address;this.revision=info.revision;this.revision_nr=info.revision_nr;this.created=info.created;this.modified=info.modified}}class LocalStorage extends Storage{constructor(dbname,settings){super(dbname,settings);this._init()}_init(){if(this.settings.provider!==null&&typeof this.settings.provider==="object"){this._localStorage=this.settings.provider}else{if(!this.settings.session&&typeof localStorage==="undefined"){throw new Error(`No localStorage available. If you are on Node: npm i node-localstorage`)}if(this.settings.session===true&&typeof sessionStorage==="undefined"){throw new Error(`No sessionStorage available`)}this._localStorage=this.settings.session===true?sessionStorage:localStorage}this.debug.log(`Database "${this.name}" details:`.intro);this.debug.log(`- Type: LocalStorage`);this.debug.log(`- Max inline value size: ${this.settings.maxInlineValueSize}`.intro);return this.getNodeInfo("").then(info=>{if(!info.exists){return this._writeNode("",{})}}).then(()=>{return this.indexes.supported&&this.indexes.load()}).then(()=>{this.emit("ready")})}get _keyPrefix(){return`${this.name}.acebase::`}_getPathFromKey(key){return key.slice(this._keyPrefix.length)}_getKeyFromPath(path){return`${this._keyPrefix}${path}`}_storeNode(path,info){const getTypedChildValue=val=>{if(val===null){throw new Error(`Not allowed to store null values. remove the property`)}else if(["string","number","boolean"].includes(typeof val)){return val}else if(val instanceof Date){return{type:VALUE_TYPES.DATETIME,value:val.getTime()}}else if(val instanceof PathReference){return{type:VALUE_TYPES.REFERENCE,value:child.path}}else if(val instanceof ArrayBuffer){return{type:VALUE_TYPES.BINARY,value:ascii85.encode(val)}}else if(typeof val==="object"){console.assert(Object.keys(val).length===0,"child object stored in parent can only be empty");return val}};const unprocessed=`Caller should have pre-processed the value by converting it to a string`;if(info.type===VALUE_TYPES.ARRAY&&info.value instanceof Array){console.warn(`Unprocessed array. ${unprocessed}`);const obj={};for(let i=0;i{info.value[key]=getTypedChildValue(original[key])})}const json=JSON.stringify(info);this._localStorage.setItem(this._getKeyFromPath(path),json)}_readNode(path){let val=this._localStorage.getItem(this._getKeyFromPath(path));if(val===null){return null}val=JSON.parse(val);const getTypedChildValue=val=>{if(val.type===VALUE_TYPES.BINARY){return ascii85.decode(val.value)}else if(val.type===VALUE_TYPES.DATETIME){return new Date(val.value)}else if(val.type===VALUE_TYPES.REFERENCE){return new PathReference(val.value)}else{throw new Error(`Unhandled child value type ${val.type}`)}};const node={type:val.type,value:val.value,revision:val.revision,revision_nr:val.revision_nr,created:val.created,modified:val.modified};switch(val.type){case VALUE_TYPES.ARRAY:case VALUE_TYPES.OBJECT:{const obj=val.value;Object.keys(obj).forEach(key=>{let item=obj[key];if(typeof item==="object"&&"type"in item){obj[key]=getTypedChildValue(item)}});node.value=obj;break}case VALUE_TYPES.BINARY:{node.value=ascii85.decode(val.value);break}case VALUE_TYPES.STRING:{node.value=val.value;break}case VALUE_TYPES.REFERENCE:{node.value=new PathReference(val.value);break}default:throw new Error(`Invalid standalone record value type`)}return node}_getTypeFromStoredValue(val){let type;if(typeof val==="string"){type=VALUE_TYPES.STRING}else if(typeof val==="number"){type=VALUE_TYPES.NUMBER}else if(typeof val==="boolean"){type=VALUE_TYPES.BOOLEAN}else if(val instanceof Array){type=VALUE_TYPES.ARRAY}else if(typeof val==="object"){if("type"in val){type=val.type;val=val.value;if(type===VALUE_TYPES.DATETIME){val=new Date(val)}else if(type===VALUE_TYPES.REFERENCE){val=new PathReference(val)}}else{type=VALUE_TYPES.OBJECT}}else{throw new Error(`Unknown value type`)}return{type:type,value:val}}_writeNode(path,value,options={merge:false,revision:null}){if(this.valueFitsInline(value)&&path!==""){throw new Error(`invalid value to store in its own node`)}else if(path===""&&(typeof value!=="object"||value instanceof Array)){throw new Error(`Invalid root node value. Must be an object`)}const currentRow=this._readNode(path);const newRevision=options&&options.revision||ID.generate();let mainNode={type:VALUE_TYPES.OBJECT,value:{}};const childNodeValues={};if(value instanceof Array){mainNode.type=VALUE_TYPES.ARRAY;const obj={};for(let i=0;i{const val=value[key];delete mainNode.value[key];if(val===null){return}else if(typeof val==="undefined"){if(this.settings.removeVoidProperties===true){delete value[key];return}else{throw new Error(`Property "${key}" has invalid value. Cannot store undefined values. Set removeVoidProperties option to true to automatically remove undefined properties`)}}if(this.valueFitsInline(val)){mainNode.value[key]=val}else{childNodeValues[key]=val}})}if(currentRow){this.debug.log(`Node "/${path}" is being ${options.merge?"updated":"overwritten"}`.cyan);if(currentIsObjectOrArray||newIsObjectOrArray){const pathInfo=PathInfo.get(path);const keys=[];for(let i=0;i{if(!children.new.includes(key)){children.new.push(key)}})}const changes={insert:children.new.filter(key=>!children.current.includes(key)),update:children.new.filter(key=>children.current.includes(key)),delete:options&&options.merge?Object.keys(value).filter(key=>value[key]===null):children.current.filter(key=>!children.new.includes(key))};Object.keys(childNodeValues).map(key=>{const childPath=PathInfo.getChildPath(path,key);const childValue=childNodeValues[key];this._writeNode(childPath,childValue,{revision:newRevision,merge:false})});const movingNodes=keys.filter(key=>key in mainNode.value);const deleteDedicatedKeys=changes.delete.concat(movingNodes);deleteDedicatedKeys.forEach(key=>{const childPath=PathInfo.getChildPath(path,key);this._deleteNode(childPath)})}this._storeNode(path,{type:mainNode.type,value:mainNode.value,revision:currentRow.revision,revision_nr:currentRow.revision_nr+1,created:currentRow.created,modified:Date.now()})}else{this.debug.log(`Node "/${path}" is being created`.cyan);Object.keys(childNodeValues).map(key=>{const childPath=PathInfo.getChildPath(path,key);const childValue=childNodeValues[key];this._writeNode(childPath,childValue,{revision:newRevision,merge:false})});this._storeNode(path,{type:mainNode.type,value:mainNode.value,revision:newRevision,revision_nr:1,created:Date.now(),modified:Date.now()})}}_deleteNode(path){const pathInfo=PathInfo.get(path);this.debug.log(`Node "/${path}" is being deleted`.cyan);this._localStorage.removeItem(this._getKeyFromPath(path));for(let i=0;i{let lock,canceled=false;const tid=options&&options.tid||ID.generate();return this.nodeLocker.lock(path,tid,false,"getChildren").then(l=>{lock=l;let row=this._localStorage.getItem(this._getKeyFromPath(path));if(!row){throw new NodeNotFoundError(`Node "/${path}" does not exist`)}row=JSON.parse(row);if(![VALUE_TYPES.OBJECT,VALUE_TYPES.ARRAY].includes(row.type)){return}const isArray=row.type===VALUE_TYPES.ARRAY;const value=row.value;let keys=Object.keys(value);if(options.keyFilter){keys=keys.filter(key=>options.keyFilter.includes(key))}const pathInfo=PathInfo.get(path);keys.length>0&&keys.every(key=>{let child=this._getTypeFromStoredValue(value[key]);const info=new LocalStorageNodeInfo({path:pathInfo.childPath(key),key:isArray?null:key,index:isArray?key:null,type:child.type,address:null,exists:true,value:child.value,revision:row.revision,revision_nr:row.revision_nr,created:row.created,modified:row.modified});canceled=callback(info)===false;return!canceled});if(canceled){return}const childRows=[];for(let i=0;i{const row=childRows[i];if(!row){return}const key=PathInfo.get(row.path).key;if(options.keyFilter&&!options.keyFilter.includes(key)){return handleNextChild(i+1)}const info=new LocalStorageNodeInfo({path:row.path,type:row.type,key:isArray?null:key,index:isArray?key:null,address:new LocalStorageNodeAddress(row.path),exists:true,value:null,revision:row.revision,revision_nr:row.revision_nr,created:new Date(row.created),modified:new Date(row.modified)});canceled=callback(info)===false;if(!canceled){return handleNextChild(i+1)}};return handleNextChild(0)}).then(()=>{lock.release();return canceled}).catch(err=>{lock.release();throw err})};return generator}getNode(path,options={include:undefined,exclude:undefined,child_objects:true,tid:undefined}){const tid=options&&options.tid||ID.generate();let lock;return this.nodeLocker.lock(path,tid,false,"getNode").then(l=>{lock=l;const filtered=options&&(options.include||options.exclude||options.child_objects===false);const pathInfo=PathInfo.get(path);const targetRow=this._readNode(path);if(!targetRow){if(path===""){return{value:null}}return lock.moveToParent().then(parentLock=>{lock=parentLock;let parentNode=this._readNode(pathInfo.parentPath);if(parentNode&&[VALUE_TYPES.OBJECT,VALUE_TYPES.ARRAY].includes(parentNode.type)&&pathInfo.key in parentNode){return{revision:parentNode.revision,value:parentNode.value[pathInfo.key]}}return{value:null}})}const includeCheck=options.include?new RegExp("^"+options.include.map(p=>"(?:"+p.replace(/\*/g,"[^/\\[]+")+")").join("|")+"(?:$|[/\\[])"):null;const excludeCheck=options.exclude?new RegExp("^"+options.exclude.map(p=>"(?:"+p.replace(/\*/g,"[^/\\[]+")+")").join("|")+"(?:$|[/\\[])"):null;const childRows=[];for(let i=0;ipathInfo.pathKeys.length+1)){include=false}if(include){const childRow=this._readNode(otherPath);childRow.path=otherPath;childRows.push(childRow)}}this.debug.log(`Read node "/${path}" and ${filtered?"(filtered) ":""}children from ${childRows.length+1} records`.magenta);const result={revision:targetRow?targetRow.revision:null,value:targetRow.value};const objectToArray=obj=>{const arr=[];Object.keys(obj).forEach(key=>{let index=parseInt(key);arr[index]=obj[index]});return arr};if(targetRow.type===VALUE_TYPES.ARRAY){result.value=objectToArray(result.value)}if(targetRow.type===VALUE_TYPES.OBJECT||targetRow.type===VALUE_TYPES.ARRAY){const targetPathKeys=PathInfo.getPathKeys(path);let value=targetRow.value;for(let i=0;i{console.assert(!(childKey in parent[key]),"child key is in parent value already?! HOW?!");parent[key][childKey]=nodeValue[childKey]})}else{parent[key]=nodeValue}parent=parent[key]}}}else if(childRows.length>0){throw new Error(`multiple records found for non-object value!`)}if(options.child_objects===false){Object.keys(result.value).forEach(key=>{if(typeof result.value[key]==="object"&&result.value[key].constructor===Object){console.assert(Object.keys(result.value[key]).length===0);delete result.value[key]}})}if(options.exclude){const process=(obj,keys)=>{if(typeof obj!=="object"){return}const key=keys[0];if(key==="*"){Object.keys(obj).forEach(k=>{process(obj[k],keys.slice(1))})}else if(keys.length>1){key in obj&&process(obj[key],keys.slice(1))}else{delete obj[key]}};options.exclude.forEach(path=>{const checkKeys=PathInfo.getPathKeys(path);process(result.value,checkKeys)})}return result}).then(result=>{lock.release();return result}).catch(err=>{lock.release();throw err})}getNodeInfo(path,options={tid:undefined}){const pathInfo=PathInfo.get(path);const tid=options&&options.tid||ID.generate();let lock;return this.nodeLocker.lock(path,tid,false,"getNodeInfo").then(l=>{lock=l;const node=this._readNode(path);const info=new LocalStorageNodeInfo({path:path,key:typeof pathInfo.key==="string"?pathInfo.key:null,index:typeof pathInfo.key==="number"?pathInfo.key:null,type:node?node.type:0,exists:node!==null,address:node?new LocalStorageNodeAddress(path):null,created:node?new Date(node.created):null,modified:node?new Date(node.modified):null,revision:node?node.revision:null,revision_nr:node?node.revision_nr:null});if(node||path===""){return info}return lock.moveToParent().then(parentLock=>{lock=parentLock;const parent=this._readNode(pathInfo.parentPath);if(parent&&[VALUE_TYPES.OBJECT,VALUE_TYPES.ARRAY].includes(parent.type)&&pathInfo.key in parent.value){info.exists=true;info.value=parent.value[pathInfo.key];info.address=null;info.type=parent.type;info.created=new Date(parent.created);info.modified=new Date(parent.modified);info.revision=parent.revision;info.revision_nr=parent.revision_nr}else{info.address=null}return info})}).then(info=>{lock.release();return info}).catch(err=>{lock&&lock.release();throw err})}removeNode(path,options={tid:undefined}){if(path===""){return Promise.reject(new Error(`Cannot remove the root node`))}const pathInfo=PathInfo.get(path);const tid=options&&options.tid||ID.generate();return this.nodeLocker.lock(pathInfo.parentPath,tid,true,"removeNode").then(lock=>{return this.updateNode(pathInfo.parentPath,{[pathInfo.key]:null},{tid:tid}).then(result=>{lock.release();return result}).catch(err=>{lock.release();throw err})})}setNode(path,value,options={assert_revision:undefined,tid:undefined}){const pathInfo=PathInfo.get(path);let lock;const tid=options&&options.tid||ID.generate();return this.nodeLocker.lock(path,tid,true,"setNode").then(l=>{lock=l;if(path===""){if(value===null||typeof value!=="object"||value instanceof Array||value instanceof ArrayBuffer||"buffer"in value&&value.buffer instanceof ArrayBuffer){return Promise.reject(new Error(`Invalid value for root node: ${value}`))}return this._writeNodeWithTracking("",value,{merge:false,tid:tid})}if(options&&typeof options.assert_revision!=="undefined"){return this.getNodeInfo(path,{tid:lock.tid}).then(info=>{if(info.revision!==options.assert_revision){throw new NodeRevisionError(`revision '${info.revision}' does not match requested revision '${options.assert_revision}'`)}if(info.address&&info.address.path===path&&!this.valueFitsInline(value)){return this._writeNodeWithTracking(path,value,{merge:false,tid:tid})}else{return lock.moveToParent().then(parentLock=>{lock=parentLock;return this._writeNodeWithTracking(pathInfo.parentPath,{[pathInfo.key]:value},{merge:true,tid:tid})})}})}else{return lock.moveToParent().then(parentLock=>{lock=parentLock;return this.updateNode(pathInfo.parentPath,{[pathInfo.key]:value},{tid:tid})})}}).then(result=>{lock.release();return result}).catch(err=>{lock.release();throw err})}updateNode(path,updates,options={tid:undefined}){if(typeof updates!=="object"){return Promise.reject(new Error(`invalid updates argument`))}const tid=options&&options.tid||ID.generate();let lock;return this.nodeLocker.lock(path,tid,true,"updateNode").then(l=>{lock=l;return this.getNodeInfo(path,{tid:lock.tid})}).then(nodeInfo=>{const pathInfo=PathInfo.get(path);if(nodeInfo.exists&&nodeInfo.address&&nodeInfo.address.path===path){return this._writeNodeWithTracking(path,updates,{merge:true,tid:tid})}else if(nodeInfo.exists){const pathInfo=PathInfo.get(path);return lock.moveToParent().then(parentLock=>{lock=parentLock;return this._writeNodeWithTracking(pathInfo.parentPath,{[pathInfo.key]:value},{merge:true,tid:tid})})}else{return lock.moveToParent().then(parentLock=>{lock=parentLock;return this.updateNode(pathInfo.parentPath,{[pathInfo.key]:updates},{tid:tid})})}}).then(result=>{lock.release();return result}).catch(err=>{lock.release();throw err})}}module.exports={LocalStorageNodeAddress:LocalStorageNodeAddress,LocalStorageNodeInfo:LocalStorageNodeInfo,LocalStorage:LocalStorage,LocalStorageSettings:LocalStorageSettings}},{"./node-info":34,"./node-value-types":36,"./storage":40,"acebase-core":11}],40:[function(require,module,exports){(function(process){const{Utils:Utils,DebugLogger:DebugLogger,PathInfo:PathInfo,ID:ID,PathReference:PathReference,ascii85:ascii85}=require("acebase-core");const{NodeLocker:NodeLocker}=require("./node-lock");const{VALUE_TYPES:VALUE_TYPES,getValueTypeName:getValueTypeName}=require("./node-value-types");const{NodeInfo:NodeInfo}=require("./node-info");const{EventEmitter:EventEmitter}=require("events");const{cloneObject:cloneObject,compareValues:compareValues,getChildValues:getChildValues,encodeString:encodeString}=Utils;const colors=require("colors");class NodeNotFoundError extends Error{}class NodeRevisionError extends Error{}class ClusterSettings{constructor(settings){settings=settings||{};this.enabled=settings.enabled===true;this.isMaster=settings.isMaster===true;this.master=this.isMaster?null:settings.master;this.workers=this.isMaster?settings.workers:null}}class ClusterManager extends EventEmitter{constructor(settings){super();this.settings=new ClusterSettings(settings);if(!settings.enabled){}else if(settings.isMaster){settings.workers.forEach(worker=>{worker.on("message",data=>{const{id:id,request:request}=data;if(typeof request==="object"&&request.type==="ping"){worker.send({id:id,result:"pong"})}else{const reply=result=>{worker.send({id:id,result:result})};const broadcast=msg=>{console.assert(!("id"in msg),"message to broadcast cannot have id property, it will confuse workers because they think it is a reply to their request");settings.workers.forEach(otherWorker=>{if(otherWorker!==worker){otherWorker.send(msg)}})};this.emit("worker_request",{request:request,reply:reply,broadcast:broadcast})}})});this.request=msg=>{throw new Error(`request can only be called by worker processes!`)}}else{const master=settings.master;const requests={};this.request=msg=>{return new Promise((resolve,reject)=>{const id=ID.generate();requests[id]=resolve;master.send({id:id,request:msg})})};master.on("message",data=>{if(typeof data.id!=="undefined"){let resolve=requests[data.id];delete requests[data.id];resolve(data.result)}else{this.emit("master_notification",data)}});this.request({type:"ping"}).then(result=>{console.log(`PING master process result: ${result}`)})}}get isMaster(){return this.settings.isMaster}get enabled(){return this.settings.enabled}}class StorageSettings{constructor(settings){settings=settings||{};this.maxInlineValueSize=typeof settings.maxInlineValueSize==="number"?settings.maxInlineValueSize:50;this.removeVoidProperties=settings.removeVoidProperties===true;this.cluster=new ClusterSettings(settings.cluster);this.path=settings.path||".";if(this.path.endsWith("/")){this.path=this.path.slice(0,-1)}this.logLevel=settings.logLevel||"log";this.info=settings.info||"realtime database"}}class Storage extends EventEmitter{constructor(name,settings){super();this.name=name;this.settings=settings;this.debug=new DebugLogger(settings.logLevel,`[${name}]`);colors.setTheme({art:["magenta","bold"],intro:["dim"]});const logo=" ___ ______ ".art+"\n"+" / _ \\ | ___ \\ ".art+"\n"+" / /_\\ \\ ___ ___| |_/ / __ _ ___ ___ ".art+"\n"+" | _ |/ __/ _ \\ ___ \\/ _` / __|/ _ \\".art+"\n"+" | | | | (_| __/ |_/ / (_| \\__ \\ __/".art+"\n"+" \\_| |_/\\___\\___\\____/ \\__,_|___/\\___|".art+"\n"+(settings.info?"".padStart(40-settings.info.length," ")+settings.info.magenta+"\n":"");this.debug.write(logo);this.nodeCache={find(path){return null},update(path,info){}};this.nodeLocker=new NodeLocker;this.cluster=new ClusterManager(settings.cluster);const{DataIndex:DataIndex,ArrayIndex:ArrayIndex,FullTextIndex:FullTextIndex,GeoIndex:GeoIndex}=require("./data-index");const _indexes=[];const storage=this;this.indexes={get supported(){const pfs=require("./promise-fs");return pfs&&pfs.hasFileSystem},create(path,key,options={rebuild:false,type:undefined,include:undefined}){path=path.replace(/\/\*$/,"");const rebuild=options&&options.rebuild===true;const indexType=options&&options.type||"normal";let includeKeys=options&&options.include||[];if(typeof includeKeys==="string"){includeKeys=[includeKeys]}const existingIndex=_indexes.find(index=>index.path===path&&index.key===key&&index.type===indexType&&index.includeKeys.length===includeKeys.length&&index.includeKeys.every((key,index)=>includeKeys[index]===key));if(existingIndex&&rebuild!==true){storage.debug.log(`Index on "/${path}/*/${key}" already exists`.inverse);return Promise.resolve(existingIndex)}const index=existingIndex||(()=>{switch(indexType){case"array":return new ArrayIndex(storage,path,key,{include:options.include,config:options.config});case"fulltext":return new FullTextIndex(storage,path,key,{include:options.include,config:options.config});case"geo":return new GeoIndex(storage,path,key,{include:options.include,config:options.config});default:return new DataIndex(storage,path,key,{include:options.include,config:options.config})}})();if(!existingIndex){_indexes.push(index)}return index.build().then(()=>{return index}).catch(err=>{storage.debug.error(`Index build on "/${path}/*/${key}" failed: ${err.message} (code: ${err.code})`.red);if(!existingIndex){_indexes.splice(_indexes.indexOf(index),1)}throw err})},get(path,key=null){return _indexes.filter(index=>index.path===path&&(key===null||key===index.key))},getAll(targetPath,options={parentPaths:true,childPaths:true}){const pathKeys=PathInfo.getPathKeys(targetPath);return _indexes.filter(index=>{const indexKeys=PathInfo.getPathKeys(index.path+"/*");if(options.parentPaths&&indexKeys.every((key,i)=>{return key==="*"||pathKeys[i]===key})&&[index.key].concat(...index.includeKeys).includes(pathKeys[indexKeys.length])){return true}else if(indexKeys.length{return[key,"*"].includes(indexKeys[i])})})},list(){return _indexes.slice()},load(){_indexes.splice(0);const pfs=require("./promise-fs");if(!pfs||!pfs.readdir){return Promise.resolve()}return pfs.readdir(`${storage.settings.path}/${storage.name}.acebase`).then(files=>{const promises=[];files.forEach(fileName=>{if(fileName.endsWith(".idx")){const p=DataIndex.readFromFile(storage,fileName).then(index=>{_indexes.push(index)}).catch(err=>{storage.debug.error(err)});promises.push(p)}});return Promise.all(promises)}).catch(err=>{if(err.code!=="ENOENT"){storage.debug.error(err)}})}};const _subs={};const _supportedEvents=["value","child_added","child_changed","child_removed"];_supportedEvents.push(..._supportedEvents.map(event=>`notify_${event}`));this.subscriptions={add(path,type,callback){if(_supportedEvents.indexOf(type)<0){throw new TypeError(`Invalid event type "${type}"`)}let pathSubs=_subs[path];if(!pathSubs){pathSubs=_subs[path]=[]}pathSubs.push({created:Date.now(),type:type,callback:callback})},remove(path,type=undefined,callback=undefined){let pathSubs=_subs[path];if(!pathSubs){return}while(true){const i=pathSubs.findIndex(ps=>(type?ps.type===type:true)&&(callback?ps.callback===callback:true));if(i<0){break}pathSubs.splice(i,1)}},hasValueSubscribersForPath(path){const valueNeeded=this.getValueSubscribersForPath(path);return!!valueNeeded},getValueSubscribersForPath(path){const pathInfo=new PathInfo(path);const valueSubscribers=[];Object.keys(_subs).forEach(subscriptionPath=>{if(pathInfo.equals(subscriptionPath)||pathInfo.isDescendantOf(subscriptionPath)){let pathSubs=_subs[subscriptionPath];const eventPath=PathInfo.fillVariables(subscriptionPath,path);pathSubs.forEach(sub=>{let dataPath=null;if(sub.type==="value"||sub.type==="notify_value"){dataPath=eventPath}else if((sub.type==="child_changed"||sub.type==="notify_child_changed")&&path!==eventPath){let childKey=PathInfo.getPathKeys(path.slice(eventPath.length).replace(/^\//,""))[0];dataPath=PathInfo.getChildPath(eventPath,childKey)}else if(["child_added","child_removed","notify_child_added","notify_child_removed"].includes(sub.type)&&pathInfo.isChildOf(eventPath)){let childKey=PathInfo.getPathKeys(path.slice(eventPath.length).replace(/^\//,""))[0];dataPath=PathInfo.getChildPath(eventPath,childKey)}if(dataPath!==null&&valueSubscribers.findIndex(s=>s.type===sub.type&&s.path===eventPath)<0){valueSubscribers.push({type:sub.type,eventPath:eventPath,dataPath:dataPath,subscriptionPath:subscriptionPath})}})}});return valueSubscribers},getAllSubscribersForPath(path){const pathInfo=PathInfo.get(path);const subscribers=[];Object.keys(_subs).forEach(subscriptionPath=>{if(pathInfo.equals(subscriptionPath)||pathInfo.isDescendantOf(subscriptionPath)||pathInfo.isAncestorOf(subscriptionPath)){let pathSubs=_subs[subscriptionPath];const eventPath=PathInfo.fillVariables(subscriptionPath,path);pathSubs.forEach(sub=>{let dataPath=null;if(sub.type==="value"||sub.type==="notify_value"){dataPath=eventPath}else if(sub.type==="child_changed"||sub.type==="notify_child_changed"){let childKey=path===eventPath||pathInfo.isAncestorOf(eventPath)?"*":PathInfo.getPathKeys(path.slice(eventPath.length).replace(/^\//,""))[0];dataPath=PathInfo.getChildPath(eventPath,childKey)}else if(["child_added","child_removed","notify_child_added","notify_child_removed"].includes(sub.type)&&(pathInfo.isChildOf(eventPath)||path===eventPath||pathInfo.isAncestorOf(eventPath))){let childKey=path===eventPath||pathInfo.isAncestorOf(eventPath)?"*":PathInfo.getPathKeys(path.slice(eventPath.length).replace(/^\//,""))[0];dataPath=PathInfo.getChildPath(eventPath,childKey)}if(dataPath!==null){subscribers.push({type:sub.type,eventPath:eventPath,dataPath:dataPath,subscriptionPath:subscriptionPath})}})}});return subscribers},trigger(event,path,dataPath,oldValue,newValue){const pathSubscriptions=_subs[path]||[];pathSubscriptions.filter(sub=>sub.type===event).forEach(sub=>{sub.callback(null,dataPath,newValue,oldValue)})}}}get path(){return`${this.settings.path}/${this.name}.acebase`}valueFitsInline(value){const encoding="utf8";if(typeof value==="number"||typeof value==="boolean"||value instanceof Date){return true}else if(typeof value==="string"){if(value.length>this.settings.maxInlineValueSize){return false}const encoded=encodeString(value);return encoded.lengththis.settings.maxInlineValueSize){return false}const encoded=encodeString(value.path);return encoded.length0){hasValueSubscribers=true;let eventPaths=valueSubscribers.map(sub=>{return{path:sub.dataPath,keys:PathInfo.getPathKeys(sub.dataPath)}}).sort((a,b)=>{if(a.keys.lengthb.keys.length)return 1;return 0});let first=eventPaths[0];topEventPath=first.path;if(valueSubscribers.filter(sub=>sub.dataPath===topEventPath).every(sub=>sub.type.startsWith("notify_"))){hasValueSubscribers=false}topEventPath=PathInfo.fillVariables(topEventPath,path)}const writeNode=()=>{if(typeof options._customWriteFunction==="function"){return options._customWriteFunction()}return this._writeNode(path,value,options)};const indexes=this.indexes.getAll(path,{childPaths:true,parentPaths:true}).map(index=>({index:index,keys:PathInfo.getPathKeys(index.path)})).sort((a,b)=>{if(a.keys.lengthb.keys.length){return 1}return 0}).map(obj=>obj.index);if(eventSubscriptions.length===0&&indexes.length===0){return writeNode()}let keysFilter=[];if(indexes.length>0){indexes.sort((a,b)=>{if(typeof a._pathKeys==="undefined"){a._pathKeys=PathInfo.getPathKeys(a.path)}if(typeof b._pathKeys==="undefined"){b._pathKeys=PathInfo.getPathKeys(b.path)}if(a._pathKeys.lengthb._pathKeys.length)return 1;return 0});const topIndex=indexes[0];let topIndexPath=topIndex.path===path?path:PathInfo.fillVariables(`${topIndex.path}/*`,path);if(topIndexPath.lengthindex.path===topIndex.path).forEach(index=>{let keys=[index.key].concat(index.includeKeys);keys.forEach(key=>!keysFilter.includes(key)&&keysFilter.push(key))})}}if(!hasValueSubscribers&&options.merge===true&&keysFilter.length===0){keysFilter=Object.keys(value);if(topEventPath!==path){let trailPath=path.slice(topEventPath.length);keysFilter=keysFilter.map(key=>`${trailPath}/${key}`)}}return this.getNodeInfo(topEventPath,{tid:tid}).then(eventNodeInfo=>{if(!eventNodeInfo.exists){return null}let valueOptions={tid:tid};if(keysFilter.length>0){valueOptions.include=keysFilter}if(topEventPath===""&&typeof valueOptions.include==="undefined"){this.debug.warn(`WARNING: One or more value event listeners on the root node are causing the entire database value to be read to facilitate change tracking. Using "value", "notify_value", "child_changed" and "notify_child_changed" events on the root node are a bad practice because of the significant performance impact`)}return this.getNodeValue(topEventPath,valueOptions)}).then(currentValue=>{topEventData=currentValue;return writeNode()}).then(result=>{let newTopEventData=cloneObject(topEventData);if(newTopEventData===null){newTopEventData=path===topEventPath?value:{}}let modifiedData=newTopEventData;if(path!==topEventPath){let trailPath=path.slice(topEventPath.length).replace(/^\//,"");let trailKeys=PathInfo.getPathKeys(trailPath);while(trailKeys.length>0){let childKey=trailKeys.shift();if(!options.merge&&trailKeys.length===0){modifiedData[childKey]=value}else{if(!(childKey in modifiedData)){modifiedData[childKey]={}}modifiedData=modifiedData[childKey]}}}if(options.merge){Object.keys(value).forEach(key=>{let newValue=value[key];if(newValue!==null){modifiedData[key]=newValue}else{delete modifiedData[key]}})}else if(path===topEventPath){newTopEventData=modifiedData=value}const indexUpdates=[];indexes.map(index=>({index:index,keys:PathInfo.getPathKeys(index.path)})).sort((a,b)=>{if(a.keys.lengthb.keys.length){return-1}return 0}).forEach(({index:index})=>{let pathKeys=PathInfo.getPathKeys(topEventPath);let indexPathKeys=PathInfo.getPathKeys(index.path+"/*");let trailKeys=indexPathKeys.slice(pathKeys.length);let oldValue=topEventData;let newValue=newTopEventData;if(trailKeys.length===0){console.assert(pathKeys.length===indexPathKeys.length,"check logic");const p=index.handleRecordUpdate(topEventPath,oldValue,newValue);indexUpdates.push(p);return}const getAllIndexUpdates=(path,oldValue,newValue)=>{if(oldValue===null&&newValue===null){return[]}let pathKeys=PathInfo.getPathKeys(path);let indexPathKeys=PathInfo.getPathKeys(index.path+"/*");let trailKeys=indexPathKeys.slice(pathKeys.length);if(trailKeys.length===0){console.assert(pathKeys.length===indexPathKeys.length,"check logic");return[{path:path,oldValue:oldValue,newValue:newValue}]}let results=[];let trailPath="";while(trailKeys.length>0){let subKey=trailKeys.shift();if(subKey==="*"){let allKeys=oldValue===null?[]:Object.keys(oldValue);newValue!==null&&Object.keys(newValue).forEach(key=>{if(allKeys.indexOf(key)<0){allKeys.push(key)}});allKeys.forEach(key=>{let childPath=PathInfo.getChildPath(trailPath,key);let childValues=getChildValues(key,oldValue,newValue);let subTrailPath=PathInfo.getChildPath(path,childPath);let childResults=getAllIndexUpdates(subTrailPath,childValues.oldValue,childValues.newValue);results=results.concat(childResults)});break}else{let values=getChildValues(subKey,oldValue,newValue);oldValue=values.oldValue;newValue=values.newValue;if(oldValue===null&&newValue===null){break}trailPath=PathInfo.getChildPath(trailPath,subKey)}}return results};let results=getAllIndexUpdates(topEventPath,oldValue,newValue);results.forEach(result=>{const p=index.handleRecordUpdate(result.path,result.oldValue,result.newValue);indexUpdates.push(p)})});const callSubscriberWithValues=(sub,oldValue,newValue,variables=[])=>{let trigger=true;let type=sub.type;if(type.startsWith("notify_")){type=type.slice("notify_".length)}if(type==="child_changed"&&(oldValue===null||newValue===null)){trigger=false}else if(type==="value"||type==="child_changed"){let changes=compareValues(oldValue,newValue);trigger=changes!=="identical"}else if(type==="child_added"){trigger=oldValue===null&&newValue!==null}else if(type==="child_removed"){trigger=oldValue!==null&&newValue===null}let dataPath=sub.dataPath;variables.forEach((variable,i)=>{const safeVarName=variable.name==="*"?"\\*":variable.name.replace("$","\\$");dataPath=dataPath.replace(new RegExp(`(^|/)${safeVarName}([/[]|$)`),`$1${variable.value}$2`)});trigger&&this.subscriptions.trigger(sub.type,sub.subscriptionPath,dataPath,oldValue,newValue)};const triggerAllEvents=()=>{eventSubscriptions.map(sub=>{const keys=PathInfo.getPathKeys(sub.dataPath);return{sub:sub,keys:keys}}).sort((a,b)=>{if(a.keys.lengthb.keys.length){return-1}return 0}).forEach(({sub:sub})=>{const process=(currentPath,oldValue,newValue,variables=[])=>{let trailPath=sub.dataPath.slice(currentPath.length).replace(/^\//,"");let trailKeys=PathInfo.getPathKeys(trailPath);while(trailKeys.length>0){let subKey=trailKeys.shift();if(typeof subKey==="string"&&(subKey==="*"||subKey[0]==="$")){let allKeys=oldValue===null?[]:Object.keys(oldValue);newValue!==null&&Object.keys(newValue).forEach(key=>{if(allKeys.indexOf(key)<0){allKeys.push(key)}});allKeys.forEach(key=>{const childValues=getChildValues(key,oldValue,newValue);const vars=variables.concat({name:subKey,value:key});if(trailKeys.length===0){callSubscriberWithValues(sub,childValues.oldValue,childValues.newValue,vars)}else{process(`${currentPath}/${subKey}`,childValues.oldValue,childValues.newValue,vars)}});return}else{currentPath=PathInfo.getChildPath(currentPath,subKey);let childValues=getChildValues(subKey,oldValue,newValue);oldValue=childValues.oldValue;newValue=childValues.newValue}}callSubscriberWithValues(sub,oldValue,newValue,variables)};process(topEventPath,topEventData,newTopEventData)})};if(options.waitForIndexUpdates===false){indexUpdates.splice(0)}return Promise.all(indexUpdates).then(()=>{process.nextTick(triggerAllEvents);return result})})}getChildren(path,options={keyFilter:undefined,tid:undefined}){throw new Error(`This method must be implemented by subclass`)}getNodeValue(path,options={include:undefined,exclude:undefined,child_objects:true,tid:undefined}){return this.getNode(path,options).then(node=>{return node.value})}getNode(path,options={include:undefined,exclude:undefined,child_objects:true,tid:undefined}){throw new Error(`This method must be implemented by subclass`)}getNodeInfo(path,options={tid:undefined}){throw new Error(`This method must be implemented by subclass`)}removeNode(path,options={tid:undefined}){throw new Error(`This method must be implemented by subclass`)}setNode(path,value,options={tid:undefined}){throw new Error(`This method must be implemented by subclass`)}updateNode(path,updates,options={tid:undefined}){throw new Error(`This method must be implemented by subclass`)}transactNode(path,callback,options={no_lock:false}){let checkRevision;const tid=ID.generate();const lockPromise=options&&options.no_lock===true?Promise.resolve({tid:tid,release(){}}):this.nodeLocker.lock(path,tid,true,"transactNode");return lockPromise.then(lock=>{let changed=false,changeCallback=(err,path)=>{changed=true};if(options&&options.no_lock){this.subscriptions.add(path,"notify_value",changeCallback)}return this.getNode(path,{tid:tid}).then(node=>{checkRevision=node.revision;let newValue;try{newValue=callback(node.value)}catch(err){this.debug.error(`Error in transaction callback: ${err.message}`)}if(newValue instanceof Promise){return newValue.catch(err=>{this.debug.error(`Error in transaction callback: ${err.message}`)})}return newValue}).then(newValue=>{if(typeof newValue==="undefined"){return}if(options&&options.no_lock){this.subscriptions.remove(path,"notify_value",changeCallback)}if(changed){return Promise.reject(new NodeRevisionError(`Node changed`))}return this.setNode(path,newValue,{assert_revision:checkRevision,tid:lock.tid})}).then(result=>{lock.release();return result}).catch(err=>{lock.release();if(err instanceof NodeRevisionError){console.warn(`node value changed, running again. Error: ${err.message}`);return this.transactNode(path,callback,options)}else{throw err}})})}matchNode(path,criteria,options={tid:undefined}){const tid=options&&options.tid||ID.generate();const checkNode=(path,criteria)=>{if(criteria.length===0){return Promise.resolve(true)}const criteriaKeys=criteria.reduce((keys,cr)=>{if(keys.indexOf(cr.key)<0){keys.push(cr.key)}return keys},[]);const unseenKeys=criteriaKeys.slice();let isMatch=true;let delayedMatchPromises=[];return this.getChildren(path,{tid:tid,keyFilter:criteriaKeys}).next(childInfo=>{unseenKeys.includes(childInfo.key)&&unseenKeys.splice(unseenKeys.indexOf(childInfo.key),1);const keyCriteria=criteria.filter(cr=>cr.key===childInfo.key).map(cr=>({op:cr.op,compare:cr.compare}));const result=checkChild(childInfo,keyCriteria);isMatch=result.isMatch;delayedMatchPromises.push(...result.promises);if(!isMatch||unseenKeys.length===0){return false}}).then(()=>{if(isMatch){return Promise.all(delayedMatchPromises).then(results=>{isMatch=results.every(res=>res.isMatch)})}}).then(()=>{if(!isMatch){return false}isMatch=unseenKeys.every(key=>{const child=new NodeInfo({key:key,exists:false});const keyCriteria=criteria.filter(cr=>cr.key===key).map(cr=>({op:cr.op,compare:cr.compare}));const result=checkChild(child,keyCriteria);return result.isMatch});return isMatch}).catch(err=>{this.debug.error(`Error matching on "${path}": `,err);throw err})};const checkChild=(child,criteria)=>{const promises=[];const isMatch=criteria.every(f=>{let proceed=true;if(f.op==="!exists"||f.op==="=="&&(typeof f.compare==="undefined"||f.compare===null)){proceed=!child.exists}else if(f.op==="exists"||f.op==="!="&&(typeof f.compare==="undefined"||f.compare===null)){proceed=child.exists}else if(!child.exists){proceed=false}else{if(child.address){if(child.valueType===VALUE_TYPES.OBJECT&&["has","!has"].indexOf(f.op)>=0){const op=f.op==="has"?"exists":"!exists";const p=checkNode(child.path,[{key:f.compare,op:op}]).then(isMatch=>{return{key:child.key,isMatch:isMatch}});promises.push(p);proceed=true}else if(child.valueType===VALUE_TYPES.ARRAY&&["contains","!contains"].indexOf(f.op)>=0){const p=this.getNodeValue(child.path,{tid:tid}).then(arr=>{const isMatch=f.op==="contains"?f.compare instanceof Array?f.compare.every(val=>arr.includes(val)):arr.includes(f.compare):f.compare instanceof Array?!f.compare.some(val=>arr.includes(val)):!arr.includes(f.compare);return{key:child.key,isMatch:isMatch}});promises.push(p);proceed=true}else if(child.valueType===VALUE_TYPES.STRING){const p=this.getNodeValue(child.path,{tid:tid}).then(val=>{return{key:child.key,isMatch:this.test(val,f.op,f.compare)}});promises.push(p);proceed=true}else{proceed=false}}else if(child.type===VALUE_TYPES.OBJECT&&["has","!has"].indexOf(f.op)>=0){const has=f.compare in child.value;proceed=has&&f.op==="has"||!has&&f.op==="!has"}else if(child.type===VALUE_TYPES.ARRAY&&["contains","!contains"].indexOf(f.op)>=0){const contains=child.value.indexOf(f.compare)>=0;proceed=contains&&f.op==="contains"||!contains&&f.op==="!contains"}else{const ret=this.test(child.value,f.op,f.compare);if(ret instanceof Promise){promises.push(ret);ret=true}proceed=ret}}return proceed});return{isMatch:isMatch,promises:promises}};return checkNode(path,criteria)}test(val,op,compare){if(op==="<"){return val"){return val>compare}if(op===">="){return val>=compare}if(op==="in"){return compare.indexOf(val)>=0}if(op==="!in"){return compare.indexOf(val)<0}if(op==="like"||op==="!like"){const pattern="^"+compare.replace(/[-[\]{}()+.,\\^$|#\s]/g,"\\$&").replace(/\?/g,".").replace(/\*/g,".*?")+"$";const re=new RegExp(pattern,"i");const isMatch=re.test(val.toString());return op==="like"?isMatch:!isMatch}if(op==="matches"){return compare.test(val.toString())}if(op==="!matches"){return!compare.test(val.toString())}if(op==="between"){return val>=compare[0]&&val<=compare[1]}if(op==="!between"){return valcompare[1]}if(op==="has"||op==="!has"){const has=typeof val==="object"&&compare in val;return op==="has"?has:!has}if(op==="contains"||op==="!contains"){const includes=typeof val==="object"&&val instanceof Array&&val.includes(compare);return op==="contains"?includes:!includes}return false}exportNode(path,stream,options={format:"json"}){if(options&&options.format!=="json"){throw new Error(`Only json output is currently supported`)}const stringifyValue=(type,val)=>{const escape=str=>str.replace(/\\/i,"\\\\").replace(/"/g,'\\"');if(type===VALUE_TYPES.DATETIME){val=`"${val.toISOString()}"`}else if(type===VALUE_TYPES.STRING){val=`"${escape(val)}"`}else if(type===VALUE_TYPES.ARRAY){val=`[]`}else if(type===VALUE_TYPES.OBJECT){val=`{}`}else if(type===VALUE_TYPES.BINARY){val=`"${escape(ascii85.encode(val))}"`}else if(type===VALUE_TYPES.REFERENCE){val=`"${val.path}"`}return val};const queue=[];let outputCount=0;let objStart="",objEnd="";const buffer={output:"",enable:false,promise:null};return this.getNodeInfo(path).then(nodeInfo=>{if(!nodeInfo.exists){stream.write("null")}else if(nodeInfo.type===VALUE_TYPES.OBJECT){objStart="{";objEnd="}"}else if(nodeInfo.type===VALUE_TYPES.ARRAY){objStart="{";objEnd="}"}else{return this.getNodeValue(path).then(value=>{const val=stringifyValue(nodeInfo.type,value);return stream.write(val)})}let p=Promise.resolve();if(objStart){p=stream.write(objStart);if(!(p instanceof Promise)){p=Promise.resolve()}}return p.then(()=>{return this.getChildren(path).next(childInfo=>{if(childInfo.address){queue.push(childInfo)}else{const val=stringifyValue(childInfo.type,childInfo.value);const comma=outputCount>0?",":"";const key=typeof childInfo.index==="number"?`"${childInfo.index}"`:`"${childInfo.key}"`;const output=`${comma}${key}:${val}`;outputCount++;if(buffer.enable){buffer.output+=output}else{const flush=output=>{const p=stream.write(output);if(p instanceof Promise){buffer.enable=true;buffer.promise=p.then(()=>{const buffered=buffer.output;buffer.enable=false;buffer.output="";buffer.promise=null;if(buffered.length>0){return flush(buffered)}});return buffer.promise}};flush(output)}}})})}).then(()=>{return buffer.promise}).then(()=>{const next=()=>{if(queue.length===0){return}const childInfo=queue.shift();const comma=outputCount>0?",":"";const key=typeof childInfo.index==="number"?`"${childInfo.index}"`:`"${childInfo.key}"`;let p=stream.write(`${comma}${key}:`);outputCount++;if(!(p instanceof Promise)){p=Promise.resolve(p)}return p.then(()=>{return this.exportNode(childInfo.address.path,stream)}).then(()=>{return next()})};return next()}).then(()=>{if(objEnd){return stream.write(objEnd)}})}}module.exports={Storage:Storage,StorageSettings:StorageSettings,NodeNotFoundError:NodeNotFoundError,NodeRevisionError:NodeRevisionError}}).call(this,require("_process"))},{"./data-index":41,"./node-info":34,"./node-lock":35,"./node-value-types":36,"./promise-fs":41,_process:45,"acebase-core":11,colors:22,events:42}],41:[function(require,module,exports){},{}],42:[function(require,module,exports){var objectCreate=Object.create||objectCreatePolyfill;var objectKeys=Object.keys||objectKeysPolyfill;var bind=Function.prototype.bind||functionBindPolyfill;function EventEmitter(){if(!this._events||!Object.prototype.hasOwnProperty.call(this,"_events")){this._events=objectCreate(null);this._eventsCount=0}this._maxListeners=this._maxListeners||undefined}module.exports=EventEmitter;EventEmitter.EventEmitter=EventEmitter;EventEmitter.prototype._events=undefined;EventEmitter.prototype._maxListeners=undefined;var defaultMaxListeners=10;var hasDefineProperty;try{var o={};if(Object.defineProperty)Object.defineProperty(o,"x",{value:0});hasDefineProperty=o.x===0}catch(err){hasDefineProperty=false}if(hasDefineProperty){Object.defineProperty(EventEmitter,"defaultMaxListeners",{enumerable:true,get:function(){return defaultMaxListeners},set:function(arg){if(typeof arg!=="number"||arg<0||arg!==arg)throw new TypeError('"defaultMaxListeners" must be a positive number');defaultMaxListeners=arg}})}else{EventEmitter.defaultMaxListeners=defaultMaxListeners}EventEmitter.prototype.setMaxListeners=function setMaxListeners(n){if(typeof n!=="number"||n<0||isNaN(n))throw new TypeError('"n" argument must be a positive number');this._maxListeners=n;return this};function $getMaxListeners(that){if(that._maxListeners===undefined)return EventEmitter.defaultMaxListeners;return that._maxListeners}EventEmitter.prototype.getMaxListeners=function getMaxListeners(){return $getMaxListeners(this)};function emitNone(handler,isFn,self){if(isFn)handler.call(self);else{var len=handler.length;var listeners=arrayClone(handler,len);for(var i=0;i1)er=arguments[1];if(er instanceof Error){throw er}else{var err=new Error('Unhandled "error" event. ('+er+")");err.context=er;throw err}return false}handler=events[type];if(!handler)return false;var isFn=typeof handler==="function";len=arguments.length;switch(len){case 1:emitNone(handler,isFn,this);break;case 2:emitOne(handler,isFn,this,arguments[1]);break;case 3:emitTwo(handler,isFn,this,arguments[1],arguments[2]);break;case 4:emitThree(handler,isFn,this,arguments[1],arguments[2],arguments[3]);break;default:args=new Array(len-1);for(i=1;i0&&existing.length>m){existing.warned=true;var w=new Error("Possible EventEmitter memory leak detected. "+existing.length+' "'+String(type)+'" listeners '+"added. Use emitter.setMaxListeners() to "+"increase limit.");w.name="MaxListenersExceededWarning";w.emitter=target;w.type=type;w.count=existing.length;if(typeof console==="object"&&console.warn){console.warn("%s: %s",w.name,w.message)}}}}return target}EventEmitter.prototype.addListener=function addListener(type,listener){return _addListener(this,type,listener,false)};EventEmitter.prototype.on=EventEmitter.prototype.addListener;EventEmitter.prototype.prependListener=function prependListener(type,listener){return _addListener(this,type,listener,true)};function onceWrapper(){if(!this.fired){this.target.removeListener(this.type,this.wrapFn);this.fired=true;switch(arguments.length){case 0:return this.listener.call(this.target);case 1:return this.listener.call(this.target,arguments[0]);case 2:return this.listener.call(this.target,arguments[0],arguments[1]);case 3:return this.listener.call(this.target,arguments[0],arguments[1],arguments[2]);default:var args=new Array(arguments.length);for(var i=0;i=0;i--){if(list[i]===listener||list[i].listener===listener){originalListener=list[i].listener;position=i;break}}if(position<0)return this;if(position===0)list.shift();else spliceOne(list,position);if(list.length===1)events[type]=list[0];if(events.removeListener)this.emit("removeListener",type,originalListener||listener)}return this};EventEmitter.prototype.removeAllListeners=function removeAllListeners(type){var listeners,events,i;events=this._events;if(!events)return this;if(!events.removeListener){if(arguments.length===0){this._events=objectCreate(null);this._eventsCount=0}else if(events[type]){if(--this._eventsCount===0)this._events=objectCreate(null);else delete events[type]}return this}if(arguments.length===0){var keys=objectKeys(events);var key;for(i=0;i=0;i--){this.removeListener(type,listeners[i])}}return this};function _listeners(target,type,unwrap){var events=target._events;if(!events)return[];var evlistener=events[type];if(!evlistener)return[];if(typeof evlistener==="function")return unwrap?[evlistener.listener||evlistener]:[evlistener];return unwrap?unwrapListeners(evlistener):arrayClone(evlistener,evlistener.length)}EventEmitter.prototype.listeners=function listeners(type){return _listeners(this,type,true)};EventEmitter.prototype.rawListeners=function rawListeners(type){return _listeners(this,type,false)};EventEmitter.listenerCount=function(emitter,type){if(typeof emitter.listenerCount==="function"){return emitter.listenerCount(type)}else{return listenerCount.call(emitter,type)}};EventEmitter.prototype.listenerCount=listenerCount;function listenerCount(type){var events=this._events;if(events){var evlistener=events[type];if(typeof evlistener==="function"){return 1}else if(evlistener){return evlistener.length}}return 0}EventEmitter.prototype.eventNames=function eventNames(){return this._eventsCount>0?Reflect.ownKeys(this._events):[]};function spliceOne(list,index){for(var i=index,k=i+1,n=list.length;k1){for(var i=1;i=len)return x;switch(x){case"%s":return String(args[i++]);case"%d":return Number(args[i++]);case"%j":try{return JSON.stringify(args[i++])}catch(_){return"[Circular]"}default:return x}}));for(var x=args[i];i=3)ctx.depth=arguments[2];if(arguments.length>=4)ctx.colors=arguments[3];if(isBoolean(opts)){ctx.showHidden=opts}else if(opts){exports._extend(ctx,opts)}if(isUndefined(ctx.showHidden))ctx.showHidden=false;if(isUndefined(ctx.depth))ctx.depth=2;if(isUndefined(ctx.colors))ctx.colors=false;if(isUndefined(ctx.customInspect))ctx.customInspect=true;if(ctx.colors)ctx.stylize=stylizeWithColor;return formatValue(ctx,obj,ctx.depth)}exports.inspect=inspect;inspect.colors={bold:[1,22],italic:[3,23],underline:[4,24],inverse:[7,27],white:[37,39],grey:[90,39],black:[30,39],blue:[34,39],cyan:[36,39],green:[32,39],magenta:[35,39],red:[31,39],yellow:[33,39]};inspect.styles={special:"cyan",number:"yellow",boolean:"yellow",undefined:"grey",null:"bold",string:"green",date:"magenta",regexp:"red"};function stylizeWithColor(str,styleType){var style=inspect.styles[styleType];if(style){return"["+inspect.colors[style][0]+"m"+str+"["+inspect.colors[style][1]+"m"}else{return str}}function stylizeNoColor(str,styleType){return str}function arrayToHash(array){var hash={};array.forEach((function(val,idx){hash[val]=true}));return hash}function formatValue(ctx,value,recurseTimes){if(ctx.customInspect&&value&&isFunction(value.inspect)&&value.inspect!==exports.inspect&&!(value.constructor&&value.constructor.prototype===value)){var ret=value.inspect(recurseTimes,ctx);if(!isString(ret)){ret=formatValue(ctx,ret,recurseTimes)}return ret}var primitive=formatPrimitive(ctx,value);if(primitive){return primitive}var keys=Object.keys(value);var visibleKeys=arrayToHash(keys);if(ctx.showHidden){keys=Object.getOwnPropertyNames(value)}if(isError(value)&&(keys.indexOf("message")>=0||keys.indexOf("description")>=0)){return formatError(value)}if(keys.length===0){if(isFunction(value)){var name=value.name?": "+value.name:"";return ctx.stylize("[Function"+name+"]","special")}if(isRegExp(value)){return ctx.stylize(RegExp.prototype.toString.call(value),"regexp")}if(isDate(value)){return ctx.stylize(Date.prototype.toString.call(value),"date")}if(isError(value)){return formatError(value)}}var base="",array=false,braces=["{","}"];if(isArray(value)){array=true;braces=["[","]"]}if(isFunction(value)){var n=value.name?": "+value.name:"";base=" [Function"+n+"]"}if(isRegExp(value)){base=" "+RegExp.prototype.toString.call(value)}if(isDate(value)){base=" "+Date.prototype.toUTCString.call(value)}if(isError(value)){base=" "+formatError(value)}if(keys.length===0&&(!array||value.length==0)){return braces[0]+base+braces[1]}if(recurseTimes<0){if(isRegExp(value)){return ctx.stylize(RegExp.prototype.toString.call(value),"regexp")}else{return ctx.stylize("[Object]","special")}}ctx.seen.push(value);var output;if(array){output=formatArray(ctx,value,recurseTimes,visibleKeys,keys)}else{output=keys.map((function(key){return formatProperty(ctx,value,recurseTimes,visibleKeys,key,array)}))}ctx.seen.pop();return reduceToSingleString(output,base,braces)}function formatPrimitive(ctx,value){if(isUndefined(value))return ctx.stylize("undefined","undefined");if(isString(value)){var simple="'"+JSON.stringify(value).replace(/^"|"$/g,"").replace(/'/g,"\\'").replace(/\\"/g,'"')+"'";return ctx.stylize(simple,"string")}if(isNumber(value))return ctx.stylize(""+value,"number");if(isBoolean(value))return ctx.stylize(""+value,"boolean");if(isNull(value))return ctx.stylize("null","null")}function formatError(value){return"["+Error.prototype.toString.call(value)+"]"}function formatArray(ctx,value,recurseTimes,visibleKeys,keys){var output=[];for(var i=0,l=value.length;i-1){if(array){str=str.split("\n").map((function(line){return" "+line})).join("\n").substr(2)}else{str="\n"+str.split("\n").map((function(line){return" "+line})).join("\n")}}}else{str=ctx.stylize("[Circular]","special")}}if(isUndefined(name)){if(array&&key.match(/^\d+$/)){return str}name=JSON.stringify(""+key);if(name.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)){name=name.substr(1,name.length-2);name=ctx.stylize(name,"name")}else{name=name.replace(/'/g,"\\'").replace(/\\"/g,'"').replace(/(^"|"$)/g,"'");name=ctx.stylize(name,"string")}}return name+": "+str}function reduceToSingleString(output,base,braces){var numLinesEst=0;var length=output.reduce((function(prev,cur){numLinesEst++;if(cur.indexOf("\n")>=0)numLinesEst++;return prev+cur.replace(/\u001b\[\d\d?m/g,"").length+1}),0);if(length>60){return braces[0]+(base===""?"":base+"\n ")+" "+output.join(",\n ")+" "+braces[1]}return braces[0]+base+" "+output.join(", ")+" "+braces[1]}function isArray(ar){return Array.isArray(ar)}exports.isArray=isArray;function isBoolean(arg){return typeof arg==="boolean"}exports.isBoolean=isBoolean;function isNull(arg){return arg===null}exports.isNull=isNull;function isNullOrUndefined(arg){return arg==null}exports.isNullOrUndefined=isNullOrUndefined;function isNumber(arg){return typeof arg==="number"}exports.isNumber=isNumber;function isString(arg){return typeof arg==="string"}exports.isString=isString;function isSymbol(arg){return typeof arg==="symbol"}exports.isSymbol=isSymbol;function isUndefined(arg){return arg===void 0}exports.isUndefined=isUndefined;function isRegExp(re){return isObject(re)&&objectToString(re)==="[object RegExp]"}exports.isRegExp=isRegExp;function isObject(arg){return typeof arg==="object"&&arg!==null}exports.isObject=isObject;function isDate(d){return isObject(d)&&objectToString(d)==="[object Date]"}exports.isDate=isDate;function isError(e){return isObject(e)&&(objectToString(e)==="[object Error]"||e instanceof Error)}exports.isError=isError;function isFunction(arg){return typeof arg==="function"}exports.isFunction=isFunction;function isPrimitive(arg){return arg===null||typeof arg==="boolean"||typeof arg==="number"||typeof arg==="string"||typeof arg==="symbol"||typeof arg==="undefined"}exports.isPrimitive=isPrimitive;exports.isBuffer=require("./support/isBuffer");function objectToString(o){return Object.prototype.toString.call(o)}function pad(n){return n<10?"0"+n.toString(10):n.toString(10)}var months=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];function timestamp(){var d=new Date;var time=[pad(d.getHours()),pad(d.getMinutes()),pad(d.getSeconds())].join(":");return[d.getDate(),months[d.getMonth()],time].join(" ")}exports.log=function(){console.log("%s - %s",timestamp(),exports.format.apply(exports,arguments))};exports.inherits=require("inherits");exports._extend=function(origin,add){if(!add||!isObject(add))return origin;var keys=Object.keys(add);var i=keys.length;while(i--){origin[keys[i]]=add[keys[i]]}return origin};function hasOwnProperty(obj,prop){return Object.prototype.hasOwnProperty.call(obj,prop)}}).call(this,require("_process"),typeof global!=="undefined"?global:typeof self!=="undefined"?self:typeof window!=="undefined"?window:{})},{"./support/isBuffer":46,_process:45,inherits:43}]},{},[33])(33)})); \ No newline at end of file +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.acebase=f()}})((function(){var define,module,exports;return function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,(function(r){var n=e[i][1][r];return o(n||r)}),p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i=7&&stringLength<=10)return true;return false};cuid.fingerprint=fingerprint;module.exports=cuid},{"./lib/fingerprint.js":2,"./lib/pad.js":3}],2:[function(require,module,exports){var pad=require("./pad.js");var env=typeof window==="object"?window:self;var globalCount=Object.keys(env).length;var mimeTypesLength=navigator.mimeTypes?navigator.mimeTypes.length:0;var clientId=pad((mimeTypesLength+navigator.userAgent.length).toString(36)+globalCount.toString(36),4);module.exports=function fingerprint(){return clientId}},{"./pad.js":3}],3:[function(require,module,exports){module.exports=function pad(num,size){var s="000000000"+num;return s.substr(s.length-size)}},{}],4:[function(require,module,exports){const{EventEmitter:EventEmitter}=require("events");const{DataReference:DataReference,DataReferenceQuery:DataReferenceQuery}=require("./data-reference");const{TypeMappings:TypeMappings}=require("./type-mappings");class AceBaseSettings{constructor(options){this.logLevel=options.logLevel||"log"}}class AceBaseBase extends EventEmitter{constructor(dbname,options){super();if(!options){options={}}this.once("ready",()=>{this._ready=true});this.types=new TypeMappings(this)}ready(callback=undefined){if(this._ready===true){callback&&callback();return Promise.resolve()}else{let resolve;const promise=new Promise(res=>resolve=res);this.on("ready",()=>{resolve();callback&&callback()});return promise}}get isReady(){return this._ready===true}ref(path){return new DataReference(this,path)}get root(){return this.ref("")}query(path){const ref=new DataReference(this,path);return new DataReferenceQuery(ref)}get indexes(){return{get:()=>{return this.api.getIndexes()},create:(path,key,options)=>{return this.api.createIndex(path,key,options)}}}}module.exports={AceBaseBase:AceBaseBase,AceBaseSettings:AceBaseSettings}},{"./data-reference":7,"./type-mappings":16,events:42}],5:[function(require,module,exports){class Api{stats(options=undefined){}subscribe(path,event,callback){}unsubscribe(path,event,callback){}update(path,updates){}set(path,value){}get(path,options){}exists(path){}query(path,query,options){}createIndex(path,key){}getIndexes(){}}module.exports={Api:Api}},{}],6:[function(require,module,exports){const c=function(input,length,result){var i,j,n,b=[0,0,0,0,0];for(i=0;i";return ret},decode:function(input){if(!input.startsWith("<~")||!input.endsWith("~>")){throw new Error("Invalid input string")}input=input.substr(2,input.length-4);var n=input.length,r=[],b=[0,0,0,0,0],i,j,t,x,y,d;for(i=0;i>>=8;y=t&255;t>>>=8;r.push(t>>>8,t&255,y,x);for(j=d;j<5;++j,r.pop());i+=4}const data=new Uint8Array(r);return data.buffer.slice(data.byteOffset,data.byteOffset+data.byteLength)}};module.exports=ascii85},{}],7:[function(require,module,exports){const{DataSnapshot:DataSnapshot}=require("./data-snapshot");const{EventStream:EventStream,EventPublisher:EventPublisher}=require("./subscription");const{ID:ID}=require("./id");const debug=require("./debug");const{PathInfo:PathInfo}=require("./path-info");class DataRetrievalOptions{constructor(options){if(!options){options={}}if(typeof options.include!=="undefined"&&!(options.include instanceof Array)){throw new TypeError(`options.include must be an array`)}if(typeof options.exclude!=="undefined"&&!(options.exclude instanceof Array)){throw new TypeError(`options.exclude must be an array`)}if(typeof options.child_objects!=="undefined"&&typeof options.child_objects!=="boolean"){throw new TypeError(`options.child_objects must be a boolean`)}if(typeof options.allow_cache!=="undefined"&&typeof options.allow_cache!=="boolean"){throw new TypeError(`options.allow_cache must be a boolean`)}this.include=options.include||undefined;this.exclude=options.exclude||undefined;this.child_objects=typeof options.child_objects==="boolean"?options.child_objects:undefined;this.allow_cache=typeof options.allow_cache==="boolean"?options.allow_cache:undefined}}class QueryDataRetrievalOptions extends DataRetrievalOptions{constructor(options){super(options);if(typeof options.snapshots!=="undefined"&&typeof options.snapshots!=="boolean"){throw new TypeError(`options.snapshots must be an array`)}this.snapshots=typeof options.snapshots==="boolean"?options.snapshots:undefined}}const _private=Symbol("private");class DataReference{constructor(db,path,vars){if(!path){path=""}path=path.replace(/^\/|\/$/g,"");const pathInfo=PathInfo.get(path);const key=pathInfo.key;const callbacks=[];this[_private]={get path(){return path},get key(){return key},get callbacks(){return callbacks},vars:vars||{}};this.db=db}get path(){return this[_private].path}get key(){return this[_private].key}get parent(){let currentPath=PathInfo.fillVariables2(this.path,this.vars);const info=PathInfo.get(currentPath);if(info.parentPath===null){return null}return new DataReference(this.db,info.parentPath)}get vars(){return this[_private].vars}child(childPath){childPath=childPath.replace(/^\/|\/$/g,"");const currentPath=PathInfo.fillVariables2(this.path,this.vars);const targetPath=PathInfo.getChildPath(currentPath,childPath);return new DataReference(this.db,targetPath)}set(value,onComplete=undefined){if(this.isWildcardPath){throw new Error(`Cannot set the value of a path with wildcards and/or variables`)}if(this.parent===null){throw new Error(`Cannot set the root object. Use update, or set individual child properties`)}if(typeof value==="undefined"){throw new TypeError(`Cannot store value undefined`)}value=this.db.types.serialize(this.path,value);return this.db.api.set(this.path,value).then(res=>{if(typeof onComplete==="function"){try{onComplete(null,this)}catch(err){console.error(`Error in onComplete callback:`,err)}}}).catch(err=>{if(typeof onComplete==="function"){try{onComplete(err)}catch(err){console.error(`Error in onComplete callback:`,err)}}else{throw err}}).then(()=>{return this})}update(updates,onComplete=undefined){if(this.isWildcardPath){throw new Error(`Cannot update the value of a path with wildcards and/or variables`)}let promise;if(typeof updates!=="object"||updates instanceof Array||updates instanceof ArrayBuffer||updates instanceof Date){promise=this.set(updates)}else{updates=this.db.types.serialize(this.path,updates);promise=this.db.api.update(this.path,updates)}return promise.then(()=>{if(typeof onComplete==="function"){try{onComplete(null,this)}catch(err){console.error(`Error in onComplete callback:`,err)}}}).catch(err=>{if(typeof onComplete==="function"){try{onComplete(err)}catch(err){console.error(`Error in onComplete callback:`,err)}}else{throw err}}).then(()=>{return this})}transaction(callback){if(this.isWildcardPath){throw new Error(`Cannot start a transaction on a path with wildcards and/or variables`)}let cb=currentValue=>{currentValue=this.db.types.deserialize(this.path,currentValue);const snap=new DataSnapshot(this,currentValue);let newValue;try{newValue=callback(snap)}catch(err){return}if(newValue instanceof Promise){return newValue.then(val=>{return this.db.types.serialize(this.path,val)})}else{return this.db.types.serialize(this.path,newValue)}};return this.db.api.transaction(this.path,cb).then(result=>{return this})}on(event,callback,cancelCallbackOrContext,context){if(this.path===""&&["value","notify_value","child_changed","notify_child_changed"].includes(event)){console.warn(`WARNING: Listening for value and child_changed events on the root node is a bad practice`)}const cancelCallback=typeof cancelCallbackOrContext==="function"&&cancelCallbackOrContext;context=typeof cancelCallbackOrContext==="object"?cancelCallbackOrContext:context;const useCallback=typeof callback==="function";let eventPublisher=null;const eventStream=new EventStream(publisher=>{eventPublisher=publisher});let cb={subscr:eventStream,original:callback,ours:(err,path,newValue,oldValue)=>{if(err){debug.error(`Error getting data for event ${event} on path "${path}"`,err);return}let ref=this.db.ref(path);ref[_private].vars=PathInfo.extractVariables(this.path,path);let callbackObject;if(event.startsWith("notify_")){callbackObject=ref}else{const isRemoved=event==="child_removed";const val=this.db.types.deserialize(path,isRemoved?oldValue:newValue);const snap=new DataSnapshot(ref,val,isRemoved);callbackObject=snap}useCallback&&callback.call(context||null,callbackObject);let keep=eventPublisher.publish(callbackObject);if(!keep&&!useCallback){let callbacks=this[_private].callbacks;callbacks.splice(callbacks.indexOf(cb),1);this.db.api.unsubscribe(this.path,event,cb.ours)}}};this[_private].callbacks.push(cb);let authorized=this.db.api.subscribe(this.path,event,cb.ours);const allSubscriptionsStoppedCallback=()=>{let callbacks=this[_private].callbacks;callbacks.splice(callbacks.indexOf(cb),1);this.db.api.unsubscribe(this.path,event,cb.ours)};if(authorized instanceof Promise){authorized.then(()=>{eventPublisher.start(allSubscriptionsStoppedCallback)}).catch(err=>{let callbacks=this[_private].callbacks;callbacks.splice(callbacks.indexOf(cb),1);this.db.api.unsubscribe(this.path,event,cb.ours);eventPublisher.cancel(err.message);cancelCallback&&cancelCallback(err.message)})}else{eventPublisher.start(allSubscriptionsStoppedCallback)}if(callback&&!this.isWildcardPath){if(event==="value"){this.get(snap=>{eventPublisher.publish(snap);useCallback&&callback(snap)})}else if(event==="child_added"){this.get(snap=>{const val=snap.val();if(val===null||typeof val!=="object"){return}Object.keys(val).forEach(key=>{let childSnap=new DataSnapshot(this.child(key),val[key]);eventPublisher.publish(childSnap);useCallback&&callback(childSnap)})})}else if(event==="notify_child_added"){const step=100;let limit=step,skip=0;const more=()=>{this.db.api.reflect(this.path,"children",{limit:limit,skip:skip}).then(children=>{children.list.forEach(child=>{const childRef=this.child(child.key);eventPublisher.publish(childRef);useCallback&&callback(childRef)});if(children.more){skip+=step;more()}})};more()}}return eventStream}off(event=undefined,callback=undefined){const callbacks=this[_private].callbacks;if(callback){const cb=callbacks.find(cb=>cb.original===callback);if(!cb){debug.error(`Can't find specified callback to unsubscribe from (path: "${this.path}", event: ${event}, callback: ${callback})`);return}callbacks.splice(callbacks.indexOf(cb),1);callback=cb.ours;cb.subscr.unsubscribe(callback)}else{callbacks.splice(0,callbacks.length).forEach(cb=>{cb.subscr.unsubscribe()})}this.db.api.unsubscribe(this.path,event,callback);return this}get(optionsOrCallback=undefined,callback=undefined){if(this.isWildcardPath){throw new Error(`Cannot get the value of a path with wildcards and/or variables. Use .query() instead`)}callback=typeof optionsOrCallback==="function"?optionsOrCallback:typeof callback==="function"?callback:undefined;const options=typeof optionsOrCallback==="object"?optionsOrCallback:new DataRetrievalOptions({allow_cache:true});if(typeof options.allow_cache==="undefined"){options.allow_cache=true}const promise=this.db.api.get(this.path,options).then(value=>{value=this.db.types.deserialize(this.path,value);const snapshot=new DataSnapshot(this,value);return snapshot});if(callback){promise.then(callback);return}else{return promise}}once(event,options){if(event==="value"&&!this.isWildcardPath){return this.get(options)}return new Promise((resolve,reject)=>{const callback=snap=>{this.off(event,snap);resolve(snap)};this.on(event,callback)})}push(value=undefined,onComplete=undefined){if(this.isWildcardPath){throw new Error(`Cannot push to a path with wildcards and/or variables`)}const id=ID.generate();const ref=this.child(id);ref.__pushed=true;if(typeof value!=="undefined"){return ref.set(value,onComplete).then(res=>ref)}else{return ref}}remove(){if(this.isWildcardPath){throw new Error(`Cannot remove a path with wildcards and/or variables. Use query().remove instead`)}if(this.parent===null){throw new Error(`Cannot remove the top node`)}return this.set(null)}exists(){if(this.isWildcardPath){throw new Error(`Cannot push to a path with wildcards and/or variables`)}return this.db.api.exists(this.path)}get isWildcardPath(){return this.path.indexOf("*")>=0||this.path.indexOf("$")>=0}query(){return new DataReferenceQuery(this)}reflect(type,args){if(this.pathHasVariables){throw new Error(`Cannot reflect on a path with wildcards and/or variables`)}return this.db.api.reflect(this.path,type,args)}export(stream,options={format:"json"}){return this.db.api.export(this.path,stream,options)}}class DataReferenceQuery{constructor(ref){this.ref=ref;this[_private]={filters:[],skip:0,take:0,order:[]}}filter(key,op,compare){if((op==="in"||op==="!in")&&(!(compare instanceof Array)||compare.length===0)){throw new Error(`${op} filter for ${key} must supply an Array compare argument containing at least 1 value`)}if((op==="between"||op==="!between")&&(!(compare instanceof Array)||compare.length!==2)){throw new Error(`${op} filter for ${key} must supply an Array compare argument containing 2 values`)}if((op==="matches"||op==="!matches")&&!(compare instanceof RegExp)){throw new Error(`${op} filter for ${key} must supply a RegExp compare argument`)}if((op==="contains"||op==="!contains")&&(typeof compare==="object"&&!(compare instanceof Array)&&!(compare instanceof Date)||compare instanceof Array&&compare.length===0)){throw new Error(`${op} filter for ${key} must supply a simple value or (non-zero length) array compare argument`)}this[_private].filters.push({key:key,op:op,compare:compare});return this}where(key,op,compare){return this.filter(key,op,compare)}take(n){this[_private].take=n;return this}skip(n){this[_private].skip=n;return this}sort(key,ascending=true){if(typeof key!=="string"){throw`key must be a string`}this[_private].order.push({key:key,ascending:ascending});return this}order(key,ascending=true){return this.sort(key,ascending)}get(optionsOrCallback=undefined,callback=undefined){callback=typeof optionsOrCallback==="function"?optionsOrCallback:typeof callback==="function"?callback:undefined;const options=typeof optionsOrCallback==="object"?optionsOrCallback:new QueryDataRetrievalOptions({snapshots:true,allow_cache:true});if(typeof options.snapshots==="undefined"){options.snapshots=true}if(typeof options.allow_cache==="undefined"){options.allow_cache=true}options.eventHandler=ev=>{if(!this._events||!this._events[ev.name]){return false}const listeners=this._events[ev.name];if(typeof listeners!=="object"||listeners.length===0){return false}if(["add","change","remove"].includes(ev.name)){const ref=new DataReference(this.ref.db,ev.path);const eventData={name:ev.name};if(options.snapshots&&ev.name!=="remove"){const val=db.types.deserialize(ev.path,ev.value);eventData.snapshot=new DataSnapshot(ref,val,false)}else{eventData.ref=ref}ev=eventData}listeners.forEach(callback=>{try{callback(ev)}catch(e){}})};options.monitor={add:false,change:false,remove:false};if(this._events){if(this._events["add"]&&this._events["add"].length>0){options.monitor.add=true}if(this._events["change"]&&this._events["change"].length>0){options.monitor.change=true}if(this._events["remove"]&&this._events["remove"].length>0){options.monitor.remove=true}}const db=this.ref.db;return db.api.query(this.ref.path,this[_private],options).catch(err=>{throw new Error(err)}).then(results=>{results.forEach((result,index)=>{if(options.snapshots){const val=db.types.deserialize(result.path,result.val);results[index]=new DataSnapshot(db.ref(result.path),val)}else{results[index]=db.ref(result)}});if(options.snapshots){return DataSnapshotsArray.from(results)}else{return DataReferencesArray.from(results)}}).then(results=>{callback&&callback(results);return results})}getRefs(callback=undefined){return this.get({snapshots:false},callback)}remove(callback){return this.get({snapshots:false}).then(refs=>{const promises=[];return Promise.all(refs.map(ref=>ref.remove())).then(()=>{callback&&callback()})})}on(event,callback){if(!this._events){this._events={}}if(!this._events[event]){this._events[event]=[]}this._events[event].push(callback);return this}off(event,callback){if(!this._events){return this}if(typeof event==="undefined"){this._events={};return this}if(!this._events[event]){return this}if(typeof callback==="undefined"){delete this._events[event];return this}const index=!this._events[event].indexOf(callback);if(!~index){return this}this._events[event].splice(index,1);return this}}class DataSnapshotsArray extends Array{static from(snaps){const arr=new DataSnapshotsArray(snaps.length);snaps.forEach((snap,i)=>arr[i]=snap);return arr}getValues(){return this.map(snap=>snap.val())}}class DataReferencesArray extends Array{static from(refs){const arr=new DataReferencesArray(refs.length);refs.forEach((ref,i)=>arr[i]=ref);return arr}getPaths(){return this.map(ref=>ref.path)}}module.exports={DataReference:DataReference,DataReferenceQuery:DataReferenceQuery,DataRetrievalOptions:DataRetrievalOptions,QueryDataRetrievalOptions:QueryDataRetrievalOptions}},{"./data-snapshot":8,"./debug":9,"./id":10,"./path-info":12,"./subscription":14}],8:[function(require,module,exports){const{DataReference:DataReference}=require("./data-reference");const{getPathKeys:getPathKeys}=require("./path-info");const getChild=(snapshot,path)=>{if(!snapshot.exists()){return null}let child=snapshot.val();getPathKeys(path).every(key=>{child=child[key];return typeof child!=="undefined"});return child||null};const getChildren=snapshot=>{if(!snapshot.exists()){return[]}let value=snapshot.val();if(value instanceof Array){return new Array(value.length).map((v,i)=>i)}if(typeof value==="object"){return Object.keys(value)}return[]};class DataSnapshot{constructor(ref,value,isRemoved=false){this.ref=ref;this.val=()=>{return value};this.exists=()=>{if(isRemoved){return false}return value!==null&&typeof value!=="undefined"}}child(path){let child=getChild(this,path);return new DataSnapshot(this.ref.child(path),child)}hasChild(path){return getChild(this,path)!==null}hasChildren(){return getChildren(this).length>0}numChildren(){return getChildren(this).length}forEach(callback){const value=this.val();return getChildren(this).every((key,i)=>{const snap=new DataSnapshot(this.ref.child(key),value[key]);return callback(snap)})}get key(){return this.ref.key}}module.exports={DataSnapshot:DataSnapshot}},{"./data-reference":7,"./path-info":12}],9:[function(require,module,exports){class DebugLogger{constructor(level="log",prefix=""){this.prefix=prefix;this.setLevel(level)}setLevel(level){const prefix=this.prefix?this.prefix:"";this.level=level;this.verbose=["verbose"].includes(level)?console.log.bind(console,prefix):()=>{};this.log=["verbose","log"].includes(level)?console.log.bind(console,prefix):()=>{};this.warn=["verbose","log","warn"].includes(level)?console.warn.bind(console,prefix):()=>{};this.error=["verbose","log","warn","error"].includes(level)?console.error.bind(console,prefix):()=>{};this.write=console.log.bind(console)}}module.exports=DebugLogger},{}],10:[function(require,module,exports){const cuid=require("cuid");class ID{static generate(){return cuid().slice(1)}}module.exports={ID:ID}},{cuid:1}],11:[function(require,module,exports){const{AceBaseBase:AceBaseBase,AceBaseSettings:AceBaseSettings}=require("./acebase-base");const{Api:Api}=require("./api");const{DataReference:DataReference,DataReferenceQuery:DataReferenceQuery,DataRetrievalOptions:DataRetrievalOptions,QueryDataRetrievalOptions:QueryDataRetrievalOptions}=require("./data-reference");const{DataSnapshot:DataSnapshot}=require("./data-snapshot");const DebugLogger=require("./debug");const{ID:ID}=require("./id");const{PathReference:PathReference}=require("./path-reference");const{EventStream:EventStream,EventPublisher:EventPublisher,EventSubscription:EventSubscription}=require("./subscription");const Transport=require("./transport");const{TypeMappings:TypeMappings,TypeMappingOptions:TypeMappingOptions}=require("./type-mappings");const Utils=require("./utils");const{PathInfo:PathInfo}=require("./path-info");const ascii85=require("./ascii85");module.exports={AceBaseBase:AceBaseBase,AceBaseSettings:AceBaseSettings,Api:Api,DataReference:DataReference,DataReferenceQuery:DataReferenceQuery,DataRetrievalOptions:DataRetrievalOptions,QueryDataRetrievalOptions:QueryDataRetrievalOptions,DataSnapshot:DataSnapshot,DebugLogger:DebugLogger,ID:ID,PathReference:PathReference,EventStream:EventStream,EventPublisher:EventPublisher,EventSubscription:EventSubscription,Transport:Transport,TypeMappings:TypeMappings,TypeMappingOptions:TypeMappingOptions,Utils:Utils,PathInfo:PathInfo,ascii85:ascii85}},{"./acebase-base":4,"./api":5,"./ascii85":6,"./data-reference":7,"./data-snapshot":8,"./debug":9,"./id":10,"./path-info":12,"./path-reference":13,"./subscription":14,"./transport":15,"./type-mappings":16,"./utils":17}],12:[function(require,module,exports){function getPathKeys(path){if(path.length===0){return[]}let keys=path.replace(/\[/g,"/[").split("/");keys.forEach((key,index)=>{if(key.startsWith("[")){keys[index]=parseInt(key.substr(1,key.length-2))}});return keys}function getPathInfo(path){if(path.length===0){return{parent:null,key:""}}const i=Math.max(path.lastIndexOf("/"),path.lastIndexOf("["));const parentPath=i<0?"":path.substr(0,i);let key=i<0?path:path.substr(i);if(key.startsWith("[")){key=parseInt(key.substr(1,key.length-2))}else if(key.startsWith("/")){key=key.substr(1)}if(parentPath===path){parentPath=null}return{parent:parentPath,key:key}}function getChildPath(path,key){if(path.length===0){if(typeof key==="number"){throw new TypeError("Cannot add array index to root path!")}return key}if(typeof key==="number"){return`${path}[${key}]`}return`${path}/${key}`}class PathInfo{static get(path){return new PathInfo(path)}static getChildPath(path,childKey){return getChildPath(path,childKey)}static getPathKeys(path){return getPathKeys(path)}constructor(path){this.path=path}get key(){return getPathInfo(this.path).key}get parentPath(){return getPathInfo(this.path).parent}childPath(childKey){return getChildPath(`${this.path}`,childKey)}get pathKeys(){return getPathKeys(this.path)}static extractVariables(varPath,fullPath){if(!varPath.includes("*")&&!varPath.includes("$")){return[]}const keys=getPathKeys(varPath);const pathKeys=getPathKeys(fullPath);let count=0;const variables={get length(){return count}};keys.forEach((key,index)=>{const pathKey=pathKeys[index];if(key==="*"){variables[count++]=pathKey}else if(typeof key==="string"&&key[0]==="$"){variables[count++]=pathKey;variables[key]=pathKey;const varName=key.slice(1);if(typeof variables[varName]==="undefined"){variables[varName]=pathKey}}});return variables}static fillVariables(varPath,fullPath){if(varPath.indexOf("*")<0&&varPath.indexOf("$")<0){return varPath}const keys=getPathKeys(varPath);const pathKeys=getPathKeys(fullPath);let merged=keys.map((key,index)=>{if(key===pathKeys[index]||index>=pathKeys.length){return key}else if(typeof key==="string"&&(key==="*"||key[0]==="$")){return pathKeys[index]}else{throw new Error(`Path "${fullPath}" cannot be used to fill variables of path "${this.path}" because they do not match`)}});let mergedPath="";merged.forEach(key=>{if(typeof key==="number"){mergedPath+=`[${key}]`}else{if(mergedPath.length>0){mergedPath+="/"}mergedPath+=key}});return mergedPath}static fillVariables2(varPath,vars){if(typeof vars!=="object"||Object.keys(vars).length===0){return varPath}let pathKeys=getPathKeys(varPath);let n=0;const targetPath=pathKeys.reduce((path,key)=>{if(key==="*"||key.startsWith("$")){key=vars[n++]}if(typeof key==="number"){return`${path}[${key}]`}else{return`${path}/${key}`}},"");return targetPath}equals(otherPath){if(this.path===otherPath){return true}const keys=getPathKeys(this.path);const otherKeys=getPathKeys(otherPath);if(keys.length!==otherKeys.length){return false}return keys.every((key,index)=>{const otherKey=otherKeys[index];return otherKey===key||typeof otherKey==="string"&&(otherKey==="*"||otherKey[0]==="$")||typeof key==="string"&&(key==="*"||key[0]==="$")})}isAncestorOf(descendantPath){if(descendantPath===""||this.path===descendantPath){return false}if(this.path===""){return true}const ancestorKeys=getPathKeys(this.path);const descendantKeys=getPathKeys(descendantPath);if(ancestorKeys.length>=descendantKeys.length){return false}return ancestorKeys.every((key,index)=>{const otherKey=descendantKeys[index];return otherKey===key||typeof otherKey==="string"&&(otherKey==="*"||otherKey[0]==="$")||typeof key==="string"&&(key==="*"||key[0]==="$")})}isDescendantOf(ancestorPath){if(this.path===""||this.path===ancestorPath){return false}if(ancestorPath===""){return true}const ancestorKeys=getPathKeys(ancestorPath);const descendantKeys=getPathKeys(this.path);if(ancestorKeys.length>=descendantKeys.length){return false}return ancestorKeys.every((key,index)=>{const otherKey=descendantKeys[index];return otherKey===key||typeof otherKey==="string"&&(otherKey==="*"||otherKey[0]==="$")||typeof key==="string"&&(key==="*"||key[0]==="$")})}isChildOf(otherPath){if(this.path===""){return false}const parentInfo=PathInfo.get(this.parentPath);return parentInfo.equals(otherPath)}isParentOf(otherPath){if(otherPath===""){return false}const parentInfo=PathInfo.get(PathInfo.get(otherPath).parentPath);return parentInfo.equals(this.path)}}module.exports={getPathInfo:getPathInfo,getChildPath:getChildPath,getPathKeys:getPathKeys,PathInfo:PathInfo}},{}],13:[function(require,module,exports){class PathReference{constructor(path){this.path=path}}module.exports={PathReference:PathReference}},{}],14:[function(require,module,exports){class EventSubscription{constructor(stop){this.stop=stop;this._internal={state:"init",cancelReason:undefined,activatePromises:[]}}activated(callback=undefined){if(callback){this._internal.activatePromises.push({callback:callback});if(this._internal.state==="active"){callback(true)}else if(this._internal.state==="canceled"){callback(false,this._internal.cancelReason)}}return new Promise((resolve,reject)=>{if(this._internal.state==="active"){return resolve()}else if(this._internal.state==="canceled"&&!callback){return reject(new Error(this._internal.cancelReason))}this._internal.activatePromises.push({resolve:resolve,reject:callback?()=>{}:reject})})}_setActivationState(activated,cancelReason){this._internal.cancelReason=cancelReason;this._internal.state=activated?"active":"canceled";while(this._internal.activatePromises.length>0){const p=this._internal.activatePromises.shift();if(activated){p.callback&&p.callback(true);p.resolve&&p.resolve()}else{p.callback&&p.callback(false,cancelReason);p.reject&&p.reject(cancelReason)}}}}class EventPublisher{constructor(publish,start,cancel){this.publish=publish;this.start=start;this.cancel=cancel}}class EventStream{constructor(eventPublisherCallback){const subscribers=[];let noMoreSubscribersCallback;let activationState;const _stoppedState="stopped (no more subscribers)";this.subscribe=(callback,activationCallback)=>{if(typeof callback!=="function"){throw new TypeError("callback must be a function")}else if(activationState===_stoppedState){throw new Error("stream can't be used anymore because all subscribers were stopped")}const sub={callback:callback,activationCallback:function(activated,cancelReason){activationCallback&&activationCallback(activated,cancelReason);this.subscription._setActivationState(activated,cancelReason)},subscription:new EventSubscription((function stop(){subscribers.splice(subscribers.indexOf(this),1);checkActiveSubscribers()}))};subscribers.push(sub);if(typeof activationState!=="undefined"){if(activationState===true){activationCallback&&activationCallback(true);sub.subscription._setActivationState(true)}else if(typeof activationState==="string"){activationCallback&&activationCallback(false,activationState);sub.subscription._setActivationState(false,activationState)}}return sub.subscription};const checkActiveSubscribers=()=>{if(subscribers.length===0){noMoreSubscribersCallback&&noMoreSubscribersCallback();activationState=_stoppedState}};this.unsubscribe=(callback=undefined)=>{const remove=callback?subscribers.filter(sub=>sub.callback===callback):subscribers;remove.forEach(sub=>{const i=subscribers.indexOf(sub);subscribers.splice(i,1)});checkActiveSubscribers()};this.stop=()=>{subscribers.splice(0);checkActiveSubscribers()};const publish=val=>{subscribers.forEach(sub=>{try{sub.callback(val)}catch(err){debug.error(`Error running subscriber callback: ${err.message}`)}});return subscribers.length>0};const start=allSubscriptionsStoppedCallback=>{activationState=true;noMoreSubscribersCallback=allSubscriptionsStoppedCallback;subscribers.forEach(sub=>{sub.activationCallback&&sub.activationCallback(true)})};const cancel=reason=>{activationState=reason;subscribers.forEach(sub=>{sub.activationCallback&&sub.activationCallback(false,reason||new Error("unknown reason"))});subscribers.splice()};const publisher=new EventPublisher(publish,start,cancel);eventPublisherCallback(publisher)}}module.exports={EventStream:EventStream,EventPublisher:EventPublisher,EventSubscription:EventSubscription}},{}],15:[function(require,module,exports){const{PathReference:PathReference}=require("./path-reference");const{cloneObject:cloneObject}=require("./utils");const ascii85=require("./ascii85");module.exports={deserialize(data){if(data.map===null||typeof data.map==="undefined"){return data.val}const deserializeValue=(type,val)=>{if(type==="date"){return new Date(val)}else if(type==="binary"){return ascii85.decode(val)}else if(type==="reference"){return new PathReference(val)}else if(type==="regexp"){return new RegExp(val.pattern,val.flags)}return val};if(typeof data.map==="string"){return deserializeValue(data.map,data.val)}Object.keys(data.map).forEach(path=>{const type=data.map[path];const keys=path.replace(/\[/g,"/[").split("/");keys.forEach((key,index)=>{if(key.startsWith("[")){keys[index]=parseInt(key.substr(1,key.length-2))}});let parent=data;let key="val";let val=data.val;keys.forEach(k=>{key=k;parent=val;val=val[key]});parent[key]=deserializeValue(type,val)});return data.val},serialize(obj){if(obj===null||typeof obj!=="object"||obj instanceof Date||obj instanceof ArrayBuffer||obj instanceof PathReference){const ser=this.serialize({value:obj});return{map:ser.map.value,val:ser.val.value}}obj=cloneObject(obj);const process=(obj,mappings,prefix)=>{Object.keys(obj).forEach(key=>{const val=obj[key];const path=prefix.length===0?key:`${prefix}/${key}`;if(val instanceof Date){obj[key]=val.toISOString();mappings[path]="date"}else if(val instanceof ArrayBuffer){obj[key]=ascii85.encode(val);mappings[path]="binary"}else if(val instanceof PathReference){obj[key]=val.path;mappings[path]="reference"}else if(val instanceof RegExp){obj[key]={pattern:val.source,flags:val.flags};mappings[path]="regexp"}else if(typeof val==="object"&&val!==null){process(val,mappings,path)}})};const mappings={};process(obj,mappings,"");return{map:mappings,val:obj}}}},{"./ascii85":6,"./path-reference":13,"./utils":17}],16:[function(require,module,exports){const{cloneObject:cloneObject}=require("./utils");const{PathInfo:PathInfo}=require("./path-info");const{AceBaseBase:AceBaseBase}=require("./acebase-base");const{DataReference:DataReference}=require("./data-reference");const{DataSnapshot:DataSnapshot}=require("./data-snapshot");const get=(mappings,path)=>{path=path.replace(/^\/|\/$/g,"");const keys=PathInfo.getPathKeys(path);const mappedPath=Object.keys(mappings).find(mpath=>{const mkeys=PathInfo.getPathKeys(mpath);if(mkeys.length!==keys.length){return false}return mkeys.every((mkey,index)=>{if(mkey==="*"||mkey[0]==="$"){return true}return mkey===keys[index]})});const mapping=mappings[mappedPath];return mapping};const map=(mappings,path)=>{const targetPath=PathInfo.get(path).parentPath;if(targetPath===null){return}return get(mappings,targetPath)};const mapDeep=(mappings,entryPath)=>{entryPath=entryPath.replace(/^\/|\/$/g,"");const pathInfo=PathInfo.get(entryPath);const startPath=pathInfo.parentPath;const keys=startPath?PathInfo.getPathKeys(startPath):[];const matches=Object.keys(mappings).reduce((m,mpath)=>{const mkeys=PathInfo.getPathKeys(mpath);if(mkeys.length{if(index>=keys.length){return false}else if(mkey==="*"||mkey[0]==="$"||mkey===keys[index]){return true}else{isMatch=false;return false}})}if(isMatch){const mapping=mappings[mpath];m.push({path:mpath,type:mapping})}return m},[]);return matches};const process=(db,mappings,path,obj,action)=>{if(obj===null||typeof obj!=="object"){return obj}const keys=PathInfo.getPathKeys(path);const m=mapDeep(mappings,path);const changes=[];m.sort((a,b)=>PathInfo.getPathKeys(a.path).length>PathInfo.getPathKeys(b.path).length?-1:1);m.forEach(mapping=>{const mkeys=PathInfo.getPathKeys(mapping.path);mkeys.push("*");const mTrailKeys=mkeys.slice(keys.length);if(mTrailKeys.length===0){const vars=PathInfo.extractVariables(mapping.path,path);const ref=new DataReference(db,path,vars);if(action==="serialize"){obj=mapping.type.serialize(obj,ref)}else if(action==="deserialize"){const snap=new DataSnapshot(ref,obj);obj=mapping.type.deserialize(snap)}return}const process=(parentPath,parent,keys)=>{if(obj===null||typeof obj!=="object"){return obj}const key=keys[0];let children=[];if(key==="*"||key[0]==="$"){if(parent instanceof Array){children=parent.map((val,index)=>({key:index,val:val}))}else{children=Object.keys(parent).map(k=>({key:k,val:parent[k]}))}}else{const child=parent[key];if(typeof child==="object"){children.push({key:key,val:child})}}children.forEach(child=>{const childPath=PathInfo.getChildPath(parentPath,child.key);const vars=PathInfo.extractVariables(mapping.path,childPath);const ref=new DataReference(db,childPath,vars);if(keys.length===1){if(action==="serialize"){changes.push({parent:parent,key:child.key,original:parent[child.key]});parent[child.key]=mapping.type.serialize(child.val,ref)}else if(action==="deserialize"){const snap=new DataSnapshot(ref,child.val);parent[child.key]=mapping.type.deserialize(snap)}}else{process(childPath,child.val,keys.slice(1))}})};process(path,obj,mTrailKeys)});if(action==="serialize"){obj=cloneObject(obj);if(changes.length>0){changes.forEach(change=>{change.parent[change.key]=change.original})}}return obj};class TypeMappingOptions{constructor(options){if(!options){options={}}this.serializer=options.serializer;this.creator=options.creator}}const _mappings=Symbol("mappings");class TypeMappings{constructor(db){this.db=db;this[_mappings]={}}get mappings(){return this[_mappings]}map(path){return map(this[_mappings],path)}bind(path,type,options={}){if(typeof path!=="string"){throw new TypeError("path must be a string")}if(typeof type!=="function"){throw new TypeError("constructor must be a function")}if(typeof options.serializer==="undefined"){}else if(typeof options.serializer==="string"){if(typeof type.prototype[options.serializer]==="function"){options.serializer=type.prototype[options.serializer]}else{throw new TypeError(`${type.name}.prototype.${options.serializer} is not a function, cannot use it as serializer`)}}else if(typeof options.serializer!=="function"){throw new TypeError(`serializer for class ${type.name} must be a function, or the name of a prototype method`)}if(typeof options.creator==="undefined"){if(typeof type.create==="function"){options.creator=type.create}}else if(typeof options.creator==="string"){if(typeof type[options.creator]==="function"){options.creator=type[options.creator]}else{throw new TypeError(`${type.name}.${options.creator} is not a function, cannot use it as creator`)}}else if(typeof options.creator!=="function"){throw new TypeError(`creator for class ${type.name} must be a function, or the name of a static method`)}path=path.replace(/^\/|\/$/g,"");this[_mappings][path]={db:this.db,type:type,creator:options.creator,serializer:options.serializer,deserialize(snap){let obj;if(this.creator){obj=this.creator.call(this.type,snap)}else{obj=new this.type(snap)}return obj},serialize(obj,ref){if(this.serializer){obj=this.serializer.call(obj,ref,obj)}else if(obj&&typeof obj.serialize==="function"){obj=obj.serialize(ref,obj)}return obj}}}serialize(path,obj){return process(this.db,this[_mappings],path,obj,"serialize")}deserialize(path,obj){return process(this.db,this[_mappings],path,obj,"deserialize")}}module.exports={TypeMappings:TypeMappings,TypeMappingOptions:TypeMappingOptions}},{"./acebase-base":4,"./data-reference":7,"./data-snapshot":8,"./path-info":12,"./utils":17}],17:[function(require,module,exports){(function(Buffer){const{PathReference:PathReference}=require("./path-reference");function numberToBytes(number){const bytes=new Uint8Array(8);const view=new DataView(bytes.buffer);view.setFloat64(0,number);return new Array(...bytes)}function bytesToNumber(bytes){if(bytes.length<8){throw new TypeError("must be 8 bytes")}const bin=new Uint8Array(bytes);const view=new DataView(bin.buffer);const nr=view.getFloat64(0);return nr}function encodeString(str){if(typeof TextEncoder!=="undefined"){const encoder=new TextEncoder;return encoder.encode(str)}else if(typeof Buffer==="function"){const buf=Buffer.from(str,"utf-8");return new Uint8Array(buf.buffer,buf.byteOffset,buf.byteLength)}else{let arr=[];for(let i=0;i128){if((code&55296)===55296){const nextCode=str.charCodeAt(i+1);if((nextCode&56320)!==56320){throw new Error("follow-up utf-16 character does not start with 0xDC00")}i++;const p1=code&1023;const p2=nextCode&1023;code=65536|p1<<10|p2}if(code<2048){const b1=192|code>>6&31;const b2=128|code&63;arr.push(b1,b2)}else if(code<65536){const b1=224|code>>12&15;const b2=128|code>>6&63;const b3=128|code&63;arr.push(b1,b2,b3)}else if(code<2097152){const b1=240|code>>18&7;const b2=128|code>>12&63;const b3=128|code>>6&63;const b4=128|code&63;arr.push(b1,b2,b3,b4)}else{throw new Error(`Cannot convert character ${str.charAt(i)} (code ${code}) to utf-8`)}}else{arr.push(code<128?code:63)}}return new Uint8Array(arr)}}function decodeString(buffer){if(typeof TextDecoder!=="undefined"){const decoder=new TextDecoder;if(buffer instanceof Uint8Array){return decoder.decode(buffer)}const buf=Uint8Array.from(buffer);return decoder.decode(buf)}else if(typeof Buffer==="function"){if(buffer instanceof Buffer){return buffer.toString("utf-8")}else if(buffer instanceof Array){const typedArray=Uint8Array.from(buffer);const buf=Buffer.from(typedArray.buffer,typedArray.byteOffset,typedArray.byteOffset+typedArray.byteLength);return buf.toString("utf-8")}else if("buffer"in buffer&&buffer["buffer"]instanceof ArrayBuffer){const buf=Buffer.from(buffer["buffer"],buffer.byteOffset,buffer.byteOffset+buffer.byteLength);return buf.toString("utf-8")}else{throw new Error(`Unsupported buffer argument`)}}else{if(!(buffer instanceof Uint8Array)&&"buffer"in buffer&&buffer["buffer"]instanceof ArrayBuffer){buffer=new Uint8Array(buffer["buffer"],buffer.byteOffset,buffer.byteLength)}if(buffer instanceof Buffer||buffer instanceof Array||buffer instanceof Uint8Array){let str="";for(let i=0;i128){if((code&240)===240){const b1=code,b2=buffer[i+1],b3=buffer[i+2],b4=buffer[i+3];code=(b1&7)<<18|(b2&63)<<12|(b3&63)<<6|b4&63;i+=3}else if((code&224)===224){const b1=code,b2=buffer[i+1],b3=buffer[i+2];code=(b1&15)<<12|(b2&63)<<6|b3&63;i+=2}else if((code&192)===192){const b1=code,b2=buffer[i+1];code=(b1&31)<<6|b2&63;i++}else{throw new Error(`invalid utf-8 data`)}}if(code>=65536){code^=65536;const p1=55296|code>>10;const p2=56320|code&1023;str+=String.fromCharCode(p1);str+=String.fromCharCode(p2)}else{str+=String.fromCharCode(code)}}return str}else{throw new Error(`Unsupported buffer argument`)}}}function concatTypedArrays(a,b){const c=new a.constructor(a.length+b.length);c.set(a);c.set(b,a.length);return c}function cloneObject(original,stack){const{DataSnapshot:DataSnapshot}=require("./data-snapshot");if(original instanceof DataSnapshot){throw new TypeError(`Object to clone is a DataSnapshot (path "${original.ref.path}")`)}const checkAndFixTypedArray=obj=>{if(obj!==null&&typeof obj==="object"&&typeof obj.constructor==="function"&&typeof obj.constructor.name==="string"&&["Buffer","Uint8Array","Int8Array","Uint16Array","Int16Array","Uint32Array","Int32Array","BigUint64Array","BigInt64Array"].includes(obj.constructor.name)){obj=obj.buffer.slice(obj.byteOffset,obj.byteOffset+obj.byteLength)}return obj};original=checkAndFixTypedArray(original);if(typeof original!=="object"||original===null||original instanceof Date||original instanceof ArrayBuffer||original instanceof PathReference||original instanceof RegExp){return original}const cloneValue=val=>{if(stack.indexOf(val)>=0){throw new ReferenceError(`object contains a circular reference`)}val=checkAndFixTypedArray(val);if(val===null||val instanceof Date||val instanceof ArrayBuffer||val instanceof PathReference||val instanceof RegExp){return val}else if(val instanceof Array){stack.push(val);val=val.map(item=>cloneValue(item));stack.pop();return val}else if(typeof val==="object"){stack.push(val);val=cloneObject(val,stack);stack.pop();return val}else{return val}};if(typeof stack==="undefined"){stack=[original]}const clone={};Object.keys(original).forEach(key=>{let val=original[key];if(typeof val==="function"){return}clone[key]=cloneValue(val)});return clone}function compareValues(oldVal,newVal){const voids=[undefined,null];if(oldVal===newVal){return"identical"}else if(voids.indexOf(oldVal)>=0&&voids.indexOf(newVal)<0){return"added"}else if(voids.indexOf(oldVal)<0&&voids.indexOf(newVal)>=0){return"removed"}else if(typeof oldVal!==typeof newVal){return"changed"}else if(typeof oldVal==="object"){const isArray=oldVal instanceof Array;const oldKeys=isArray?Object.keys(oldVal).map(v=>parseInt(v)):Object.keys(oldVal);const newKeys=isArray?Object.keys(newVal).map(v=>parseInt(v)):Object.keys(newVal);const removedKeys=oldKeys.filter(key=>newKeys.indexOf(key)<0);const addedKeys=newKeys.filter(key=>oldKeys.indexOf(key)<0);const changedKeys=newKeys.reduce((changed,key)=>{if(oldKeys.indexOf(key)>=0){const val1=oldVal[key];const val2=newVal[key];const c=compareValues(val1,val2);if(c!=="identical"){changed.push({key:key,change:c})}}return changed},[]);if(addedKeys.length===0&&removedKeys.length===0&&changedKeys.length===0){return"identical"}else{return{added:addedKeys,removed:removedKeys,changed:changedKeys}}}else if(oldVal!==newVal){return"changed"}return"identical"}const getChildValues=(childKey,oldValue,newValue)=>{oldValue=oldValue===null?null:oldValue[childKey];if(typeof oldValue==="undefined"){oldValue=null}newValue=newValue===null?null:newValue[childKey];if(typeof newValue==="undefined"){newValue=null}return{oldValue:oldValue,newValue:newValue}};module.exports={numberToBytes:numberToBytes,bytesToNumber:bytesToNumber,concatTypedArrays:concatTypedArrays,cloneObject:cloneObject,compareValues:compareValues,getChildValues:getChildValues,encodeString:encodeString,decodeString:decodeString}}).call(this,require("buffer").Buffer)},{"./data-snapshot":8,"./path-reference":13,buffer:41}],18:[function(require,module,exports){var colors={};module["exports"]=colors;colors.themes={};var util=require("util");var ansiStyles=colors.styles=require("./styles");var defineProps=Object.defineProperties;var newLineRegex=new RegExp(/[\r\n]+/g);colors.supportsColor=require("./system/supports-colors").supportsColor;if(typeof colors.enabled==="undefined"){colors.enabled=colors.supportsColor()!==false}colors.enable=function(){colors.enabled=true};colors.disable=function(){colors.enabled=false};colors.stripColors=colors.strip=function(str){return(""+str).replace(/\x1B\[\d+m/g,"")};var stylize=colors.stylize=function stylize(str,style){if(!colors.enabled){return str+""}return ansiStyles[style].open+str+ansiStyles[style].close};var matchOperatorsRe=/[|\\{}()[\]^$+*?.]/g;var escapeStringRegexp=function(str){if(typeof str!=="string"){throw new TypeError("Expected a string")}return str.replace(matchOperatorsRe,"\\$&")};function build(_styles){var builder=function builder(){return applyStyle.apply(builder,arguments)};builder._styles=_styles;builder.__proto__=proto;return builder}var styles=function(){var ret={};ansiStyles.grey=ansiStyles.gray;Object.keys(ansiStyles).forEach((function(key){ansiStyles[key].closeRe=new RegExp(escapeStringRegexp(ansiStyles[key].close),"g");ret[key]={get:function(){return build(this._styles.concat(key))}}}));return ret}();var proto=defineProps((function colors(){}),styles);function applyStyle(){var args=Array.prototype.slice.call(arguments);var str=args.map((function(arg){if(arg!=undefined&&arg.constructor===String){return arg}else{return util.inspect(arg)}})).join(" ");if(!colors.enabled||!str){return str}var newLinesPresent=str.indexOf("\n")!=-1;var nestedStyles=this._styles;var i=nestedStyles.length;while(i--){var code=ansiStyles[nestedStyles[i]];str=code.open+str.replace(code.closeRe,code.open)+code.close;if(newLinesPresent){str=str.replace(newLineRegex,(function(match){return code.close+match+code.open}))}}return str}colors.setTheme=function(theme){if(typeof theme==="string"){console.log("colors.setTheme now only accepts an object, not a string. "+"If you are trying to set a theme from a file, it is now your (the "+"caller's) responsibility to require the file. The old syntax "+"looked like colors.setTheme(__dirname + "+"'/../themes/generic-logging.js'); The new syntax looks like "+"colors.setTheme(require(__dirname + "+"'/../themes/generic-logging.js'));");return}for(var style in theme){(function(style){colors[style]=function(str){if(typeof theme[style]==="object"){var out=str;for(var i in theme[style]){out=colors[theme[style][i]](out)}return out}return colors[theme[style]](str)}})(style)}};function init(){var ret={};Object.keys(styles).forEach((function(name){ret[name]={get:function(){return build([name])}}}));return ret}var sequencer=function sequencer(map,str){var exploded=str.split("");exploded=exploded.map(map);return exploded.join("")};colors.trap=require("./custom/trap");colors.zalgo=require("./custom/zalgo");colors.maps={};colors.maps.america=require("./maps/america")(colors);colors.maps.zebra=require("./maps/zebra")(colors);colors.maps.rainbow=require("./maps/rainbow")(colors);colors.maps.random=require("./maps/random")(colors);for(var map in colors.maps){(function(map){colors[map]=function(str){return sequencer(colors.maps[map],str)}})(map)}defineProps(colors,init())},{"./custom/trap":19,"./custom/zalgo":20,"./maps/america":23,"./maps/rainbow":24,"./maps/random":25,"./maps/zebra":26,"./styles":27,"./system/supports-colors":29,util:47}],19:[function(require,module,exports){module["exports"]=function runTheTrap(text,options){var result="";text=text||"Run the trap, drop the bass";text=text.split("");var trap={a:["@","Ą","Ⱥ","Ʌ","Δ","Λ","Д"],b:["ß","Ɓ","Ƀ","ɮ","β","฿"],c:["©","Ȼ","Ͼ"],d:["Ð","Ɗ","Ԁ","ԁ","Ԃ","ԃ"],e:["Ë","ĕ","Ǝ","ɘ","Σ","ξ","Ҽ","੬"],f:["Ӻ"],g:["ɢ"],h:["Ħ","ƕ","Ң","Һ","Ӈ","Ԋ"],i:["༏"],j:["Ĵ"],k:["ĸ","Ҡ","Ӄ","Ԟ"],l:["Ĺ"],m:["ʍ","Ӎ","ӎ","Ԡ","ԡ","൩"],n:["Ñ","ŋ","Ɲ","Ͷ","Π","Ҋ"],o:["Ø","õ","ø","Ǿ","ʘ","Ѻ","ם","۝","๏"],p:["Ƿ","Ҏ"],q:["্"],r:["®","Ʀ","Ȑ","Ɍ","ʀ","Я"],s:["§","Ϟ","ϟ","Ϩ"],t:["Ł","Ŧ","ͳ"],u:["Ʊ","Ս"],v:["ט"],w:["Ш","Ѡ","Ѽ","൰"],x:["Ҳ","Ӿ","Ӽ","ӽ"],y:["¥","Ұ","Ӌ"],z:["Ƶ","ɀ"]};text.forEach((function(c){c=c.toLowerCase();var chars=trap[c]||[" "];var rand=Math.floor(Math.random()*chars.length);if(typeof trap[c]!=="undefined"){result+=trap[c][rand]}else{result+=c}}));return result}},{}],20:[function(require,module,exports){module["exports"]=function zalgo(text,options){text=text||" he is here ";var soul={up:["̍","̎","̄","̅","̿","̑","̆","̐","͒","͗","͑","̇","̈","̊","͂","̓","̈","͊","͋","͌","̃","̂","̌","͐","̀","́","̋","̏","̒","̓","̔","̽","̉","ͣ","ͤ","ͥ","ͦ","ͧ","ͨ","ͩ","ͪ","ͫ","ͬ","ͭ","ͮ","ͯ","̾","͛","͆","̚"],down:["̖","̗","̘","̙","̜","̝","̞","̟","̠","̤","̥","̦","̩","̪","̫","̬","̭","̮","̯","̰","̱","̲","̳","̹","̺","̻","̼","ͅ","͇","͈","͉","͍","͎","͓","͔","͕","͖","͙","͚","̣"],mid:["̕","̛","̀","́","͘","̡","̢","̧","̨","̴","̵","̶","͜","͝","͞","͟","͠","͢","̸","̷","͡"," ҉"]};var all=[].concat(soul.up,soul.down,soul.mid);function randomNumber(range){var r=Math.floor(Math.random()*range);return r}function isChar(character){var bool=false;all.filter((function(i){bool=i===character}));return bool}function heComes(text,options){var result="";var counts;var l;options=options||{};options["up"]=typeof options["up"]!=="undefined"?options["up"]:true;options["mid"]=typeof options["mid"]!=="undefined"?options["mid"]:true;options["down"]=typeof options["down"]!=="undefined"?options["down"]:true;options["size"]=typeof options["size"]!=="undefined"?options["size"]:"maxi";text=text.split("");for(l in text){if(isChar(l)){continue}result=result+text[l];counts={up:0,down:0,mid:0};switch(options.size){case"mini":counts.up=randomNumber(8);counts.mid=randomNumber(2);counts.down=randomNumber(8);break;case"maxi":counts.up=randomNumber(16)+3;counts.mid=randomNumber(4)+1;counts.down=randomNumber(64)+3;break;default:counts.up=randomNumber(8)+1;counts.mid=randomNumber(6)/2;counts.down=randomNumber(8)+1;break}var arr=["up","mid","down"];for(var d in arr){var index=arr[d];for(var i=0;i<=counts[index];i++){if(options[index]){result=result+soul[index][randomNumber(soul[index].length)]}}}}return result}return heComes(text,options)}},{}],21:[function(require,module,exports){var colors=require("./colors");module["exports"]=function(){var addProperty=function(color,func){String.prototype.__defineGetter__(color,func)};addProperty("strip",(function(){return colors.strip(this)}));addProperty("stripColors",(function(){return colors.strip(this)}));addProperty("trap",(function(){return colors.trap(this)}));addProperty("zalgo",(function(){return colors.zalgo(this)}));addProperty("zebra",(function(){return colors.zebra(this)}));addProperty("rainbow",(function(){return colors.rainbow(this)}));addProperty("random",(function(){return colors.random(this)}));addProperty("america",(function(){return colors.america(this)}));var x=Object.keys(colors.styles);x.forEach((function(style){addProperty(style,(function(){return colors.stylize(this,style)}))}));function applyTheme(theme){var stringPrototypeBlacklist=["__defineGetter__","__defineSetter__","__lookupGetter__","__lookupSetter__","charAt","constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf","charCodeAt","indexOf","lastIndexOf","length","localeCompare","match","repeat","replace","search","slice","split","substring","toLocaleLowerCase","toLocaleUpperCase","toLowerCase","toUpperCase","trim","trimLeft","trimRight"];Object.keys(theme).forEach((function(prop){if(stringPrototypeBlacklist.indexOf(prop)!==-1){console.log("warn: ".red+("String.prototype"+prop).magenta+" is probably something you don't want to override. "+"Ignoring style name")}else{if(typeof theme[prop]==="string"){colors[prop]=colors[theme[prop]]}else{var tmp=colors[theme[prop][0]];for(var t=1;t=2,has16m:level>=3}}function supportsColor(stream){if(forceColor===false){return 0}if(hasFlag("color=16m")||hasFlag("color=full")||hasFlag("color=truecolor")){return 3}if(hasFlag("color=256")){return 2}if(stream&&!stream.isTTY&&forceColor!==true){return 0}var min=forceColor?1:0;if(process.platform==="win32"){var osRelease=os.release().split(".");if(Number(process.versions.node.split(".")[0])>=8&&Number(osRelease[0])>=10&&Number(osRelease[2])>=10586){return Number(osRelease[2])>=14931?3:2}return 1}if("CI"in env){if(["TRAVIS","CIRCLECI","APPVEYOR","GITLAB_CI"].some((function(sign){return sign in env}))||env.CI_NAME==="codeship"){return 1}return min}if("TEAMCITY_VERSION"in env){return/^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(env.TEAMCITY_VERSION)?1:0}if("TERM_PROGRAM"in env){var version=parseInt((env.TERM_PROGRAM_VERSION||"").split(".")[0],10);switch(env.TERM_PROGRAM){case"iTerm.app":return version>=3?3:2;case"Hyper":return 3;case"Apple_Terminal":return 2}}if(/-256(color)?$/i.test(env.TERM)){return 2}if(/^screen|^xterm|^vt100|^rxvt|color|ansi|cygwin|linux/i.test(env.TERM)){return 1}if("COLORTERM"in env){return 1}if(env.TERM==="dumb"){return min}return min}function getSupportLevel(stream){var level=supportsColor(stream);return translateLevel(level)}module.exports={supportsColor:getSupportLevel,stdout:getSupportLevel(process.stdout),stderr:getSupportLevel(process.stderr)}}).call(this,require("_process"))},{"./has-flag.js":28,_process:45,os:44}],30:[function(require,module,exports){const{AceBase:AceBase,AceBaseLocalSettings:AceBaseLocalSettings}=require("./acebase-local");const{CustomStorageSettings:CustomStorageSettings,CustomStorageTransaction:CustomStorageTransaction,CustomStorageHelpers:CustomStorageHelpers,ICustomStorageNode:ICustomStorageNode,ICustomStorageNodeMetaData:ICustomStorageNodeMetaData}=require("./storage-custom");const deprecatedConstructorError=`Using AceBase constructor in the browser to use localStorage is deprecated!\nSwitch to:\nIndexedDB implementation (FASTER, MORE RELIABLE):\n let db = AceBase.WithIndexedDB(name, settings)\nOr, new LocalStorage implementation:\n let db = AceBase.WithLocalStorage(name, settings)\nOr, write your own CustomStorage adapter:\n let myCustomStorage = new CustomStorageSettings({ ... });\n let db = new AceBase(name, { storage: myCustomStorage })`;class BrowserAceBase extends AceBase{constructor(name,settings){if(typeof settings!=="object"||typeof settings.storage!=="object"){throw new Error(deprecatedConstructorError)}super(name,settings)}static WithIndexedDB(dbname,settings){settings=settings||{};if(!settings.logLevel){settings.logLevel="error"}const IndexedDB=window.indexedDB||window.mozIndexedDB||window.webkitIndexedDB||window.msIndexedDB;let request=IndexedDB.open(`${dbname}.acebase`,1);let readyResolve,readyReject,readyPromise=new Promise((rs,rj)=>{readyResolve=rs;readyReject=rj});request.onupgradeneeded=e=>{let db=request.result;db.createObjectStore("nodes",{keyPath:"path"});db.createObjectStore("content")};let db;request.onsuccess=e=>{db=request.result;readyResolve()};request.onerror=e=>{readyReject(e)};const storageSettings=new CustomStorageSettings({name:"IndexedDB",locking:false,ready(){return readyPromise},async getTransaction(target){const context={debug:true,db:db};const transaction=new IndexedDBStorageTransaction(context,target);await transaction.start();return transaction}});return new AceBase(dbname,{logLevel:settings.logLevel,storage:storageSettings})}static WithLocalStorage(dbname,settings){settings=settings||{};if(!settings.logLevel){settings.logLevel="error"}const localStorage=settings.provider?settings.provider:settings.temp?window.localStorage:window.sessionStorage;const storageSettings=new CustomStorageSettings({name:"LocalStorage",locking:true,ready(){return Promise.resolve()},getTransaction(target){const context={debug:true,dbname:dbname,localStorage:localStorage};const transaction=new LocalStorageTransaction(context,target);return Promise.resolve(transaction)}});return new AceBase(dbname,{logLevel:settings.logLevel,storage:storageSettings})}}class IndexedDBStorageTransaction extends CustomStorageTransaction{constructor(context,target){super(target);this.context=context}start(){return new Promise((resolve,reject)=>{this._tx=this.context.db.transaction(["nodes","content"],this.target.write?"readwrite":"readonly");resolve()})}commit(){const tx=this._tx;tx.commit&&tx.commit()}rollback(err){const tx=this._tx;tx.abort&&tx.abort()}get(path){const tx=this._tx;const request=tx.objectStore("nodes").get(path);return new Promise((resolve,reject)=>{request.onsuccess=event=>{const data=request.result;if(!data){return resolve(null)}const node=data.metadata;const contentReq=tx.objectStore("content").get(path);contentReq.onsuccess=e=>{node.value=contentReq.result;resolve(node)};contentReq.onerror=e=>reject(e)};request.onerror=e=>{console.error(`IndexedDB get error`,e);reject(e)}})}set(path,node){const copy={};const value=node.value;Object.assign(copy,node);delete copy.value;const metadata=copy;const obj={path:path,metadata:metadata};return new Promise((resolve,reject)=>{const tx=this._tx;const request=tx.objectStore("nodes").put(obj);request.onerror=e=>reject(e);request.onsuccess=e=>{const contentReq=tx.objectStore("content").put(value,path);contentReq.onsuccess=e=>resolve();contentReq.onerror=e=>{tx.abort();reject(e)}}})}remove(path){const tx=this._tx;return new Promise((resolve,reject)=>{const r1=tx.objectStore("content").delete(path);r1.onerror=e=>reject(e);r1.onsuccess=e=>{const r2=tx.objectStore("nodes").delete(path);r2.onerror=e=>{tx.abort();reject(e)};r2.onsuccess=e=>resolve()}})}childrenOf(path,include,checkCallback,addCallback){const pathInfo=CustomStorageHelpers.PathInfo.get(path);const tx=this._tx;const store=tx.objectStore("nodes");const query=IDBKeyRange.lowerBound(path,true);return new Promise((resolve,reject)=>{const cursor=include.metadata?store.openCursor(query):store.openKeyCursor(query);cursor.onerror=e=>reject(e);cursor.onsuccess=async e=>{const otherPath=cursor.result?cursor.result.key:null;let keepGoing=true;if(otherPath===null){keepGoing=false}else if(!pathInfo.isAncestorOf(otherPath)){keepGoing=false}else if(pathInfo.isParentOf(otherPath)&&checkCallback(otherPath)){let node;if(include.metadata){const valueCursor=cursor;const data=valueCursor.result.value;node=data.metadata;if(include.value){const req=tx.objectStore("content").get(otherPath);await new Promise((resolve,reject)=>{req.onerror=e=>reject(e);req.onsuccess=e=>{node.value=req.result;resolve()}})}}keepGoing=addCallback(otherPath,node)}if(keepGoing){try{cursor.result.continue()}catch(err){keepGoing=false}}if(!keepGoing){resolve()}}})}descendantsOf(path,include,checkCallback,addCallback){const pathInfo=CustomStorageHelpers.PathInfo.get(path);const tx=this._tx;const store=tx.objectStore("nodes");const query=IDBKeyRange.lowerBound(path,true);return new Promise((resolve,reject)=>{const cursor=include.metadata?store.openCursor(query):store.openKeyCursor(query);cursor.onerror=e=>reject(e);cursor.onsuccess=async e=>{const otherPath=cursor.result?cursor.result.key:null;let keepGoing=true;if(otherPath===null){keepGoing=false}else if(!pathInfo.isAncestorOf(otherPath)){keepGoing=false}else if(checkCallback(otherPath)){let node;if(include.metadata){const valueCursor=cursor;const data=valueCursor.result.value;node=data.metadata;if(include.value){const req=tx.objectStore("content").get(otherPath);await new Promise((resolve,reject)=>{req.onerror=e=>reject(e);req.onsuccess=e=>{node.value=req.result;resolve()}})}}keepGoing=addCallback(otherPath,node)}if(keepGoing){try{cursor.result.continue()}catch(err){keepGoing=false}}if(!keepGoing){resolve()}}})}}class LocalStorageTransaction extends CustomStorageTransaction{constructor(context,target){super(target);this.context=context;this._storageKeysPrefix=`${this.context.dbname}.acebase::`}commit(){return Promise.resolve()}rollback(err){return Promise.resolve()}get(path){return new Promise(resolve=>{const json=this.context.localStorage.getItem(this.getStorageKeyForPath(path));const val=JSON.parse(json);resolve(val)})}set(path,val){return new Promise(resolve=>{const json=JSON.stringify(val);this.context.localStorage.setItem(this.getStorageKeyForPath(path),json);resolve()})}remove(path){return new Promise(resolve=>{this.context.localStorage.removeItem(this.getStorageKeyForPath(path));resolve()})}childrenOf(path,include,checkCallback,addCallback){return new Promise(resolve=>{const pathInfo=CustomStorageHelpers.PathInfo.get(path);for(let i=0;i{const pathInfo=CustomStorageHelpers.PathInfo.get(path);for(let i=0;i{this.emit("ready")})}}module.exports={AceBase:AceBase,AceBaseLocalSettings:AceBaseLocalSettings}},{"./api-local":32,"./storage":40,"acebase-core":11}],32:[function(require,module,exports){const{Api:Api,Utils:Utils}=require("acebase-core");const{AceBase:AceBase}=require("./acebase-local");const{StorageSettings:StorageSettings}=require("./storage");const{AceBaseStorage:AceBaseStorage,AceBaseStorageSettings:AceBaseStorageSettings}=require("./storage-acebase");const{SQLiteStorage:SQLiteStorage,SQLiteStorageSettings:SQLiteStorageSettings}=require("./storage-sqlite");const{MSSQLStorage:MSSQLStorage,MSSQLStorageSettings:MSSQLStorageSettings}=require("./storage-mssql");const{LocalStorage:LocalStorage,LocalStorageSettings:LocalStorageSettings}=require("./storage-localstorage");const{CustomStorage:CustomStorage,CustomStorageSettings:CustomStorageSettings}=require("./storage-custom");const{Node:Node}=require("./node");const{DataIndex:DataIndex}=require("./data-index");class LocalApi extends Api{constructor(dbname="default",settings,readyCallback){super();this.db=settings.db;if(typeof settings.storage==="object"){settings.storage.logLevel=settings.logLevel;if(SQLiteStorageSettings&&(settings.storage instanceof SQLiteStorageSettings||settings.storage.type==="sqlite")){this.storage=new SQLiteStorage(dbname,settings.storage)}else if(MSSQLStorageSettings&&(settings.storage instanceof MSSQLStorageSettings||settings.storage.type==="mssql")){this.storage=new MSSQLStorage(dbname,settings.storage)}else if(LocalStorageSettings&&(settings.storage instanceof LocalStorageSettings||settings.storage.type==="localstorage")){this.storage=new LocalStorage(dbname,settings.storage)}else if(CustomStorageSettings&&(settings.storage instanceof CustomStorageSettings||settings.storage.type==="custom")){this.storage=new CustomStorage(dbname,settings.storage)}else{const storageSettings=settings.storage instanceof AceBaseStorageSettings?settings.storage:new AceBaseStorageSettings(settings.storage);this.storage=new AceBaseStorage(dbname,storageSettings)}}else{settings.storage=new AceBaseStorageSettings({logLevel:settings.logLevel});this.storage=new AceBaseStorage(dbname,settings.storage)}this.storage.on("ready",readyCallback)}stats(options){return Promise.resolve(this.storage.stats)}subscribe(path,event,callback){this.storage.subscriptions.add(path,event,callback)}unsubscribe(path,event=undefined,callback=undefined){this.storage.subscriptions.remove(path,event,callback)}set(path,value,flags=undefined){return Node.update(this.storage,path,value,{merge:false})}update(path,updates,flags=undefined){return Node.update(this.storage,path,updates,{merge:true})}get(path,options){return Node.getValue(this.storage,path,options)}transaction(path,callback){return Node.transaction(this.storage,path,callback)}exists(path){return Node.exists(this.storage,path)}query2(path,query,options={snapshots:false,include:undefined,exclude:undefined,child_objects:undefined}){}query(path,query,options={snapshots:false,include:undefined,exclude:undefined,child_objects:undefined,eventHandler:event=>{}}){if(typeof options!=="object"){options={}}if(typeof options.snapshots==="undefined"){options.snapshots=false}const sortMatches=matches=>{matches.sort((a,b)=>{const compare=i=>{const o=query.order[i];let left=a.val[o.key];let right=b.val[o.key];if(typeof left==="undefined"&&typeof right!=="undefined"){return o.ascending?-1:1}if(typeof left!=="undefined"&&typeof right==="undefined"){return o.ascending?1:-1}if(typeof left==="undefined"&&typeof right==="undefined"){return 0}if(left==right){if(iright){return o.ascending?1:-1}};return compare(0)})};const loadResultsData=(preResults,options)=>{if(preResults.length===0){return Promise.resolve([])}const maxBatchSize=50;let batches=[];const items=preResults.map((result,index)=>({path:result.path,index:index}));while(items.length>0){let batchItems=items.splice(0,maxBatchSize);batches.push(batchItems)}const results=[];const nextBatch=()=>{const batch=batches.shift();return Promise.all(batch.map(item=>{const{path:path,index:index}=item;return Node.getValue(this.storage,path,options).then(val=>{if(val===null){this.storage.debug.warn(`Indexed result "/${path}" does not have a record!`);return}const result={path:path,val:val};if(stepsExecuted.sorted){results[index]=result}else{results.push(result);if(!stepsExecuted.skipped&&results.length>query.skip+query.take){sortMatches(results);results.pop()}}})})).then(()=>{if(batches.length>0){return nextBatch()}})};return nextBatch().then(()=>{return results})};const isWildcardPath=path.indexOf("*")>=0||path.indexOf("$")>=0;const availableIndexes=this.storage.indexes.get(path);const usingIndexes=[];query.filters.forEach(filter=>{if(filter.index){return}const indexesOnKey=availableIndexes.filter(index=>index.key===filter.key).filter(index=>{return index.validOperators.includes(filter.op)});if(indexesOnKey.length>=1){const otherFilterKeys=query.filters.filter(f=>f!==filter).map(f=>f.key);const sortKeys=query.order.map(o=>o.key).filter(key=>key!==filter.key);const beneficialIndexes=indexesOnKey.map(index=>{const availableKeys=index.includeKeys.concat(index.key);const forOtherFilters=availableKeys.filter(key=>otherFilterKeys.indexOf(key)>=0);const forSorting=availableKeys.filter(key=>sortKeys.indexOf(key)>=0);const forBoth=forOtherFilters.concat(forSorting.filter(index=>forOtherFilters.indexOf(index)<0));const points={filters:forOtherFilters.length,sorting:forSorting.length*(query.take>0?forSorting.length:1),both:forBoth.length*forBoth.length,get total(){return this.filters+this.sorting+this.both}};return{index:index,points:points.total,filterKeys:forOtherFilters,sortKeys:forSorting}});beneficialIndexes.sort((a,b)=>a.points>b.points?-1:1);const bestBenificialIndex=beneficialIndexes[0];filter.index=bestBenificialIndex.index;bestBenificialIndex.filterKeys.forEach(key=>{query.filters.filter(f=>f!==filter&&f.key===key).forEach(f=>{if(!DataIndex.validOperators.includes(f.op)){return}f.indexUsage="filter";f.index=bestBenificialIndex.index})});bestBenificialIndex.sortKeys.forEach(key=>{query.order.filter(s=>s.key===key).forEach(s=>{s.index=bestBenificialIndex.index})})}if(filter.index){usingIndexes.push({index:filter.index,description:filter.index.description})}});if(query.order.length>0&&query.take>0){query.order.forEach(sort=>{if(sort.index){return}sort.index=availableIndexes.filter(index=>index.key===sort.key).find(index=>index.type==="normal")})}const indexDescriptions=usingIndexes.map(index=>index.description).join(", ");usingIndexes.length>0&&this.storage.debug.log(`Using indexes for query: ${indexDescriptions}`);const tableScanFilters=query.filters.filter(filter=>!filter.index);const specialOpsRegex=/^[a-z]+\:/i;if(tableScanFilters.some(filter=>specialOpsRegex.test(filter.op))){const f=tableScanFilters.find(filter=>specialOpsRegex.test(filter.op));const err=new Error(`query contains operator "${f.op}" which requires a special index that was not found on path "${path}", key "${f.key}"`);return Promise.reject(err)}const allowedTableScanOperators=["<","<=","==","!=",">=",">","like","!like","in","!in","matches","!matches","between","!between","has","!has","contains","!contains","exists","!exists"];for(let i=0;i0){const keys=tableScanFilters.reduce((keys,f)=>{if(keys.indexOf(f.key)<0){keys.push(f.key)}return keys},[]).map(key=>`"${key}"`);const err=new Error(`This wildcard path query on "/${path}" requires index(es) on key(s): ${keys.join(", ")}. Create the index(es) and retry`);return Promise.reject(err)}const indexScanPromises=[];query.filters.forEach(filter=>{if(filter.index&&filter.indexUsage!=="filter"){let promise=filter.index.query(filter.op,filter.compare).then(results=>{options.eventHandler&&options.eventHandler({name:"stats",type:"index_query",source:filter.index.description,stats:results.stats});if(results.hints.length>0){options.eventHandler&&options.eventHandler({name:"hints",type:"index_query",source:filter.index.description,hints:results.hints})}return results});const resultFilters=query.filters.filter(f=>f.index===filter.index&&f.indexUsage==="filter");if(resultFilters.length>0){promise=promise.then(results=>{resultFilters.forEach(filter=>{results=results.filterMetadata(filter.key,filter.op,filter.compare)});return results})}indexScanPromises.push(promise)}});const stepsExecuted={filtered:query.filters.length===0,skipped:query.skip===0,taken:query.take===0,sorted:query.order.length===0,preDataLoaded:false,dataLoaded:false};if(query.filters.length===0&&query.take===0){this.storage.debug.error(`Filterless queries must use .take to limit the results. Defaulting to 100 for query on path "${path}"`);query.take=100}if(query.filters.length===0&&query.order.length>0&&query.order[0].index){const sortIndex=query.order[0].index;this.storage.debug.log(`Using index for sorting: ${sortIndex.description}`);const promise=sortIndex.take(query.skip,query.take,query.order[0].ascending).then(results=>{options.eventHandler&&options.eventHandler({name:"stats",type:"sort_index_take",source:filter.index.description,stats:results.stats});if(results.hints.length>0){options.eventHandler&&options.eventHandler({name:"hints",type:"sort_index_take",source:filter.index.description,hints:results.hints})}return results});indexScanPromises.push(promise);stepsExecuted.skipped=true;stepsExecuted.taken=true;stepsExecuted.sorted=true}return Promise.all(indexScanPromises).then(indexResultSets=>{let indexedResults=[];if(indexResultSets.length===1){const resultSet=indexResultSets[0];indexedResults=resultSet.map(match=>{const result={key:match.key,path:match.path,val:{[resultSet.filterKey]:match.value}};match.metadata&&Object.assign(result.val,match.metadata);return result});stepsExecuted.filtered=true}else if(indexResultSets.length>1){indexResultSets.sort((a,b)=>a.length{const result={key:match.key,path:match.path,val:{[shortestSet.filterKey]:match.value}};const matchedInAllSets=otherSets.every(set=>set.findIndex(m=>match.path===match.path)>=0);if(matchedInAllSets){match.metadata&&Object.assign(result.val,match.metadata);otherSets.forEach(set=>{const otherResult=set.find(r=>r.path===result.path);result.val[set.filterKey]=otherResult.value;otherResult.metadata&&Object.assign(result.val,otherResult.metadata)});results.push(result)}return results},[]);stepsExecuted.filtered=true}if(isWildcardPath||indexScanPromises.length>0&&tableScanFilters.length===0){if(query.order.length===0||query.order.every(o=>o.index)){stepsExecuted.preDataLoaded=true;if(!stepsExecuted.sorted&&query.order.length>0){sortMatches(indexedResults)}stepsExecuted.sorted=true;if(!stepsExecuted.skipped&&query.skip>0){indexedResults=indexedResults.slice(query.skip)}if(!stepsExecuted.taken&&query.take>0){indexedResults=indexedResults.slice(0,query.take)}stepsExecuted.skipped=true;stepsExecuted.taken=true;if(!options.snapshots){return indexedResults}const childOptions={include:options.include,exclude:options.exclude,child_objects:options.child_objects};return loadResultsData(indexedResults,childOptions).then(results=>{stepsExecuted.dataLoaded=true;return results})}if(options.snapshots||!stepsExecuted.sorted){const loadPartialResults=query.order.length>0;const childOptions=loadPartialResults?{include:query.order.map(order=>order.key)}:{include:options.include,exclude:options.exclude,child_objects:options.child_objects};return loadResultsData(indexedResults,childOptions).then(results=>{if(query.order.length>0){sortMatches(results)}stepsExecuted.sorted=true;if(query.skip>0){results=results.slice(query.skip)}if(query.take>0){results=results.slice(0,query.take)}stepsExecuted.skipped=true;stepsExecuted.taken=true;if(options.snapshots&&loadPartialResults){return loadResultsData(results,{include:options.include,exclude:options.exclude,child_objects:options.child_objects})}return results})}else{return indexedResults}}let indexKeyFilter;if(indexedResults.length>0){indexKeyFilter=indexedResults.map(result=>result.key)}const promises=[];let matches=[];let preliminaryStop=false;const loadPartialData=query.order.length>0;const childOptions=loadPartialData?{include:query.order.map(order=>order.key)}:{include:options.include,exclude:options.exclude,child_objects:options.child_objects};return Node.getChildren(this.storage,path,indexKeyFilter).next(child=>{if(child.type===Node.VALUE_TYPES.OBJECT){if(!child.address){return}if(preliminaryStop){return false}const p=Node.matches(this.storage,child.address.path,tableScanFilters).then(isMatch=>{if(!isMatch){return null}const childPath=child.address.path;if(options.snapshots||query.order.length>0){return Node.getValue(this.storage,childPath,childOptions).then(val=>{return{path:childPath,val:val}})}else{return{path:childPath}}}).then(result=>{if(result!==null){matches.push(result);if(query.take>0&&matches.length>query.take+query.skip){if(query.order.length>0){sortMatches(matches)}else{preliminaryStop=true}matches.pop()}}});promises.push(p)}}).catch(reason=>{this.storage.debug.warn(`Error getting child stream: ${reason}`);return[]}).then(()=>{return Promise.all(promises).then(()=>{stepsExecuted.preDataLoaded=loadPartialData;stepsExecuted.dataLoaded=!loadPartialData;if(query.order.length>0){sortMatches(matches)}stepsExecuted.sorted=true;if(query.skip>0){matches=matches.slice(query.skip)}stepsExecuted.skipped=true;if(query.take>0){matches=matches.slice(0,query.take)}stepsExecuted.taken=true;if(!stepsExecuted.dataLoaded){return loadResultsData(matches,{include:options.include,exclude:options.exclude,child_objects:options.child_objects}).then(results=>{stepsExecuted.dataLoaded=true;return results})}return matches})})}).then(matches=>{if(!stepsExecuted.sorted&&query.order.length>0){sortMatches(matches)}if(!options.snapshots){matches=matches.map(match=>match.path)}if(!stepsExecuted.skipped&&query.skip>0){matches=matches.slice(query.skip)}if(!stepsExecuted.taken&&query.take>0){matches=matches.slice(0,query.take)}if(options.monitor===true){options.monitor={add:true,change:true,remove:true}}if(typeof options.monitor==="object"&&(options.monitor.add||options.monitor.change||options.monitor.remove)){const matchedPaths=options.snapshots?matches.map(match=>match.path):matches.slice();const ref=this.db.ref(path);const removeMatch=path=>{const index=matchedPaths.indexOf(path);if(index<0){return}matchedPaths.splice(index,1)};const addMatch=path=>{if(matchedPaths.includes(path)){return}matchedPaths.push(path)};const stopMonitoring=()=>{this.unsubscribe(ref.path,"notify_child_changed",childChangedCallback);this.unsubscribe(ref.path,"notify_child_added",childAddedCallback);this.unsubscribe(ref.path,"notify_child_removed",childRemovedCallback)};const childChangedCallback=(err,path,newValue,oldValue)=>{const wasMatch=matchedPaths.includes(path);let keepMonitoring=true;const checkKeys=[];query.filters.forEach(f=>!checkKeys.includes(f.key)&&checkKeys.push(f.key));const seenKeys=[];typeof oldValue==="object"&&Object.keys(oldValue).forEach(key=>!seenKeys.includes(key)&&seenKeys.push(key));typeof newValue==="object"&&Object.keys(newValue).forEach(key=>!seenKeys.includes(key)&&seenKeys.push(key));const missingKeys=[];let isMatch=seenKeys.every(key=>{if(!checkKeys.includes(key)){return true}const filters=query.filters.filter(filter=>filter.key===key);return filters.every(filter=>{if(allowedTableScanOperators.includes(filter.op)){return this.storage.test(newValue[key],filter.op,filter.compare)}if(filter.index.constructor.name==="FullTextDataIndex"&&filter.index.localeKey&&!seenKeys.includes(filter.index.localeKey)){missingKeys.push(filter.index.localeKey);return true}return filter.index.test(newValue,filter.op,filter.compare)})});if(isMatch){missingKeys.push(...checkKeys.filter(key=>!seenKeys.includes(key)));let promise=Promise.resolve(true);if(!wasMatch&&missingKeys.length>0){const filterQueue=query.filters.filter(f=>missingKeys.includes(f.key));const simpleFilters=filterQueue.filter(f=>allowedTableScanOperators.includes(f.op));const indexFilters=filterQueue.filter(f=>!allowedTableScanOperators.includes(f.op));const processFilters=()=>{const checkIndexFilters=()=>{const keysToLoad=indexFilters.reduce((keys,filter)=>{if(!keys.includes(filter.key)){keys.push(filter.key)}if(filter.index.constructor.name==="FullTextDataIndex"&&filter.index.localeKey&&!keys.includes(filter.index.localeKey)){keys.push(filter.index.localeKey)}return keys},[]);return Node.getValue(this.storage,path,{include:keysToLoad}).then(val=>{if(val===null){return false}return indexFilters.every(filter=>filter.index.test(val,filter.op,filter.compare))})};if(simpleFilters.length>0){return Node.matches(this.storage,path,simpleFilters).then(isMatch=>{if(isMatch){if(indexFilters.length===0){return true}return checkIndexFilters()}return false})}else{return checkIndexFilters()}};promise=processFilters()}return promise.then(isMatch=>{if(isMatch){if(!wasMatch){addMatch(path)}let gotValue=value=>{if(wasMatch&&options.monitor.change){keepMonitoring=options.eventHandler({name:"change",path:path,value:value})}else if(!wasMatch&&options.monitor.add){keepMonitoring=options.eventHandler({name:"add",path:path,value:value})}if(keepMonitoring===false){stopMonitoring()}};if(options.snapshots){const loadOptions={include:options.include,exclude:options.exclude,child_objects:options.child_objects};return this.storage.getNodeValue(path,loadOptions).then(gotValue)}else{return gotValue(newValue)}}else if(wasMatch){removeMatch(path);if(options.monitor.remove){keepMonitoring=options.eventHandler({name:"remove",path:path,value:oldValue})}}if(keepMonitoring===false){stopMonitoring()}})}else{if(wasMatch){removeMatch(path);if(options.monitor.remove){keepMonitoring=options.eventHandler({name:"remove",path:path,value:oldValue});if(keepMonitoring===false){stopMonitoring()}}}}};const childAddedCallback=(err,path,newValue,oldValue)=>{let isMatch=query.filters.every(filter=>{if(allowedTableScanOperators.includes(filter.op)){return this.storage.test(newValue[filter.key],filter.op,filter.compare)}else{return filter.index.test(newValue,filter.op,filter.compare)}});let keepMonitoring=true;if(isMatch){addMatch(path);if(options.monitor.add){keepMonitoring=options.eventHandler({name:"add",path:path,value:options.snapshots?newValue:null})}}if(keepMonitoring===false){stopMonitoring()}};const childRemovedCallback=(err,path,newValue,oldValue)=>{let keepMonitoring=true;removeMatch(path);if(options.monitor.remove){keepMonitoring=options.eventHandler({name:"remove",path:path,value:options.snapshots?oldValue:null})}if(keepMonitoring===false){stopMonitoring()}};if(options.monitor.add||options.monitor.change||options.monitor.remove){this.subscribe(ref.path,"notify_child_changed",childChangedCallback)}if(options.monitor.remove){this.subscribe(ref.path,"notify_child_removed",childRemovedCallback)}if(options.monitor.add){this.subscribe(ref.path,"notify_child_added",childAddedCallback)}}return matches})}createIndex(path,key,options){return this.storage.indexes.create(path,key,options)}getIndexes(){return Promise.resolve(this.storage.indexes.list())}reflect(path,type,args){const getChildren=(path,limit=50,skip=0)=>{if(typeof limit==="string"){limit=parseInt(limit)}if(typeof skip==="string"){skip=parseInt(skip)}const children=[];let n=0,stop=skip+limit;return Node.getChildren(this.storage,path).next(childInfo=>{n++;if(limit===0||n<=stop&&n>skip){children.push({key:typeof childInfo.key==="string"?childInfo.key:childInfo.index,type:childInfo.valueTypeName,value:childInfo.value,address:typeof childInfo.address==="object"&&"pageNr"in childInfo.address?{pageNr:childInfo.address.pageNr,recordNr:childInfo.address.recordNr}:undefined})}if(limit>0&&n>stop){return false}}).then(()=>{return{more:limit!==0&&n>stop,list:children}})};switch(type){case"children":{return getChildren(path,args.limit,args.skip)}case"info":{const info={key:"",exists:false,type:"unknown",value:undefined,children:{more:false,list:[]}};return Node.getInfo(this.storage,path).then(nodeInfo=>{info.key=nodeInfo.key;info.exists=nodeInfo.exists;info.type=nodeInfo.valueTypeName;info.value=nodeInfo.value;let hasChildren=nodeInfo.exists&&nodeInfo.address&&[Node.VALUE_TYPES.OBJECT,Node.VALUE_TYPES.ARRAY].includes(nodeInfo.type);if(hasChildren){return getChildren(path,args.child_limit,args.child_skip)}}).then(children=>{info.children=children;return info})}}}export(path,stream,options={format:"json"}){return this.storage.exportNode(path,stream,options)}}module.exports={LocalApi:LocalApi}},{"./acebase-local":31,"./data-index":41,"./node":37,"./storage":40,"./storage-acebase":41,"./storage-custom":38,"./storage-localstorage":39,"./storage-mssql":41,"./storage-sqlite":41,"acebase-core":11}],33:[function(require,module,exports){const{DataReference:DataReference,DataSnapshot:DataSnapshot,EventSubscription:EventSubscription,PathReference:PathReference,TypeMappings:TypeMappings,TypeMappingOptions:TypeMappingOptions}=require("acebase-core");const{AceBaseLocalSettings:AceBaseLocalSettings}=require("./acebase-local");const{BrowserAceBase:BrowserAceBase}=require("./acebase-browser");const{LocalStorageSettings:LocalStorageSettings}=require("./storage-localstorage");const{CustomStorageSettings:CustomStorageSettings,CustomStorageTransaction:CustomStorageTransaction,CustomStorageHelpers:CustomStorageHelpers}=require("./storage-custom");const acebase={AceBase:BrowserAceBase,AceBaseLocalSettings:AceBaseLocalSettings,DataReference:DataReference,DataSnapshot:DataSnapshot,EventSubscription:EventSubscription,PathReference:PathReference,TypeMappings:TypeMappings,TypeMappingOptions:TypeMappingOptions,LocalStorageSettings:LocalStorageSettings,CustomStorageSettings:CustomStorageSettings,CustomStorageTransaction:CustomStorageTransaction,CustomStorageHelpers:CustomStorageHelpers};window.acebase=acebase;window.AceBase=BrowserAceBase;module.exports=acebase},{"./acebase-browser":30,"./acebase-local":31,"./storage-custom":38,"./storage-localstorage":39,"acebase-core":11}],34:[function(require,module,exports){const{VALUE_TYPES:VALUE_TYPES,getValueTypeName:getValueTypeName}=require("./node-value-types");const{PathInfo:PathInfo}=require("acebase-core");class NodeInfo{constructor(info){this.path=info.path;this.type=info.type;this.index=info.index;this.key=info.key;this.exists=info.exists;this.address=info.address;this.value=info.value;if(typeof this.path==="string"&&(typeof this.key==="undefined"&&typeof this.index==="undefined")){let pathInfo=PathInfo.get(this.path);if(typeof pathInfo.key==="number"){this.index=pathInfo.key}else{this.key=pathInfo.key}}if(typeof this.exists==="undefined"){this.exists=true}}get valueType(){return this.type}get valueTypeName(){return getValueTypeName(this.valueType)}toString(){if(!this.exists){return`"${this.path}" doesn't exist`}if(this.address){return`"${this.path}" is ${this.valueTypeName} stored at ${this.address.pageNr},${this.address.recordNr}`}else{return`"${this.path}" is ${this.valueTypeName} with value ${this.value}`}}}module.exports={NodeInfo:NodeInfo}},{"./node-value-types":36,"acebase-core":11}],35:[function(require,module,exports){const{PathInfo:PathInfo}=require("acebase-core");const SECOND=1e3;const MINUTE=6e4;const DEBUG_MODE=false;const LOCK_TIMEOUT=DEBUG_MODE?15*MINUTE:90*SECOND;const LOCK_STATE={PENDING:"pending",LOCKED:"locked",EXPIRED:"expired",DONE:"done"};class NodeLocker{constructor(){this._locks=[]}_allowLock(path,tid,forWriting){const pathInfo=PathInfo.get(path);const existing=this._locks.find(otherLock=>otherLock.tid===tid&&otherLock.state===LOCK_STATE.LOCKED&&(otherLock.path===path||pathInfo.isDescendantOf(otherLock.path))&&(otherLock.forWriting||!forWriting));if(typeof existing==="object"){return{allow:true}}const conflict=this._locks.filter(otherLock=>otherLock.tid!==tid&&otherLock.state===LOCK_STATE.LOCKED).find(otherLock=>{return(forWriting||otherLock.forWriting)&&(path===otherLock.path||pathInfo.isDescendantOf(otherLock.path))});const clashes=typeof conflict!=="undefined";return{allow:!clashes,conflict:conflict}}_processLockQueue(){const pending=this._locks.filter(lock=>lock.state===LOCK_STATE.PENDING&&(lock.waitingFor===null||lock.waitingFor.state!==LOCK_STATE.LOCKED)).sort((a,b)=>{if(a.priority&&!b.priority){return-1}else if(!a.priority&&b.priority){return 1}return a.requested{const check=this._allowLock(lock.path,lock.tid,lock.forWriting);lock.waitingFor=check.conflict||null;if(check.allow){this.lock(lock).then(lock.resolve).catch(lock.reject)}})}lock(path,tid,forWriting=true,comment="",options={withPriority:false,noTimeout:false}){let lock,proceed;if(path instanceof NodeLock){lock=path;lock.comment=`(retry: ${lock.comment})`;proceed=true}else if(this._locks.findIndex(l=>l.tid===tid&&l.state===LOCK_STATE.EXPIRED)>=0){return Promise.reject(new Error(`lock on tid ${tid} has expired, not allowed to continue`))}else{lock=new NodeLock(this,path,tid,forWriting,options.withPriority===true);lock.comment=comment;this._locks.push(lock);const check=this._allowLock(path,tid,forWriting);lock.waitingFor=check.conflict||null;proceed=check.allow}if(proceed){lock.state=LOCK_STATE.LOCKED;if(typeof lock.granted==="number"){}else{lock.granted=Date.now();if(options.noTimeout!==true){lock.expires=Date.now()+LOCK_TIMEOUT;lock.timeout=setTimeout(()=>{if(lock.state!==LOCK_STATE.LOCKED){return}console.error(`lock :: ${lock.forWriting?"write":"read"} lock on path "/${lock.path}" by tid ${lock.tid} took too long, ${lock.comment}`);lock.state=LOCK_STATE.EXPIRED;this._processLockQueue()},LOCK_TIMEOUT)}}return Promise.resolve(lock)}else{console.assert(lock.state===LOCK_STATE.PENDING);const p=new Promise((resolve,reject)=>{lock.resolve=resolve;lock.reject=reject});return p}}unlock(lockOrId,comment,processQueue=true){let lock,i;if(lockOrId instanceof NodeLock){lock=lockOrId;i=this._locks.indexOf(lock)}else{let id=lockOrId;i=this._locks.findIndex(l=>l.id===id);lock=this._locks[i]}if(i<0){const msg=`lock on "/${lock.path}" for tid ${lock.tid} wasn't found; ${comment}`;return Promise.reject(new Error(msg))}lock.state=LOCK_STATE.DONE;clearTimeout(lock.timeout);this._locks.splice(i,1);processQueue&&this._processLockQueue();return Promise.resolve(lock)}list(){return this._locks||[]}isAllowed(path,tid,forWriting){return this._allowLock(path,tid,forWriting).allow}}let lastid=0;class NodeLock{static get LOCK_STATE(){return LOCK_STATE}constructor(locker,path,tid,forWriting,priority=false){this.locker=locker;this.path=path;this.tid=tid;this.forWriting=forWriting;this.priority=priority;this.state=LOCK_STATE.PENDING;this.requested=Date.now();this.granted=undefined;this.expires=undefined;this.comment="";this.waitingFor=null;this.id=++lastid}release(comment){return this.locker.unlock(this,comment||this.comment)}moveToParent(){const parentPath=PathInfo.get(this.path).parentPath;const allowed=this.locker.isAllowed(parentPath,this.tid,this.forWriting);if(allowed){this.waitingFor=null;this.path=parentPath;this.comment=`moved to parent: ${this.comment}`;return Promise.resolve(this)}else{this.locker.unlock(this,`moveLockToParent: ${this.comment}`,false);return this.locker.lock(parentPath,this.tid,this.forWriting,`moved to parent (queued): ${this.comment}`,{withPriority:true}).then(newLock=>{return newLock})}}moveTo(otherPath,forWriting){const allowed=this.locker.isAllowed(otherPath,this.tid,forWriting);if(allowed){this.waitingFor=null;this.path=otherPath;this.forWriting=forWriting;this.comment=`moved to "/${otherPath}": ${this.comment}`;return Promise.resolve(this)}else{this.locker.unlock(this,`moving to "/${otherPath}": ${this.comment}`,false);return this.locker.lock(otherPath,this.tid,forWriting,`moved to "/${otherPath}" (queued): ${this.comment}`,{withPriority:true}).then(newLock=>{return newLock})}}}module.exports={NodeLocker:NodeLocker,NodeLock:NodeLock}},{"acebase-core":11}],36:[function(require,module,exports){const VALUE_TYPES={OBJECT:1,ARRAY:2,NUMBER:3,BOOLEAN:4,STRING:5,DATETIME:6,BINARY:8,REFERENCE:9};function getValueTypeName(valueType){switch(valueType){case VALUE_TYPES.ARRAY:return"array";case VALUE_TYPES.BINARY:return"binary";case VALUE_TYPES.BOOLEAN:return"boolean";case VALUE_TYPES.DATETIME:return"date";case VALUE_TYPES.NUMBER:return"number";case VALUE_TYPES.OBJECT:return"object";case VALUE_TYPES.REFERENCE:return"reference";case VALUE_TYPES.STRING:return"string";default:"unknown"}}module.exports={VALUE_TYPES:VALUE_TYPES,getValueTypeName:getValueTypeName}},{}],37:[function(require,module,exports){const{Storage:Storage}=require("./storage");const{NodeInfo:NodeInfo}=require("./node-info");const{VALUE_TYPES:VALUE_TYPES,getValueTypeName:getValueTypeName}=require("./node-value-types");const colors=require("colors");class Node{static get VALUE_TYPES(){return VALUE_TYPES}static getInfo(storage,path,options={no_cache:false}){if(options&&!options.no_cache){let cachedInfo=storage.nodeCache.find(path);if(cachedInfo){return Promise.resolve(cachedInfo)}}return storage.getNodeInfo(path).then(info=>{if(options&&!options.no_cache){storage.nodeCache.update(info)}return info})}static update(storage,path,value,options={merge:true}){if(options.merge){return storage.updateNode(path,value)}else{return storage.setNode(path,value)}}static exists(storage,path){return storage.getNodeInfo(path).then(nodeInfo=>{return nodeInfo.exists})}static getValue(storage,path,options={include:undefined,exclude:undefined,child_objects:true}){if(!options){options={}}if(typeof options.include!=="undefined"&&!(options.include instanceof Array)){throw new TypeError(`options.include must be an array of key names`)}if(typeof options.exclude!=="undefined"&&!(options.exclude instanceof Array)){throw new TypeError(`options.exclude must be an array of key names`)}if(["undefined","boolean"].indexOf(typeof options.child_objects)<0){throw new TypeError(`options.child_objects must be a boolean`)}return storage.getNodeValue(path,options)}static getChildInfo(storage,path,childKeyOrIndex){let childInfo;return storage.getChildren(path,{keyFilter:[childKeyOrIndex]}).next(info=>{childInfo=info}).then(()=>{return childInfo})}static getChildren(storage,path,keyFilter=undefined){return storage.getChildren(path,{keyFilter:keyFilter})}static remove(storage,path){return storage.removeNode(path)}static set(storage,path,value){return Node.update(storage,path,value,{merge:false})}static transaction(storage,path,callback){return storage.transactNode(path,callback)}static matches(storage,path,criteria,options){return storage.matchNode(path,criteria,options)}}class NodeChange{static get CHANGE_TYPE(){return{UPDATE:"update",DELETE:"delete",INSERT:"insert"}}constructor(keyOrIndex,changeType,oldValue,newValue){this.keyOrIndex=keyOrIndex;this.changeType=changeType;this.oldValue=oldValue;this.newValue=newValue}}class NodeChangeTracker{constructor(path){this.path=path;this._changes=[];this._oldValue=undefined;this._newValue=undefined}addDelete(keyOrIndex,oldValue){this._changes.push(new NodeChange(keyOrIndex,NodeChange.CHANGE_TYPE.DELETE,oldValue,null))}addUpdate(keyOrIndex,oldValue,newValue){this._changes.push(new NodeChange(keyOrIndex,NodeChange.CHANGE_TYPE.UPDATE,oldValue,newValue))}addInsert(keyOrIndex,newValue){this._changes.push(new NodeChange(keyOrIndex,NodeChange.CHANGE_TYPE.INSERT,null,newValue))}add(keyOrIndex,currentValue,newValue){if(currentValue===null){if(newValue===null){throw new Error(`Wrong logic for node change on "${this.nodeInfo.path}/${keyOrIndex}" - both old and new values are null`)}this.addInsert(keyOrIndex,newValue)}else if(newValue===null){this.addDelete(keyOrIndex,currentValue)}else{this.addUpdate(keyOrIndex,currentValue,newValue)}}get updates(){return this._changes.filter(change=>change.changeType===NodeChange.CHANGE_TYPE.UPDATE)}get deletes(){return this._changes.filter(change=>change.changeType===NodeChange.CHANGE_TYPE.DELETE)}get inserts(){return this._changes.filter(change=>change.changeType===NodeChange.CHANGE_TYPE.INSERT)}get all(){return this._changes}get totalChanges(){return this._changes.length}get(keyOrIndex){return this._changes.find(change=>change.keyOrIndex===keyOrIndex)}hasChanged(keyOrIndex){return!!this.get(keyOrIndex)}get newValue(){if(typeof this._newValue==="object"){return this._newValue}if(typeof this._oldValue==="undefined"){throw new TypeError(`oldValue is not set`)}let newValue={};Object.keys(this.oldValue).forEach(key=>newValue[key]=oldValue[key]);this.deletes.forEach(change=>delete newValue[change.key]);this.updates.forEach(change=>newValue[change.key]=change.newValue);this.inserts.forEach(change=>newValue[change.key]=change.newValue);return newValue}set newValue(value){this._newValue=value}get oldValue(){if(typeof this._oldValue==="object"){return this._oldValue}if(typeof this._newValue==="undefined"){throw new TypeError(`newValue is not set`)}let oldValue={};Object.keys(this.newValue).forEach(key=>oldValue[key]=newValue[key]);this.deletes.forEach(change=>oldValue[change.key]=change.oldValue);this.updates.forEach(change=>oldValue[change.key]=change.oldValue);this.inserts.forEach(change=>delete oldValue[change.key]);return oldValue}set oldValue(value){this._oldValue=value}get typeChanged(){return typeof this.oldValue!==typeof this.newValue||this.oldValue instanceof Array&&!(this.newValue instanceof Array)||this.newValue instanceof Array&&!(this.oldValue instanceof Array)}static create(path,oldValue,newValue){const changes=new NodeChangeTracker(path);changes.oldValue=oldValue;changes.newValue=newValue;typeof oldValue==="object"&&Object.keys(oldValue).forEach(key=>{if(typeof newValue==="object"&&key in newValue&&newValue!==null){changes.add(key,oldValue[key],newValue[key])}else{changes.add(key,oldValue[key],null)}});typeof newValue==="object"&&Object.keys(newValue).forEach(key=>{if(typeof oldValue!=="object"||!(key in oldValue)||oldValue[key]===null){changes.add(key,null,newValue[key])}});return changes}}module.exports={Node:Node,NodeInfo:NodeInfo}},{"./node-info":34,"./node-value-types":36,"./storage":40,colors:22}],38:[function(require,module,exports){const{debug:debug,ID:ID,PathReference:PathReference,PathInfo:PathInfo,ascii85:ascii85}=require("acebase-core");const{NodeInfo:NodeInfo}=require("./node-info");const{NodeLocker:NodeLocker}=require("./node-lock");const{VALUE_TYPES:VALUE_TYPES}=require("./node-value-types");const{Storage:Storage,StorageSettings:StorageSettings,NodeNotFoundError:NodeNotFoundError}=require("./storage");class ICustomStorageNodeMetaData{constructor(){this.revision="";this.revision_nr=0;this.created=0;this.modified=0;this.type=0}}class ICustomStorageNode extends ICustomStorageNodeMetaData{constructor(){super();this.value=null}}class CustomStorageTransaction{constructor(target){this.target={get originalPath(){return target.path},path:target.path,get write(){return target.write}};this.id=ID.generate()}get(path){throw new Error(`CustomStorageTransaction.get must be overridden by subclass`)}set(path,node){throw new Error(`CustomStorageTransaction.set must be overridden by subclass`)}remove(path){throw new Error(`CustomStorageTransaction.remove must be overridden by subclass`)}childrenOf(path,include,checkCallback,addCallback){throw new Error(`CustomStorageTransaction.childrenOf must be overridden by subclass`)}descendantsOf(path,include,checkCallback,addCallback){throw new Error(`CustomStorageTransaction.descendantsOf must be overridden by subclass`)}getMultiple(paths){const map=new Map;return Promise.all(paths.map(path=>this.get(path).then(val=>map.set(path,val)))).then(done=>map)}async setMultiple(nodes){await Promise.all(paths.map(({path:path,node:node})=>this.set(path,node)))}async removeMultiple(paths){await Promise.all(paths.map(path=>this.remove(path)))}rollback(reason){throw new Error(`CustomStorageTransaction.rollback must be overridden by subclass`)}commit(){throw new Error(`CustomStorageTransaction.rollback must be overridden by subclass`)}async moveToParentPath(targetPath){const currentPath=this._lock&&this._lock.path||this.target.path;if(currentPath===targetPath){return targetPath}const pathInfo=CustomStorageHelpers.PathInfo.get(targetPath);if(pathInfo.isParentOf(currentPath)){if(this._lock){this._lock=await this._lock.moveToParent()}}else{throw new Error(`Locking issue. Locked path "${this._lock.path}" is not a child/descendant of "${targetPath}"`)}this.target.path=targetPath;return targetPath}}class CustomStorageSettings extends StorageSettings{constructor(settings){super(settings);settings=settings||{};if(typeof settings.ready!=="function"){throw new Error(`ready must be a function`)}if(typeof settings.getTransaction!=="function"){throw new Error(`getTransaction must be a function`)}this.name=settings.name;this.info=`${this.name||"CustomStorage"} realtime database`;this.locking=settings.locking!==false;this.ready=settings.ready;const useLocking=this.locking;const nodeLocker=useLocking?new NodeLocker:null;this.getTransaction=async({path:path,write:write})=>{const transaction=await settings.getTransaction({path:path,write:write});console.assert(typeof transaction.id==="string",`transaction id not set`);const rollback=transaction.rollback;const commit=transaction.commit;transaction.commit=async()=>{const ret=await commit.call(transaction);if(useLocking){await transaction._lock.release("commit")}return ret};transaction.rollback=async reason=>{const ret=await rollback.call(transaction,reason);if(useLocking){await transaction._lock.release("rollback")}return ret};if(useLocking){transaction._lock=await nodeLocker.lock(path,transaction.id,write,`${this.name}::getTransaction`)}return transaction}}}class CustomStorageNodeAddress{constructor(containerPath){this.path=containerPath}}class CustomStorageNodeInfo extends NodeInfo{constructor(info){super(info);this.address;this.revision=info.revision;this.revision_nr=info.revision_nr;this.created=info.created;this.modified=info.modified}}class CustomStorageHelpers{static ChildPathsSql(path,columnName="path"){const where=path===""?`${columnName} <> '' AND ${columnName} NOT LIKE '%/%'`:`(${columnName} LIKE '${path}/%' OR ${columnName} LIKE '${path}[%') AND ${columnName} NOT LIKE '${path}/%/%' AND ${columnName} NOT LIKE '${path}[%]/%' AND ${columnName} NOT LIKE '${path}[%][%'`;return where}static ChildPathsRegex(path){return new RegExp(`^${path}(?:/[^/[]+|[[0-9]+])$`)}static DescendantPathsSql(path,columnName="path"){const where=path===""?`${columnName} <> ''`:`${columnName} LIKE '${path}/%' OR ${columnName} LIKE '${path}[%'`;return where}static DescendantPathsRegex(path){return new RegExp(`^${path}(?:/[^/[]+|[[0-9]+])`)}static get PathInfo(){return PathInfo}}class CustomStorage extends Storage{constructor(dbname,settings){super(dbname,settings);this._init()}async _init(){this._customImplementation=this.settings;this.debug.log(`Database "${this.name}" details:`.intro);this.debug.log(`- Type: CustomStorage`);this.debug.log(`- Path: ${this.settings.path}`);this.debug.log(`- Max inline value size: ${this.settings.maxInlineValueSize}`.intro);this.debug.log(`- Autoremove undefined props: ${this.settings.removeVoidProperties}`);await this._customImplementation.ready();const transaction=await this._customImplementation.getTransaction({path:"",write:true});const info=await this.getNodeInfo("",{transaction:transaction});if(!info.exists){await this._writeNode("",{},{transaction:transaction})}await transaction.commit();if(this.indexes.supported){await this.indexes.load()}this.emit("ready")}_storeNode(path,node,options){const getTypedChildValue=val=>{if(val===null){throw new Error(`Not allowed to store null values. remove the property`)}else if(["string","number","boolean"].includes(typeof val)){return val}else if(val instanceof Date){return{type:VALUE_TYPES.DATETIME,value:val.getTime()}}else if(val instanceof PathReference){return{type:VALUE_TYPES.REFERENCE,value:child.path}}else if(val instanceof ArrayBuffer){return{type:VALUE_TYPES.BINARY,value:ascii85.encode(val)}}else if(typeof val==="object"){console.assert(Object.keys(val).length===0,"child object stored in parent can only be empty");return val}};const unprocessed=`Caller should have pre-processed the value by converting it to a string`;if(node.type===VALUE_TYPES.ARRAY&&node.value instanceof Array){console.warn(`Unprocessed array. ${unprocessed}`);const obj={};for(let i=0;i{node.value[key]=getTypedChildValue(original[key])})}return options.transaction.set(path,node)}_processReadNodeValue(node){const getTypedChildValue=val=>{if(val.type===VALUE_TYPES.BINARY){return ascii85.decode(val.value)}else if(val.type===VALUE_TYPES.DATETIME){return new Date(val.value)}else if(val.type===VALUE_TYPES.REFERENCE){return new PathReference(val.value)}else{throw new Error(`Unhandled child value type ${val.type}`)}};switch(node.type){case VALUE_TYPES.ARRAY:case VALUE_TYPES.OBJECT:{const obj=node.value;Object.keys(obj).forEach(key=>{let item=obj[key];if(typeof item==="object"&&"type"in item){obj[key]=getTypedChildValue(item)}});node.value=obj;break}case VALUE_TYPES.BINARY:{node.value=ascii85.decode(node.value);break}case VALUE_TYPES.REFERENCE:{node.value=new PathReference(node.value);break}case VALUE_TYPES.STRING:{break}default:throw new Error(`Invalid standalone record value type`)}}async _readNode(path,options){let node=await options.transaction.get(path);if(node===null){return null}if(typeof node!=="object"){throw new Error(`CustomStorage get function must return an ICustomStorageNode object. Use JSON.parse if your set function stored it as a string`)}this._processReadNodeValue(node);return node}_getTypeFromStoredValue(val){let type;if(typeof val==="string"){type=VALUE_TYPES.STRING}else if(typeof val==="number"){type=VALUE_TYPES.NUMBER}else if(typeof val==="boolean"){type=VALUE_TYPES.BOOLEAN}else if(val instanceof Array){type=VALUE_TYPES.ARRAY}else if(typeof val==="object"){if("type"in val){type=val.type;val=val.value;if(type===VALUE_TYPES.DATETIME){val=new Date(val)}else if(type===VALUE_TYPES.REFERENCE){val=new PathReference(val)}}else{type=VALUE_TYPES.OBJECT}}else{throw new Error(`Unknown value type`)}return{type:type,value:val}}async _writeNode(path,value,options){if(this.valueFitsInline(value)&&path!==""){throw new Error(`invalid value to store in its own node`)}else if(path===""&&(typeof value!=="object"||value instanceof Array)){throw new Error(`Invalid root node value. Must be an object`)}const transaction=options.transaction;const currentRow=await this._readNode(path,{transaction:transaction});const revision=options.revision||ID.generate();let mainNode={type:VALUE_TYPES.OBJECT,value:{}};const childNodeValues={};if(value instanceof Array){mainNode.type=VALUE_TYPES.ARRAY;const obj={};for(let i=0;i{const val=value[key];delete mainNode.value[key];if(val===null){return}else if(typeof val==="undefined"){if(this.settings.removeVoidProperties===true){delete value[key];return}else{throw new Error(`Property "${key}" has invalid value. Cannot store undefined values. Set removeVoidProperties option to true to automatically remove undefined properties`)}}if(this.valueFitsInline(val)){mainNode.value[key]=val}else{childNodeValues[key]=val}})}if(currentRow){this.debug.log(`Node "/${path}" is being ${options.merge?"updated":"overwritten"}`.cyan);if(currentIsObjectOrArray||newIsObjectOrArray){const pathInfo=PathInfo.get(path);const keys=[];let checkExecuted=false;const includeChildCheck=childPath=>{checkExecuted=true;if(!pathInfo.isParentOf(childPath)){throw new Error(`"${childPath}" is not a child of "${path}" - childrenOf must only check and return paths that are children`)}return true};const addChildPath=childPath=>{if(!checkExecuted){throw new Error(`${this._customImplementation.info} childrenOf did not call checkCallback before addCallback`)}const key=PathInfo.get(childPath).key;keys.push(key);return true};await transaction.childrenOf(path,{metadata:false,value:false},includeChildCheck,addChildPath);children.current=children.current.concat(keys);if(newIsObjectOrArray){if(options&&options.merge){children.new=children.current.slice()}Object.keys(value).forEach(key=>{if(!children.new.includes(key)){children.new.push(key)}})}const changes={insert:children.new.filter(key=>!children.current.includes(key)),update:children.new.filter(key=>children.current.includes(key)),delete:options&&options.merge?Object.keys(value).filter(key=>value[key]===null):children.current.filter(key=>!children.new.includes(key))};const writePromises=Object.keys(childNodeValues).map(key=>{const childPath=pathInfo.childPath(key);const childValue=childNodeValues[key];return this._writeNode(childPath,childValue,{transaction:transaction,revision:revision,merge:false})});const movingNodes=keys.filter(key=>key in mainNode.value);const deleteDedicatedKeys=changes.delete.concat(movingNodes);const deletePromises=deleteDedicatedKeys.map(key=>{const childPath=pathInfo.childPath(key);return this._deleteNode(childPath,{transaction:transaction})});const promises=writePromises.concat(deletePromises);await Promise.all(promises)}return await this._storeNode(path,{type:mainNode.type,value:mainNode.value,revision:currentRow.revision,revision_nr:currentRow.revision_nr+1,created:currentRow.created,modified:Date.now()},{transaction:transaction})}else{this.debug.log(`Node "/${path}" is being created`.cyan);const promises=Object.keys(childNodeValues).map(key=>{const childPath=PathInfo.getChildPath(path,key);const childValue=childNodeValues[key];return this._writeNode(childPath,childValue,{transaction:transaction,revision:revision,merge:false})});const p=this._storeNode(path,{type:mainNode.type,value:mainNode.value,revision:revision,revision_nr:1,created:Date.now(),modified:Date.now()},{transaction:transaction});promises.push(p);return Promise.all(promises)}}async _deleteNode(path,options){const pathInfo=PathInfo.get(path);this.debug.log(`Node "/${path}" is being deleted`.cyan);const deletePaths=[path];let checkExecuted=false;const includeDescendantCheck=descPath=>{checkExecuted=true;if(!pathInfo.isAncestorOf(descPath)){throw new Error(`"${descPath}" is not a descendant of "${path}" - descendantsOf must only check and return paths that are descendants`)}return true};const addDescendant=descPath=>{if(!checkExecuted){throw new Error(`${this._customImplementation.info} descendantsOf did not call checkCallback before addCallback`)}deletePaths.push(descPath);return true};const transaction=options.transaction;await transaction.descendantsOf(path,{metadata:false,value:false},includeDescendantCheck,addDescendant);this.debug.log(`Nodes ${deletePaths.map(p=>`"/${p}"`).join(",")} are being deleted`.cyan);return transaction.removeMultiple(deletePaths)}getChildren(path,options){options=options||{};var callback;const generator={next(valueCallback){callback=valueCallback;return start()}};const start=async()=>{const transaction=options.transaction||await this._customImplementation.getTransaction({path:path,write:false});try{let canceled=false;await(async()=>{let node=await this._readNode(path,{transaction:transaction});if(!node){throw new NodeNotFoundError(`Node "/${path}" does not exist`)}if(![VALUE_TYPES.OBJECT,VALUE_TYPES.ARRAY].includes(node.type)){return}const isArray=node.type===VALUE_TYPES.ARRAY;const value=node.value;let keys=Object.keys(value);if(options.keyFilter){keys=keys.filter(key=>options.keyFilter.includes(key))}const pathInfo=PathInfo.get(path);keys.length>0&&keys.every(key=>{let child=this._getTypeFromStoredValue(value[key]);const info=new CustomStorageNodeInfo({path:pathInfo.childPath(key),key:isArray?null:key,index:isArray?key:null,type:child.type,address:null,exists:true,value:child.value,revision:node.revision,revision_nr:node.revision_nr,created:node.created,modified:node.modified});canceled=callback(info)===false;return!canceled});if(canceled){return}let checkExecuted=false;const includeChildCheck=childPath=>{checkExecuted=true;if(!pathInfo.isParentOf(childPath)){throw new Error(`"${childPath}" is not a child of "${path}" - childrenOf must only check and return paths that are children`)}if(options.keyFilter){const key=PathInfo.get(childPath).key;return options.keyFilter.includes(key)}return true};const addChildNode=(childPath,node)=>{if(!checkExecuted){throw new Error(`${this._customImplementation.info} childrenOf did not call checkCallback before addCallback`)}const key=PathInfo.get(childPath).key;const info=new CustomStorageNodeInfo({path:childPath,type:node.type,key:isArray?null:key,index:isArray?key:null,address:new CustomStorageNodeAddress(childPath),exists:true,value:null,revision:node.revision,revision_nr:node.revision_nr,created:new Date(node.created),modified:new Date(node.modified)});canceled=callback(info)===false;return!canceled};await transaction.childrenOf(path,{metadata:true,value:false},includeChildCheck,addChildNode)})();if(!options.transaction){await transaction.commit()}return canceled}catch(err){if(!options.transaction){await transaction.rollback(err)}throw err}};return generator}async getNode(path,options){options=options||{};const transaction=options.transaction||await this._customImplementation.getTransaction({path:path,write:false});try{const node=await(async()=>{const filtered=options.include||options.exclude||options.child_objects===false;const pathInfo=PathInfo.get(path);const targetNode=await this._readNode(path,{transaction:transaction});if(!targetNode){if(path===""){return{value:null}}const lockPath=await transaction.moveToParentPath(pathInfo.parentPath);console.assert(lockPath===pathInfo.parentPath,`transaction.moveToParentPath() did not move to the right parent path of "${path}"`);let parentNode=await this._readNode(pathInfo.parentPath,{transaction:transaction});if(parentNode&&[VALUE_TYPES.OBJECT,VALUE_TYPES.ARRAY].includes(parentNode.type)&&pathInfo.key in parentNode){const childValueInfo=this._getTypeFromStoredValue(parentNode.value[pathInfo.key]);return{revision:parentNode.revision,revision_nr:parentNode.revision_nr,created:parentNode.created,modified:parentNode.modified,type:childValueInfo.type,value:childValueInfo.value}}return{value:null}}const includeCheck=options.include?new RegExp("^"+options.include.map(p=>"(?:"+p.replace(/\*/g,"[^/\\[]+")+")").join("|")+"(?:$|[/\\[])"):null;const excludeCheck=options.exclude?new RegExp("^"+options.exclude.map(p=>"(?:"+p.replace(/\*/g,"[^/\\[]+")+")").join("|")+"(?:$|[/\\[])"):null;let checkExecuted=false;const includeDescendantCheck=descPath=>{checkExecuted=true;if(!pathInfo.isAncestorOf(descPath)){throw new Error(`"${descPath}" is not a descendant of "${path}" - descendantsOf must only check and return paths that are descendants`)}if(!filtered){return true}let checkPath=descPath.slice(path.length);if(checkPath[0]==="/"){checkPath=checkPath.slice(1)}let include=(includeCheck?includeCheck.test(checkPath):true)&&(excludeCheck?!excludeCheck.test(checkPath):true);if(include&&options.child_objects===false&&(pathInfo.isParentOf(descPath)&&[VALUE_TYPES.OBJECT,VALUE_TYPES.ARRAY].includes(childNode.type)||PathInfo.getPathKeys(descPath).length>pathInfo.pathKeys.length+1)){include=false}return include};const descRows=[];const addDescendant=(descPath,node)=>{if(!checkExecuted){throw new Error(`${this._customImplementation.info} descendantsOf did not call checkCallback before addCallback`)}this._processReadNodeValue(node);node.path=descPath;descRows.push(node);return true};await transaction.descendantsOf(path,{metadata:true,value:true},includeDescendantCheck,addDescendant);this.debug.log(`Read node "/${path}" and ${filtered?"(filtered) ":""}descendants from ${descRows.length+1} records`.magenta);const result=targetNode;const objectToArray=obj=>{const arr=[];Object.keys(obj).forEach(key=>{let index=parseInt(key);arr[index]=obj[index]});return arr};if(targetNode.type===VALUE_TYPES.ARRAY){result.value=objectToArray(result.value)}if(targetNode.type===VALUE_TYPES.OBJECT||targetNode.type===VALUE_TYPES.ARRAY){const targetPathKeys=PathInfo.getPathKeys(path);let value=targetNode.value;for(let i=0;i{console.assert(!(childKey in parent[key]),"child key is in parent value already?! HOW?!");parent[key][childKey]=nodeValue[childKey]})}else{parent[key]=nodeValue}parent=parent[key]}}}else if(descRows.length>0){throw new Error(`multiple records found for non-object value!`)}if(options.child_objects===false){Object.keys(result.value).forEach(key=>{if(typeof result.value[key]==="object"&&result.value[key].constructor===Object){console.assert(Object.keys(result.value[key]).length===0);delete result.value[key]}})}if(options.exclude){const process=(obj,keys)=>{if(typeof obj!=="object"){return}const key=keys[0];if(key==="*"){Object.keys(obj).forEach(k=>{process(obj[k],keys.slice(1))})}else if(keys.length>1){key in obj&&process(obj[key],keys.slice(1))}else{delete obj[key]}};options.exclude.forEach(path=>{const checkKeys=PathInfo.getPathKeys(path);process(result.value,checkKeys)})}return result})();if(!options.transaction){await transaction.commit()}return node}catch(err){if(!options.transaction){await transaction.rollback(err)}throw err}}async getNodeInfo(path,options){options=options||{};const pathInfo=PathInfo.get(path);const transaction=options.transaction||await this._customImplementation.getTransaction({path:path,write:true});try{const node=await this._readNode(path,{transaction:transaction});const info=new CustomStorageNodeInfo({path:path,key:typeof pathInfo.key==="string"?pathInfo.key:null,index:typeof pathInfo.key==="number"?pathInfo.key:null,type:node?node.type:0,exists:node!==null,address:node?new CustomStorageNodeAddress(path):null,created:node?new Date(node.created):null,modified:node?new Date(node.modified):null,revision:node?node.revision:null,revision_nr:node?node.revision_nr:null});if(!node&&path!==""){const lockPath=await transaction.moveToParentPath(pathInfo.parentPath);console.assert(lockPath===pathInfo.parentPath,`transaction.moveToParentPath() did not move to the right parent path of "${path}"`);const parent=await this._readNode(pathInfo.parentPath,{transaction:transaction});if(parent&&[VALUE_TYPES.OBJECT,VALUE_TYPES.ARRAY].includes(parent.type)&&pathInfo.key in parent.value){info.exists=true;info.value=parent.value[pathInfo.key];info.address=null;info.type=parent.type;info.created=new Date(parent.created);info.modified=new Date(parent.modified);info.revision=parent.revision;info.revision_nr=parent.revision_nr}else{info.address=null}}if(!options.transaction){await transaction.commit()}return info}catch(err){if(!options.transaction){await transaction.rollback(err)}throw err}}async removeNode(path,options){if(path===""){return Promise.reject(new Error(`Cannot remove the root node`))}options=options||{};const pathInfo=PathInfo.get(path);const transaction=options.transaction||this._customImplementation.getTransaction({path:path,write:true});try{await this.updateNode(pathInfo.parentPath,{[pathInfo.key]:null},{transaction:transaction});if(!options.transaction){await transaction.commit()}}catch(err){if(!options.transaction){await transaction.rollback(err)}throw err}}async setNode(path,value,options){const pathInfo=PathInfo.get(path);options=options||{};const transaction=options.transaction||await this._customImplementation.getTransaction({path:path,write:true});try{if(path===""){if(value===null||typeof value!=="object"||value instanceof Array||value instanceof ArrayBuffer||"buffer"in value&&value.buffer instanceof ArrayBuffer){throw new Error(`Invalid value for root node: ${value}`)}await this._writeNodeWithTracking("",value,{merge:false,transaction:transaction})}else if(typeof options.assert_revision!=="undefined"){const info=await this.getNodeInfo(path,{transaction:transaction});if(info.revision!==options.assert_revision){throw new NodeRevisionError(`revision '${info.revision}' does not match requested revision '${options.assert_revision}'`)}if(info.address&&info.address.path===path&&!this.valueFitsInline(value)){await this._writeNodeWithTracking(path,value,{merge:false,transaction:transaction})}else{const lockPath=await transaction.moveToParentPath(pathInfo.parentPath);console.assert(lockPath===pathInfo.parentPath,`transaction.moveToParentPath() did not move to the right parent path of "${path}"`);await this._writeNodeWithTracking(pathInfo.parentPath,{[pathInfo.key]:value},{merge:true,transaction:transaction})}}else{const lockPath=await transaction.moveToParentPath(pathInfo.parentPath);console.assert(lockPath===pathInfo.parentPath,`transaction.moveToParentPath() did not move to the right parent path of "${path}"`);await this.updateNode(pathInfo.parentPath,{[pathInfo.key]:value},{transaction:transaction})}if(!options.transaction){await transaction.commit()}}catch(err){if(!options.transaction){await transaction.rollback(err)}throw err}}async updateNode(path,updates,options){if(typeof updates!=="object"){throw new Error(`invalid updates argument`)}options=options||{};const transaction=options.transaction||await this._customImplementation.getTransaction({path:path,write:true});try{const nodeInfo=await this.getNodeInfo(path,{transaction:transaction});const pathInfo=PathInfo.get(path);if(nodeInfo.exists&&nodeInfo.address&&nodeInfo.address.path===path){await this._writeNodeWithTracking(path,updates,{transaction:transaction,merge:true})}else if(nodeInfo.exists){const pathInfo=PathInfo.get(path);const lockPath=await transaction.moveToParentPath(pathInfo.parentPath);console.assert(lockPath===pathInfo.parentPath,`transaction.moveToParentPath() did not move to the right parent path of "${path}"`);await this._writeNodeWithTracking(pathInfo.parentPath,{[pathInfo.key]:value},{transaction:transaction,merge:true})}else{const lockPath=await transaction.moveToParentPath(pathInfo.parentPath);console.assert(lockPath===pathInfo.parentPath,`transaction.moveToParentPath() did not move to the right parent path of "${path}"`);await this.updateNode(pathInfo.parentPath,{[pathInfo.key]:updates},{transaction:transaction})}if(!options.transaction){await transaction.commit()}}catch(err){if(!options.transaction){await transaction.rollback(err)}throw err}}}module.exports={CustomStorageNodeAddress:CustomStorageNodeAddress,CustomStorageNodeInfo:CustomStorageNodeInfo,CustomStorage:CustomStorage,CustomStorageSettings:CustomStorageSettings,CustomStorageHelpers:CustomStorageHelpers,CustomStorageTransaction:CustomStorageTransaction,ICustomStorageNodeMetaData:ICustomStorageNodeMetaData,ICustomStorageNode:ICustomStorageNode}},{"./node-info":34,"./node-lock":35,"./node-value-types":36,"./storage":40,"acebase-core":11}],39:[function(require,module,exports){const{debug:debug,ID:ID,PathReference:PathReference,PathInfo:PathInfo,ascii85:ascii85}=require("acebase-core");const{NodeInfo:NodeInfo}=require("./node-info");const{VALUE_TYPES:VALUE_TYPES}=require("./node-value-types");const{Storage:Storage,StorageSettings:StorageSettings,NodeNotFoundError:NodeNotFoundError}=require("./storage");class LocalStorageSettings extends StorageSettings{constructor(settings){super(settings);settings=settings||{};this.session=settings.session===true;this.provider=typeof settings.provider==="object"?settings.provider:null}}class LocalStorageNodeAddress{constructor(containerPath){this.path=containerPath}}class LocalStorageNodeInfo extends NodeInfo{constructor(info){super(info);this.address;this.revision=info.revision;this.revision_nr=info.revision_nr;this.created=info.created;this.modified=info.modified}}class LocalStorage extends Storage{constructor(dbname,settings){super(dbname,settings);this._init()}_init(){if(this.settings.provider!==null&&typeof this.settings.provider==="object"){this._localStorage=this.settings.provider}else{if(!this.settings.session&&typeof localStorage==="undefined"){throw new Error(`No localStorage available. If you are on Node: npm i node-localstorage`)}if(this.settings.session===true&&typeof sessionStorage==="undefined"){throw new Error(`No sessionStorage available`)}this._localStorage=this.settings.session===true?sessionStorage:localStorage}this.debug.log(`Database "${this.name}" details:`.intro);this.debug.log(`- Type: LocalStorage`);this.debug.log(`- Max inline value size: ${this.settings.maxInlineValueSize}`.intro);return this.getNodeInfo("").then(info=>{if(!info.exists){return this._writeNode("",{})}}).then(()=>{return this.indexes.supported&&this.indexes.load()}).then(()=>{this.emit("ready")})}get _keyPrefix(){return`${this.name}.acebase::`}_getPathFromKey(key){return key.slice(this._keyPrefix.length)}_getKeyFromPath(path){return`${this._keyPrefix}${path}`}_storeNode(path,info){const getTypedChildValue=val=>{if(val===null){throw new Error(`Not allowed to store null values. remove the property`)}else if(["string","number","boolean"].includes(typeof val)){return val}else if(val instanceof Date){return{type:VALUE_TYPES.DATETIME,value:val.getTime()}}else if(val instanceof PathReference){return{type:VALUE_TYPES.REFERENCE,value:child.path}}else if(val instanceof ArrayBuffer){return{type:VALUE_TYPES.BINARY,value:ascii85.encode(val)}}else if(typeof val==="object"){console.assert(Object.keys(val).length===0,"child object stored in parent can only be empty");return val}};const unprocessed=`Caller should have pre-processed the value by converting it to a string`;if(info.type===VALUE_TYPES.ARRAY&&info.value instanceof Array){console.warn(`Unprocessed array. ${unprocessed}`);const obj={};for(let i=0;i{info.value[key]=getTypedChildValue(original[key])})}const json=JSON.stringify(info);this._localStorage.setItem(this._getKeyFromPath(path),json)}_readNode(path){let val=this._localStorage.getItem(this._getKeyFromPath(path));if(val===null){return null}val=JSON.parse(val);const getTypedChildValue=val=>{if(val.type===VALUE_TYPES.BINARY){return ascii85.decode(val.value)}else if(val.type===VALUE_TYPES.DATETIME){return new Date(val.value)}else if(val.type===VALUE_TYPES.REFERENCE){return new PathReference(val.value)}else{throw new Error(`Unhandled child value type ${val.type}`)}};const node={type:val.type,value:val.value,revision:val.revision,revision_nr:val.revision_nr,created:val.created,modified:val.modified};switch(val.type){case VALUE_TYPES.ARRAY:case VALUE_TYPES.OBJECT:{const obj=val.value;Object.keys(obj).forEach(key=>{let item=obj[key];if(typeof item==="object"&&"type"in item){obj[key]=getTypedChildValue(item)}});node.value=obj;break}case VALUE_TYPES.BINARY:{node.value=ascii85.decode(val.value);break}case VALUE_TYPES.STRING:{node.value=val.value;break}case VALUE_TYPES.REFERENCE:{node.value=new PathReference(val.value);break}default:throw new Error(`Invalid standalone record value type`)}return node}_getTypeFromStoredValue(val){let type;if(typeof val==="string"){type=VALUE_TYPES.STRING}else if(typeof val==="number"){type=VALUE_TYPES.NUMBER}else if(typeof val==="boolean"){type=VALUE_TYPES.BOOLEAN}else if(val instanceof Array){type=VALUE_TYPES.ARRAY}else if(typeof val==="object"){if("type"in val){type=val.type;val=val.value;if(type===VALUE_TYPES.DATETIME){val=new Date(val)}else if(type===VALUE_TYPES.REFERENCE){val=new PathReference(val)}}else{type=VALUE_TYPES.OBJECT}}else{throw new Error(`Unknown value type`)}return{type:type,value:val}}_writeNode(path,value,options={merge:false,revision:null}){if(this.valueFitsInline(value)&&path!==""){throw new Error(`invalid value to store in its own node`)}else if(path===""&&(typeof value!=="object"||value instanceof Array)){throw new Error(`Invalid root node value. Must be an object`)}const currentRow=this._readNode(path);const newRevision=options&&options.revision||ID.generate();let mainNode={type:VALUE_TYPES.OBJECT,value:{}};const childNodeValues={};if(value instanceof Array){mainNode.type=VALUE_TYPES.ARRAY;const obj={};for(let i=0;i{const val=value[key];delete mainNode.value[key];if(val===null){return}else if(typeof val==="undefined"){if(this.settings.removeVoidProperties===true){delete value[key];return}else{throw new Error(`Property "${key}" has invalid value. Cannot store undefined values. Set removeVoidProperties option to true to automatically remove undefined properties`)}}if(this.valueFitsInline(val)){mainNode.value[key]=val}else{childNodeValues[key]=val}})}if(currentRow){this.debug.log(`Node "/${path}" is being ${options.merge?"updated":"overwritten"}`.cyan);if(currentIsObjectOrArray||newIsObjectOrArray){const pathInfo=PathInfo.get(path);const keys=[];for(let i=0;i{if(!children.new.includes(key)){children.new.push(key)}})}const changes={insert:children.new.filter(key=>!children.current.includes(key)),update:children.new.filter(key=>children.current.includes(key)),delete:options&&options.merge?Object.keys(value).filter(key=>value[key]===null):children.current.filter(key=>!children.new.includes(key))};Object.keys(childNodeValues).map(key=>{const childPath=PathInfo.getChildPath(path,key);const childValue=childNodeValues[key];this._writeNode(childPath,childValue,{revision:newRevision,merge:false})});const movingNodes=keys.filter(key=>key in mainNode.value);const deleteDedicatedKeys=changes.delete.concat(movingNodes);deleteDedicatedKeys.forEach(key=>{const childPath=PathInfo.getChildPath(path,key);this._deleteNode(childPath)})}this._storeNode(path,{type:mainNode.type,value:mainNode.value,revision:currentRow.revision,revision_nr:currentRow.revision_nr+1,created:currentRow.created,modified:Date.now()})}else{this.debug.log(`Node "/${path}" is being created`.cyan);Object.keys(childNodeValues).map(key=>{const childPath=PathInfo.getChildPath(path,key);const childValue=childNodeValues[key];this._writeNode(childPath,childValue,{revision:newRevision,merge:false})});this._storeNode(path,{type:mainNode.type,value:mainNode.value,revision:newRevision,revision_nr:1,created:Date.now(),modified:Date.now()})}}_deleteNode(path){const pathInfo=PathInfo.get(path);this.debug.log(`Node "/${path}" is being deleted`.cyan);this._localStorage.removeItem(this._getKeyFromPath(path));for(let i=0;i{let lock,canceled=false;const tid=options&&options.tid||ID.generate();return this.nodeLocker.lock(path,tid,false,"getChildren").then(l=>{lock=l;let row=this._localStorage.getItem(this._getKeyFromPath(path));if(!row){throw new NodeNotFoundError(`Node "/${path}" does not exist`)}row=JSON.parse(row);if(![VALUE_TYPES.OBJECT,VALUE_TYPES.ARRAY].includes(row.type)){return}const isArray=row.type===VALUE_TYPES.ARRAY;const value=row.value;let keys=Object.keys(value);if(options.keyFilter){keys=keys.filter(key=>options.keyFilter.includes(key))}const pathInfo=PathInfo.get(path);keys.length>0&&keys.every(key=>{let child=this._getTypeFromStoredValue(value[key]);const info=new LocalStorageNodeInfo({path:pathInfo.childPath(key),key:isArray?null:key,index:isArray?key:null,type:child.type,address:null,exists:true,value:child.value,revision:row.revision,revision_nr:row.revision_nr,created:row.created,modified:row.modified});canceled=callback(info)===false;return!canceled});if(canceled){return}const childRows=[];for(let i=0;i{const row=childRows[i];if(!row){return}const key=PathInfo.get(row.path).key;if(options.keyFilter&&!options.keyFilter.includes(key)){return handleNextChild(i+1)}const info=new LocalStorageNodeInfo({path:row.path,type:row.type,key:isArray?null:key,index:isArray?key:null,address:new LocalStorageNodeAddress(row.path),exists:true,value:null,revision:row.revision,revision_nr:row.revision_nr,created:new Date(row.created),modified:new Date(row.modified)});canceled=callback(info)===false;if(!canceled){return handleNextChild(i+1)}};return handleNextChild(0)}).then(()=>{lock.release();return canceled}).catch(err=>{lock.release();throw err})};return generator}getNode(path,options={include:undefined,exclude:undefined,child_objects:true,tid:undefined}){const tid=options&&options.tid||ID.generate();let lock;return this.nodeLocker.lock(path,tid,false,"getNode").then(l=>{lock=l;const filtered=options&&(options.include||options.exclude||options.child_objects===false);const pathInfo=PathInfo.get(path);const targetRow=this._readNode(path);if(!targetRow){if(path===""){return{value:null}}return lock.moveToParent().then(parentLock=>{lock=parentLock;let parentNode=this._readNode(pathInfo.parentPath);if(parentNode&&[VALUE_TYPES.OBJECT,VALUE_TYPES.ARRAY].includes(parentNode.type)&&pathInfo.key in parentNode){return{revision:parentNode.revision,value:parentNode.value[pathInfo.key]}}return{value:null}})}const includeCheck=options.include?new RegExp("^"+options.include.map(p=>"(?:"+p.replace(/\*/g,"[^/\\[]+")+")").join("|")+"(?:$|[/\\[])"):null;const excludeCheck=options.exclude?new RegExp("^"+options.exclude.map(p=>"(?:"+p.replace(/\*/g,"[^/\\[]+")+")").join("|")+"(?:$|[/\\[])"):null;const childRows=[];for(let i=0;ipathInfo.pathKeys.length+1)){include=false}if(include){const childRow=this._readNode(otherPath);childRow.path=otherPath;childRows.push(childRow)}}this.debug.log(`Read node "/${path}" and ${filtered?"(filtered) ":""}children from ${childRows.length+1} records`.magenta);const result={revision:targetRow?targetRow.revision:null,value:targetRow.value};const objectToArray=obj=>{const arr=[];Object.keys(obj).forEach(key=>{let index=parseInt(key);arr[index]=obj[index]});return arr};if(targetRow.type===VALUE_TYPES.ARRAY){result.value=objectToArray(result.value)}if(targetRow.type===VALUE_TYPES.OBJECT||targetRow.type===VALUE_TYPES.ARRAY){const targetPathKeys=PathInfo.getPathKeys(path);let value=targetRow.value;for(let i=0;i{console.assert(!(childKey in parent[key]),"child key is in parent value already?! HOW?!");parent[key][childKey]=nodeValue[childKey]})}else{parent[key]=nodeValue}parent=parent[key]}}}else if(childRows.length>0){throw new Error(`multiple records found for non-object value!`)}if(options.child_objects===false){Object.keys(result.value).forEach(key=>{if(typeof result.value[key]==="object"&&result.value[key].constructor===Object){console.assert(Object.keys(result.value[key]).length===0);delete result.value[key]}})}if(options.exclude){const process=(obj,keys)=>{if(typeof obj!=="object"){return}const key=keys[0];if(key==="*"){Object.keys(obj).forEach(k=>{process(obj[k],keys.slice(1))})}else if(keys.length>1){key in obj&&process(obj[key],keys.slice(1))}else{delete obj[key]}};options.exclude.forEach(path=>{const checkKeys=PathInfo.getPathKeys(path);process(result.value,checkKeys)})}return result}).then(result=>{lock.release();return result}).catch(err=>{lock.release();throw err})}getNodeInfo(path,options={tid:undefined}){const pathInfo=PathInfo.get(path);const tid=options&&options.tid||ID.generate();let lock;return this.nodeLocker.lock(path,tid,false,"getNodeInfo").then(l=>{lock=l;const node=this._readNode(path);const info=new LocalStorageNodeInfo({path:path,key:typeof pathInfo.key==="string"?pathInfo.key:null,index:typeof pathInfo.key==="number"?pathInfo.key:null,type:node?node.type:0,exists:node!==null,address:node?new LocalStorageNodeAddress(path):null,created:node?new Date(node.created):null,modified:node?new Date(node.modified):null,revision:node?node.revision:null,revision_nr:node?node.revision_nr:null});if(node||path===""){return info}return lock.moveToParent().then(parentLock=>{lock=parentLock;const parent=this._readNode(pathInfo.parentPath);if(parent&&[VALUE_TYPES.OBJECT,VALUE_TYPES.ARRAY].includes(parent.type)&&pathInfo.key in parent.value){info.exists=true;info.value=parent.value[pathInfo.key];info.address=null;info.type=parent.type;info.created=new Date(parent.created);info.modified=new Date(parent.modified);info.revision=parent.revision;info.revision_nr=parent.revision_nr}else{info.address=null}return info})}).then(info=>{lock.release();return info}).catch(err=>{lock&&lock.release();throw err})}removeNode(path,options={tid:undefined}){if(path===""){return Promise.reject(new Error(`Cannot remove the root node`))}const pathInfo=PathInfo.get(path);const tid=options&&options.tid||ID.generate();return this.nodeLocker.lock(pathInfo.parentPath,tid,true,"removeNode").then(lock=>{return this.updateNode(pathInfo.parentPath,{[pathInfo.key]:null},{tid:tid}).then(result=>{lock.release();return result}).catch(err=>{lock.release();throw err})})}setNode(path,value,options={assert_revision:undefined,tid:undefined}){const pathInfo=PathInfo.get(path);let lock;const tid=options&&options.tid||ID.generate();return this.nodeLocker.lock(path,tid,true,"setNode").then(l=>{lock=l;if(path===""){if(value===null||typeof value!=="object"||value instanceof Array||value instanceof ArrayBuffer||"buffer"in value&&value.buffer instanceof ArrayBuffer){return Promise.reject(new Error(`Invalid value for root node: ${value}`))}return this._writeNodeWithTracking("",value,{merge:false,tid:tid})}if(options&&typeof options.assert_revision!=="undefined"){return this.getNodeInfo(path,{tid:lock.tid}).then(info=>{if(info.revision!==options.assert_revision){throw new NodeRevisionError(`revision '${info.revision}' does not match requested revision '${options.assert_revision}'`)}if(info.address&&info.address.path===path&&!this.valueFitsInline(value)){return this._writeNodeWithTracking(path,value,{merge:false,tid:tid})}else{return lock.moveToParent().then(parentLock=>{lock=parentLock;return this._writeNodeWithTracking(pathInfo.parentPath,{[pathInfo.key]:value},{merge:true,tid:tid})})}})}else{return lock.moveToParent().then(parentLock=>{lock=parentLock;return this.updateNode(pathInfo.parentPath,{[pathInfo.key]:value},{tid:tid})})}}).then(result=>{lock.release();return result}).catch(err=>{lock.release();throw err})}updateNode(path,updates,options={tid:undefined}){if(typeof updates!=="object"){return Promise.reject(new Error(`invalid updates argument`))}const tid=options&&options.tid||ID.generate();let lock;return this.nodeLocker.lock(path,tid,true,"updateNode").then(l=>{lock=l;return this.getNodeInfo(path,{tid:lock.tid})}).then(nodeInfo=>{const pathInfo=PathInfo.get(path);if(nodeInfo.exists&&nodeInfo.address&&nodeInfo.address.path===path){return this._writeNodeWithTracking(path,updates,{merge:true,tid:tid})}else if(nodeInfo.exists){const pathInfo=PathInfo.get(path);return lock.moveToParent().then(parentLock=>{lock=parentLock;return this._writeNodeWithTracking(pathInfo.parentPath,{[pathInfo.key]:value},{merge:true,tid:tid})})}else{return lock.moveToParent().then(parentLock=>{lock=parentLock;return this.updateNode(pathInfo.parentPath,{[pathInfo.key]:updates},{tid:tid})})}}).then(result=>{lock.release();return result}).catch(err=>{lock.release();throw err})}}module.exports={LocalStorageNodeAddress:LocalStorageNodeAddress,LocalStorageNodeInfo:LocalStorageNodeInfo,LocalStorage:LocalStorage,LocalStorageSettings:LocalStorageSettings}},{"./node-info":34,"./node-value-types":36,"./storage":40,"acebase-core":11}],40:[function(require,module,exports){(function(process){const{Utils:Utils,DebugLogger:DebugLogger,PathInfo:PathInfo,ID:ID,PathReference:PathReference,ascii85:ascii85}=require("acebase-core");const{NodeLocker:NodeLocker}=require("./node-lock");const{VALUE_TYPES:VALUE_TYPES,getValueTypeName:getValueTypeName}=require("./node-value-types");const{NodeInfo:NodeInfo}=require("./node-info");const{EventEmitter:EventEmitter}=require("events");const{cloneObject:cloneObject,compareValues:compareValues,getChildValues:getChildValues,encodeString:encodeString}=Utils;const colors=require("colors");class NodeNotFoundError extends Error{}class NodeRevisionError extends Error{}class ClusterSettings{constructor(settings){settings=settings||{};this.enabled=settings.enabled===true;this.isMaster=settings.isMaster===true;this.master=this.isMaster?null:settings.master;this.workers=this.isMaster?settings.workers:null}}class ClusterManager extends EventEmitter{constructor(settings){super();this.settings=new ClusterSettings(settings);if(!settings.enabled){}else if(settings.isMaster){settings.workers.forEach(worker=>{worker.on("message",data=>{const{id:id,request:request}=data;if(typeof request==="object"&&request.type==="ping"){worker.send({id:id,result:"pong"})}else{const reply=result=>{worker.send({id:id,result:result})};const broadcast=msg=>{console.assert(!("id"in msg),"message to broadcast cannot have id property, it will confuse workers because they think it is a reply to their request");settings.workers.forEach(otherWorker=>{if(otherWorker!==worker){otherWorker.send(msg)}})};this.emit("worker_request",{request:request,reply:reply,broadcast:broadcast})}})});this.request=msg=>{throw new Error(`request can only be called by worker processes!`)}}else{const master=settings.master;const requests={};this.request=msg=>{return new Promise((resolve,reject)=>{const id=ID.generate();requests[id]=resolve;master.send({id:id,request:msg})})};master.on("message",data=>{if(typeof data.id!=="undefined"){let resolve=requests[data.id];delete requests[data.id];resolve(data.result)}else{this.emit("master_notification",data)}});this.request({type:"ping"}).then(result=>{console.log(`PING master process result: ${result}`)})}}get isMaster(){return this.settings.isMaster}get enabled(){return this.settings.enabled}}class StorageSettings{constructor(settings){settings=settings||{};this.maxInlineValueSize=typeof settings.maxInlineValueSize==="number"?settings.maxInlineValueSize:50;this.removeVoidProperties=settings.removeVoidProperties===true;this.cluster=new ClusterSettings(settings.cluster);this.path=settings.path||".";if(this.path.endsWith("/")){this.path=this.path.slice(0,-1)}this.logLevel=settings.logLevel||"log";this.info=settings.info||"realtime database"}}class Storage extends EventEmitter{constructor(name,settings){super();this.name=name;this.settings=settings;this.debug=new DebugLogger(settings.logLevel,`[${name}]`);colors.setTheme({art:["magenta","bold"],intro:["dim"]});const logo=" ___ ______ ".art+"\n"+" / _ \\ | ___ \\ ".art+"\n"+" / /_\\ \\ ___ ___| |_/ / __ _ ___ ___ ".art+"\n"+" | _ |/ __/ _ \\ ___ \\/ _` / __|/ _ \\".art+"\n"+" | | | | (_| __/ |_/ / (_| \\__ \\ __/".art+"\n"+" \\_| |_/\\___\\___\\____/ \\__,_|___/\\___|".art+"\n"+(settings.info?"".padStart(40-settings.info.length," ")+settings.info.magenta+"\n":"");this.debug.write(logo);this.nodeCache={find(path){return null},update(path,info){}};this.nodeLocker=new NodeLocker;this.cluster=new ClusterManager(settings.cluster);const{DataIndex:DataIndex,ArrayIndex:ArrayIndex,FullTextIndex:FullTextIndex,GeoIndex:GeoIndex}=require("./data-index");const _indexes=[];const storage=this;this.indexes={get supported(){const pfs=require("./promise-fs");return pfs&&pfs.hasFileSystem},create(path,key,options={rebuild:false,type:undefined,include:undefined}){path=path.replace(/\/\*$/,"");const rebuild=options&&options.rebuild===true;const indexType=options&&options.type||"normal";let includeKeys=options&&options.include||[];if(typeof includeKeys==="string"){includeKeys=[includeKeys]}const existingIndex=_indexes.find(index=>index.path===path&&index.key===key&&index.type===indexType&&index.includeKeys.length===includeKeys.length&&index.includeKeys.every((key,index)=>includeKeys[index]===key));if(existingIndex&&rebuild!==true){storage.debug.log(`Index on "/${path}/*/${key}" already exists`.inverse);return Promise.resolve(existingIndex)}const index=existingIndex||(()=>{switch(indexType){case"array":return new ArrayIndex(storage,path,key,{include:options.include,config:options.config});case"fulltext":return new FullTextIndex(storage,path,key,{include:options.include,config:options.config});case"geo":return new GeoIndex(storage,path,key,{include:options.include,config:options.config});default:return new DataIndex(storage,path,key,{include:options.include,config:options.config})}})();if(!existingIndex){_indexes.push(index)}return index.build().then(()=>{return index}).catch(err=>{storage.debug.error(`Index build on "/${path}/*/${key}" failed: ${err.message} (code: ${err.code})`.red);if(!existingIndex){_indexes.splice(_indexes.indexOf(index),1)}throw err})},get(path,key=null){return _indexes.filter(index=>index.path===path&&(key===null||key===index.key))},getAll(targetPath,options={parentPaths:true,childPaths:true}){const pathKeys=PathInfo.getPathKeys(targetPath);return _indexes.filter(index=>{const indexKeys=PathInfo.getPathKeys(index.path+"/*");if(options.parentPaths&&indexKeys.every((key,i)=>{return key==="*"||pathKeys[i]===key})&&[index.key].concat(...index.includeKeys).includes(pathKeys[indexKeys.length])){return true}else if(indexKeys.length{return[key,"*"].includes(indexKeys[i])})})},list(){return _indexes.slice()},load(){_indexes.splice(0);const pfs=require("./promise-fs");if(!pfs||!pfs.readdir){return Promise.resolve()}return pfs.readdir(`${storage.settings.path}/${storage.name}.acebase`).then(files=>{const promises=[];files.forEach(fileName=>{if(fileName.endsWith(".idx")){const p=DataIndex.readFromFile(storage,fileName).then(index=>{_indexes.push(index)}).catch(err=>{storage.debug.error(err)});promises.push(p)}});return Promise.all(promises)}).catch(err=>{if(err.code!=="ENOENT"){storage.debug.error(err)}})}};const _subs={};const _supportedEvents=["value","child_added","child_changed","child_removed"];_supportedEvents.push(..._supportedEvents.map(event=>`notify_${event}`));this.subscriptions={add(path,type,callback){if(_supportedEvents.indexOf(type)<0){throw new TypeError(`Invalid event type "${type}"`)}let pathSubs=_subs[path];if(!pathSubs){pathSubs=_subs[path]=[]}pathSubs.push({created:Date.now(),type:type,callback:callback})},remove(path,type=undefined,callback=undefined){let pathSubs=_subs[path];if(!pathSubs){return}while(true){const i=pathSubs.findIndex(ps=>(type?ps.type===type:true)&&(callback?ps.callback===callback:true));if(i<0){break}pathSubs.splice(i,1)}},hasValueSubscribersForPath(path){const valueNeeded=this.getValueSubscribersForPath(path);return!!valueNeeded},getValueSubscribersForPath(path){const pathInfo=new PathInfo(path);const valueSubscribers=[];Object.keys(_subs).forEach(subscriptionPath=>{if(pathInfo.equals(subscriptionPath)||pathInfo.isDescendantOf(subscriptionPath)){let pathSubs=_subs[subscriptionPath];const eventPath=PathInfo.fillVariables(subscriptionPath,path);pathSubs.forEach(sub=>{let dataPath=null;if(sub.type==="value"||sub.type==="notify_value"){dataPath=eventPath}else if((sub.type==="child_changed"||sub.type==="notify_child_changed")&&path!==eventPath){let childKey=PathInfo.getPathKeys(path.slice(eventPath.length).replace(/^\//,""))[0];dataPath=PathInfo.getChildPath(eventPath,childKey)}else if(["child_added","child_removed","notify_child_added","notify_child_removed"].includes(sub.type)&&pathInfo.isChildOf(eventPath)){let childKey=PathInfo.getPathKeys(path.slice(eventPath.length).replace(/^\//,""))[0];dataPath=PathInfo.getChildPath(eventPath,childKey)}if(dataPath!==null&&valueSubscribers.findIndex(s=>s.type===sub.type&&s.path===eventPath)<0){valueSubscribers.push({type:sub.type,eventPath:eventPath,dataPath:dataPath,subscriptionPath:subscriptionPath})}})}});return valueSubscribers},getAllSubscribersForPath(path){const pathInfo=PathInfo.get(path);const subscribers=[];Object.keys(_subs).forEach(subscriptionPath=>{if(pathInfo.equals(subscriptionPath)||pathInfo.isDescendantOf(subscriptionPath)||pathInfo.isAncestorOf(subscriptionPath)){let pathSubs=_subs[subscriptionPath];const eventPath=PathInfo.fillVariables(subscriptionPath,path);pathSubs.forEach(sub=>{let dataPath=null;if(sub.type==="value"||sub.type==="notify_value"){dataPath=eventPath}else if(sub.type==="child_changed"||sub.type==="notify_child_changed"){let childKey=path===eventPath||pathInfo.isAncestorOf(eventPath)?"*":PathInfo.getPathKeys(path.slice(eventPath.length).replace(/^\//,""))[0];dataPath=PathInfo.getChildPath(eventPath,childKey)}else if(["child_added","child_removed","notify_child_added","notify_child_removed"].includes(sub.type)&&(pathInfo.isChildOf(eventPath)||path===eventPath||pathInfo.isAncestorOf(eventPath))){let childKey=path===eventPath||pathInfo.isAncestorOf(eventPath)?"*":PathInfo.getPathKeys(path.slice(eventPath.length).replace(/^\//,""))[0];dataPath=PathInfo.getChildPath(eventPath,childKey)}if(dataPath!==null){subscribers.push({type:sub.type,eventPath:eventPath,dataPath:dataPath,subscriptionPath:subscriptionPath})}})}});return subscribers},trigger(event,path,dataPath,oldValue,newValue){const pathSubscriptions=_subs[path]||[];pathSubscriptions.filter(sub=>sub.type===event).forEach(sub=>{sub.callback(null,dataPath,newValue,oldValue)})}}}get path(){return`${this.settings.path}/${this.name}.acebase`}valueFitsInline(value){const encoding="utf8";if(typeof value==="number"||typeof value==="boolean"||value instanceof Date){return true}else if(typeof value==="string"){if(value.length>this.settings.maxInlineValueSize){return false}const encoded=encodeString(value);return encoded.lengththis.settings.maxInlineValueSize){return false}const encoded=encodeString(value.path);return encoded.length0){hasValueSubscribers=true;let eventPaths=valueSubscribers.map(sub=>{return{path:sub.dataPath,keys:PathInfo.getPathKeys(sub.dataPath)}}).sort((a,b)=>{if(a.keys.lengthb.keys.length)return 1;return 0});let first=eventPaths[0];topEventPath=first.path;if(valueSubscribers.filter(sub=>sub.dataPath===topEventPath).every(sub=>sub.type.startsWith("notify_"))){hasValueSubscribers=false}topEventPath=PathInfo.fillVariables(topEventPath,path)}const writeNode=()=>{if(typeof options._customWriteFunction==="function"){return options._customWriteFunction()}return this._writeNode(path,value,options)};const indexes=this.indexes.getAll(path,{childPaths:true,parentPaths:true}).map(index=>({index:index,keys:PathInfo.getPathKeys(index.path)})).sort((a,b)=>{if(a.keys.lengthb.keys.length){return 1}return 0}).map(obj=>obj.index);if(eventSubscriptions.length===0&&indexes.length===0){return writeNode()}let keysFilter=[];if(indexes.length>0){indexes.sort((a,b)=>{if(typeof a._pathKeys==="undefined"){a._pathKeys=PathInfo.getPathKeys(a.path)}if(typeof b._pathKeys==="undefined"){b._pathKeys=PathInfo.getPathKeys(b.path)}if(a._pathKeys.lengthb._pathKeys.length)return 1;return 0});const topIndex=indexes[0];let topIndexPath=topIndex.path===path?path:PathInfo.fillVariables(`${topIndex.path}/*`,path);if(topIndexPath.lengthindex.path===topIndex.path).forEach(index=>{let keys=[index.key].concat(index.includeKeys);keys.forEach(key=>!keysFilter.includes(key)&&keysFilter.push(key))})}}if(!hasValueSubscribers&&options.merge===true&&keysFilter.length===0){keysFilter=Object.keys(value);if(topEventPath!==path){let trailPath=path.slice(topEventPath.length);keysFilter=keysFilter.map(key=>`${trailPath}/${key}`)}}return this.getNodeInfo(topEventPath,{transaction:transaction,tid:tid}).then(eventNodeInfo=>{if(!eventNodeInfo.exists){return null}let valueOptions={transaction:transaction,tid:tid};if(keysFilter.length>0){valueOptions.include=keysFilter}if(topEventPath===""&&typeof valueOptions.include==="undefined"){this.debug.warn(`WARNING: One or more value event listeners on the root node are causing the entire database value to be read to facilitate change tracking. Using "value", "notify_value", "child_changed" and "notify_child_changed" events on the root node are a bad practice because of the significant performance impact`)}return this.getNodeValue(topEventPath,valueOptions)}).then(currentValue=>{topEventData=currentValue;return writeNode()}).then(result=>{let newTopEventData=cloneObject(topEventData);if(newTopEventData===null){newTopEventData=path===topEventPath?value:{}}let modifiedData=newTopEventData;if(path!==topEventPath){let trailPath=path.slice(topEventPath.length).replace(/^\//,"");let trailKeys=PathInfo.getPathKeys(trailPath);while(trailKeys.length>0){let childKey=trailKeys.shift();if(!options.merge&&trailKeys.length===0){modifiedData[childKey]=value}else{if(!(childKey in modifiedData)){modifiedData[childKey]={}}modifiedData=modifiedData[childKey]}}}if(options.merge){Object.keys(value).forEach(key=>{let newValue=value[key];if(newValue!==null){modifiedData[key]=newValue}else{delete modifiedData[key]}})}else if(path===topEventPath){newTopEventData=modifiedData=value}const indexUpdates=[];indexes.map(index=>({index:index,keys:PathInfo.getPathKeys(index.path)})).sort((a,b)=>{if(a.keys.lengthb.keys.length){return-1}return 0}).forEach(({index:index})=>{let pathKeys=PathInfo.getPathKeys(topEventPath);let indexPathKeys=PathInfo.getPathKeys(index.path+"/*");let trailKeys=indexPathKeys.slice(pathKeys.length);let oldValue=topEventData;let newValue=newTopEventData;if(trailKeys.length===0){console.assert(pathKeys.length===indexPathKeys.length,"check logic");const p=index.handleRecordUpdate(topEventPath,oldValue,newValue);indexUpdates.push(p);return}const getAllIndexUpdates=(path,oldValue,newValue)=>{if(oldValue===null&&newValue===null){return[]}let pathKeys=PathInfo.getPathKeys(path);let indexPathKeys=PathInfo.getPathKeys(index.path+"/*");let trailKeys=indexPathKeys.slice(pathKeys.length);if(trailKeys.length===0){console.assert(pathKeys.length===indexPathKeys.length,"check logic");return[{path:path,oldValue:oldValue,newValue:newValue}]}let results=[];let trailPath="";while(trailKeys.length>0){let subKey=trailKeys.shift();if(subKey==="*"){let allKeys=oldValue===null?[]:Object.keys(oldValue);newValue!==null&&Object.keys(newValue).forEach(key=>{if(allKeys.indexOf(key)<0){allKeys.push(key)}});allKeys.forEach(key=>{let childPath=PathInfo.getChildPath(trailPath,key);let childValues=getChildValues(key,oldValue,newValue);let subTrailPath=PathInfo.getChildPath(path,childPath);let childResults=getAllIndexUpdates(subTrailPath,childValues.oldValue,childValues.newValue);results=results.concat(childResults)});break}else{let values=getChildValues(subKey,oldValue,newValue);oldValue=values.oldValue;newValue=values.newValue;if(oldValue===null&&newValue===null){break}trailPath=PathInfo.getChildPath(trailPath,subKey)}}return results};let results=getAllIndexUpdates(topEventPath,oldValue,newValue);results.forEach(result=>{const p=index.handleRecordUpdate(result.path,result.oldValue,result.newValue);indexUpdates.push(p)})});const callSubscriberWithValues=(sub,oldValue,newValue,variables=[])=>{let trigger=true;let type=sub.type;if(type.startsWith("notify_")){type=type.slice("notify_".length)}if(type==="child_changed"&&(oldValue===null||newValue===null)){trigger=false}else if(type==="value"||type==="child_changed"){let changes=compareValues(oldValue,newValue);trigger=changes!=="identical"}else if(type==="child_added"){trigger=oldValue===null&&newValue!==null}else if(type==="child_removed"){trigger=oldValue!==null&&newValue===null}let dataPath=sub.dataPath;variables.forEach((variable,i)=>{const safeVarName=variable.name==="*"?"\\*":variable.name.replace("$","\\$");dataPath=dataPath.replace(new RegExp(`(^|/)${safeVarName}([/[]|$)`),`$1${variable.value}$2`)});trigger&&this.subscriptions.trigger(sub.type,sub.subscriptionPath,dataPath,oldValue,newValue)};const triggerAllEvents=()=>{eventSubscriptions.map(sub=>{const keys=PathInfo.getPathKeys(sub.dataPath);return{sub:sub,keys:keys}}).sort((a,b)=>{if(a.keys.lengthb.keys.length){return-1}return 0}).forEach(({sub:sub})=>{const process=(currentPath,oldValue,newValue,variables=[])=>{let trailPath=sub.dataPath.slice(currentPath.length).replace(/^\//,"");let trailKeys=PathInfo.getPathKeys(trailPath);while(trailKeys.length>0){let subKey=trailKeys.shift();if(typeof subKey==="string"&&(subKey==="*"||subKey[0]==="$")){let allKeys=oldValue===null?[]:Object.keys(oldValue);newValue!==null&&Object.keys(newValue).forEach(key=>{if(allKeys.indexOf(key)<0){allKeys.push(key)}});allKeys.forEach(key=>{const childValues=getChildValues(key,oldValue,newValue);const vars=variables.concat({name:subKey,value:key});if(trailKeys.length===0){callSubscriberWithValues(sub,childValues.oldValue,childValues.newValue,vars)}else{process(`${currentPath}/${subKey}`,childValues.oldValue,childValues.newValue,vars)}});return}else{currentPath=PathInfo.getChildPath(currentPath,subKey);let childValues=getChildValues(subKey,oldValue,newValue);oldValue=childValues.oldValue;newValue=childValues.newValue}}callSubscriberWithValues(sub,oldValue,newValue,variables)};process(topEventPath,topEventData,newTopEventData)})};if(options.waitForIndexUpdates===false){indexUpdates.splice(0)}return Promise.all(indexUpdates).then(()=>{process.nextTick(triggerAllEvents);return result})})}getChildren(path,options={keyFilter:undefined,tid:undefined}){throw new Error(`This method must be implemented by subclass`)}getNodeValue(path,options={include:undefined,exclude:undefined,child_objects:true,tid:undefined}){return this.getNode(path,options).then(node=>{return node.value})}getNode(path,options={include:undefined,exclude:undefined,child_objects:true,tid:undefined}){throw new Error(`This method must be implemented by subclass`)}getNodeInfo(path,options={tid:undefined}){throw new Error(`This method must be implemented by subclass`)}removeNode(path,options={tid:undefined}){throw new Error(`This method must be implemented by subclass`)}setNode(path,value,options={tid:undefined}){throw new Error(`This method must be implemented by subclass`)}updateNode(path,updates,options={tid:undefined}){throw new Error(`This method must be implemented by subclass`)}transactNode(path,callback,options={no_lock:false}){let checkRevision;const tid=ID.generate();const lockPromise=options&&options.no_lock===true?Promise.resolve({tid:tid,release(){}}):this.nodeLocker.lock(path,tid,true,"transactNode");return lockPromise.then(lock=>{let changed=false,changeCallback=(err,path)=>{changed=true};if(options&&options.no_lock){this.subscriptions.add(path,"notify_value",changeCallback)}return this.getNode(path,{tid:tid}).then(node=>{checkRevision=node.revision;let newValue;try{newValue=callback(node.value)}catch(err){this.debug.error(`Error in transaction callback: ${err.message}`)}if(newValue instanceof Promise){return newValue.catch(err=>{this.debug.error(`Error in transaction callback: ${err.message}`)})}return newValue}).then(newValue=>{if(typeof newValue==="undefined"){return}if(options&&options.no_lock){this.subscriptions.remove(path,"notify_value",changeCallback)}if(changed){return Promise.reject(new NodeRevisionError(`Node changed`))}return this.setNode(path,newValue,{assert_revision:checkRevision,tid:lock.tid})}).then(result=>{lock.release();return result}).catch(err=>{lock.release();if(err instanceof NodeRevisionError){console.warn(`node value changed, running again. Error: ${err.message}`);return this.transactNode(path,callback,options)}else{throw err}})})}matchNode(path,criteria,options={tid:undefined}){const tid=options&&options.tid||ID.generate();const checkNode=(path,criteria)=>{if(criteria.length===0){return Promise.resolve(true)}const criteriaKeys=criteria.reduce((keys,cr)=>{if(keys.indexOf(cr.key)<0){keys.push(cr.key)}return keys},[]);const unseenKeys=criteriaKeys.slice();let isMatch=true;let delayedMatchPromises=[];return this.getChildren(path,{tid:tid,keyFilter:criteriaKeys}).next(childInfo=>{unseenKeys.includes(childInfo.key)&&unseenKeys.splice(unseenKeys.indexOf(childInfo.key),1);const keyCriteria=criteria.filter(cr=>cr.key===childInfo.key).map(cr=>({op:cr.op,compare:cr.compare}));const result=checkChild(childInfo,keyCriteria);isMatch=result.isMatch;delayedMatchPromises.push(...result.promises);if(!isMatch||unseenKeys.length===0){return false}}).then(()=>{if(isMatch){return Promise.all(delayedMatchPromises).then(results=>{isMatch=results.every(res=>res.isMatch)})}}).then(()=>{if(!isMatch){return false}isMatch=unseenKeys.every(key=>{const child=new NodeInfo({key:key,exists:false});const keyCriteria=criteria.filter(cr=>cr.key===key).map(cr=>({op:cr.op,compare:cr.compare}));const result=checkChild(child,keyCriteria);return result.isMatch});return isMatch}).catch(err=>{this.debug.error(`Error matching on "${path}": `,err);throw err})};const checkChild=(child,criteria)=>{const promises=[];const isMatch=criteria.every(f=>{let proceed=true;if(f.op==="!exists"||f.op==="=="&&(typeof f.compare==="undefined"||f.compare===null)){proceed=!child.exists}else if(f.op==="exists"||f.op==="!="&&(typeof f.compare==="undefined"||f.compare===null)){proceed=child.exists}else if(!child.exists){proceed=false}else{if(child.address){if(child.valueType===VALUE_TYPES.OBJECT&&["has","!has"].indexOf(f.op)>=0){const op=f.op==="has"?"exists":"!exists";const p=checkNode(child.path,[{key:f.compare,op:op}]).then(isMatch=>{return{key:child.key,isMatch:isMatch}});promises.push(p);proceed=true}else if(child.valueType===VALUE_TYPES.ARRAY&&["contains","!contains"].indexOf(f.op)>=0){const p=this.getNodeValue(child.path,{tid:tid}).then(arr=>{const isMatch=f.op==="contains"?f.compare instanceof Array?f.compare.every(val=>arr.includes(val)):arr.includes(f.compare):f.compare instanceof Array?!f.compare.some(val=>arr.includes(val)):!arr.includes(f.compare);return{key:child.key,isMatch:isMatch}});promises.push(p);proceed=true}else if(child.valueType===VALUE_TYPES.STRING){const p=this.getNodeValue(child.path,{tid:tid}).then(val=>{return{key:child.key,isMatch:this.test(val,f.op,f.compare)}});promises.push(p);proceed=true}else{proceed=false}}else if(child.type===VALUE_TYPES.OBJECT&&["has","!has"].indexOf(f.op)>=0){const has=f.compare in child.value;proceed=has&&f.op==="has"||!has&&f.op==="!has"}else if(child.type===VALUE_TYPES.ARRAY&&["contains","!contains"].indexOf(f.op)>=0){const contains=child.value.indexOf(f.compare)>=0;proceed=contains&&f.op==="contains"||!contains&&f.op==="!contains"}else{const ret=this.test(child.value,f.op,f.compare);if(ret instanceof Promise){promises.push(ret);ret=true}proceed=ret}}return proceed});return{isMatch:isMatch,promises:promises}};return checkNode(path,criteria)}test(val,op,compare){if(op==="<"){return val"){return val>compare}if(op===">="){return val>=compare}if(op==="in"){return compare.indexOf(val)>=0}if(op==="!in"){return compare.indexOf(val)<0}if(op==="like"||op==="!like"){const pattern="^"+compare.replace(/[-[\]{}()+.,\\^$|#\s]/g,"\\$&").replace(/\?/g,".").replace(/\*/g,".*?")+"$";const re=new RegExp(pattern,"i");const isMatch=re.test(val.toString());return op==="like"?isMatch:!isMatch}if(op==="matches"){return compare.test(val.toString())}if(op==="!matches"){return!compare.test(val.toString())}if(op==="between"){return val>=compare[0]&&val<=compare[1]}if(op==="!between"){return valcompare[1]}if(op==="has"||op==="!has"){const has=typeof val==="object"&&compare in val;return op==="has"?has:!has}if(op==="contains"||op==="!contains"){const includes=typeof val==="object"&&val instanceof Array&&val.includes(compare);return op==="contains"?includes:!includes}return false}exportNode(path,stream,options={format:"json"}){if(options&&options.format!=="json"){throw new Error(`Only json output is currently supported`)}const stringifyValue=(type,val)=>{const escape=str=>str.replace(/\\/i,"\\\\").replace(/"/g,'\\"');if(type===VALUE_TYPES.DATETIME){val=`"${val.toISOString()}"`}else if(type===VALUE_TYPES.STRING){val=`"${escape(val)}"`}else if(type===VALUE_TYPES.ARRAY){val=`[]`}else if(type===VALUE_TYPES.OBJECT){val=`{}`}else if(type===VALUE_TYPES.BINARY){val=`"${escape(ascii85.encode(val))}"`}else if(type===VALUE_TYPES.REFERENCE){val=`"${val.path}"`}return val};const queue=[];let outputCount=0;let objStart="",objEnd="";const buffer={output:"",enable:false,promise:null};return this.getNodeInfo(path).then(nodeInfo=>{if(!nodeInfo.exists){stream.write("null")}else if(nodeInfo.type===VALUE_TYPES.OBJECT){objStart="{";objEnd="}"}else if(nodeInfo.type===VALUE_TYPES.ARRAY){objStart="{";objEnd="}"}else{return this.getNodeValue(path).then(value=>{const val=stringifyValue(nodeInfo.type,value);return stream.write(val)})}let p=Promise.resolve();if(objStart){p=stream.write(objStart);if(!(p instanceof Promise)){p=Promise.resolve()}}return p.then(()=>{return this.getChildren(path).next(childInfo=>{if(childInfo.address){queue.push(childInfo)}else{const val=stringifyValue(childInfo.type,childInfo.value);const comma=outputCount>0?",":"";const key=typeof childInfo.index==="number"?`"${childInfo.index}"`:`"${childInfo.key}"`;const output=`${comma}${key}:${val}`;outputCount++;if(buffer.enable){buffer.output+=output}else{const flush=output=>{const p=stream.write(output);if(p instanceof Promise){buffer.enable=true;buffer.promise=p.then(()=>{const buffered=buffer.output;buffer.enable=false;buffer.output="";buffer.promise=null;if(buffered.length>0){return flush(buffered)}});return buffer.promise}};flush(output)}}})})}).then(()=>{return buffer.promise}).then(()=>{const next=()=>{if(queue.length===0){return}const childInfo=queue.shift();const comma=outputCount>0?",":"";const key=typeof childInfo.index==="number"?`"${childInfo.index}"`:`"${childInfo.key}"`;let p=stream.write(`${comma}${key}:`);outputCount++;if(!(p instanceof Promise)){p=Promise.resolve(p)}return p.then(()=>{return this.exportNode(childInfo.address.path,stream)}).then(()=>{return next()})};return next()}).then(()=>{if(objEnd){return stream.write(objEnd)}})}}module.exports={Storage:Storage,StorageSettings:StorageSettings,NodeNotFoundError:NodeNotFoundError,NodeRevisionError:NodeRevisionError}}).call(this,require("_process"))},{"./data-index":41,"./node-info":34,"./node-lock":35,"./node-value-types":36,"./promise-fs":41,_process:45,"acebase-core":11,colors:22,events:42}],41:[function(require,module,exports){},{}],42:[function(require,module,exports){var objectCreate=Object.create||objectCreatePolyfill;var objectKeys=Object.keys||objectKeysPolyfill;var bind=Function.prototype.bind||functionBindPolyfill;function EventEmitter(){if(!this._events||!Object.prototype.hasOwnProperty.call(this,"_events")){this._events=objectCreate(null);this._eventsCount=0}this._maxListeners=this._maxListeners||undefined}module.exports=EventEmitter;EventEmitter.EventEmitter=EventEmitter;EventEmitter.prototype._events=undefined;EventEmitter.prototype._maxListeners=undefined;var defaultMaxListeners=10;var hasDefineProperty;try{var o={};if(Object.defineProperty)Object.defineProperty(o,"x",{value:0});hasDefineProperty=o.x===0}catch(err){hasDefineProperty=false}if(hasDefineProperty){Object.defineProperty(EventEmitter,"defaultMaxListeners",{enumerable:true,get:function(){return defaultMaxListeners},set:function(arg){if(typeof arg!=="number"||arg<0||arg!==arg)throw new TypeError('"defaultMaxListeners" must be a positive number');defaultMaxListeners=arg}})}else{EventEmitter.defaultMaxListeners=defaultMaxListeners}EventEmitter.prototype.setMaxListeners=function setMaxListeners(n){if(typeof n!=="number"||n<0||isNaN(n))throw new TypeError('"n" argument must be a positive number');this._maxListeners=n;return this};function $getMaxListeners(that){if(that._maxListeners===undefined)return EventEmitter.defaultMaxListeners;return that._maxListeners}EventEmitter.prototype.getMaxListeners=function getMaxListeners(){return $getMaxListeners(this)};function emitNone(handler,isFn,self){if(isFn)handler.call(self);else{var len=handler.length;var listeners=arrayClone(handler,len);for(var i=0;i1)er=arguments[1];if(er instanceof Error){throw er}else{var err=new Error('Unhandled "error" event. ('+er+")");err.context=er;throw err}return false}handler=events[type];if(!handler)return false;var isFn=typeof handler==="function";len=arguments.length;switch(len){case 1:emitNone(handler,isFn,this);break;case 2:emitOne(handler,isFn,this,arguments[1]);break;case 3:emitTwo(handler,isFn,this,arguments[1],arguments[2]);break;case 4:emitThree(handler,isFn,this,arguments[1],arguments[2],arguments[3]);break;default:args=new Array(len-1);for(i=1;i0&&existing.length>m){existing.warned=true;var w=new Error("Possible EventEmitter memory leak detected. "+existing.length+' "'+String(type)+'" listeners '+"added. Use emitter.setMaxListeners() to "+"increase limit.");w.name="MaxListenersExceededWarning";w.emitter=target;w.type=type;w.count=existing.length;if(typeof console==="object"&&console.warn){console.warn("%s: %s",w.name,w.message)}}}}return target}EventEmitter.prototype.addListener=function addListener(type,listener){return _addListener(this,type,listener,false)};EventEmitter.prototype.on=EventEmitter.prototype.addListener;EventEmitter.prototype.prependListener=function prependListener(type,listener){return _addListener(this,type,listener,true)};function onceWrapper(){if(!this.fired){this.target.removeListener(this.type,this.wrapFn);this.fired=true;switch(arguments.length){case 0:return this.listener.call(this.target);case 1:return this.listener.call(this.target,arguments[0]);case 2:return this.listener.call(this.target,arguments[0],arguments[1]);case 3:return this.listener.call(this.target,arguments[0],arguments[1],arguments[2]);default:var args=new Array(arguments.length);for(var i=0;i=0;i--){if(list[i]===listener||list[i].listener===listener){originalListener=list[i].listener;position=i;break}}if(position<0)return this;if(position===0)list.shift();else spliceOne(list,position);if(list.length===1)events[type]=list[0];if(events.removeListener)this.emit("removeListener",type,originalListener||listener)}return this};EventEmitter.prototype.removeAllListeners=function removeAllListeners(type){var listeners,events,i;events=this._events;if(!events)return this;if(!events.removeListener){if(arguments.length===0){this._events=objectCreate(null);this._eventsCount=0}else if(events[type]){if(--this._eventsCount===0)this._events=objectCreate(null);else delete events[type]}return this}if(arguments.length===0){var keys=objectKeys(events);var key;for(i=0;i=0;i--){this.removeListener(type,listeners[i])}}return this};function _listeners(target,type,unwrap){var events=target._events;if(!events)return[];var evlistener=events[type];if(!evlistener)return[];if(typeof evlistener==="function")return unwrap?[evlistener.listener||evlistener]:[evlistener];return unwrap?unwrapListeners(evlistener):arrayClone(evlistener,evlistener.length)}EventEmitter.prototype.listeners=function listeners(type){return _listeners(this,type,true)};EventEmitter.prototype.rawListeners=function rawListeners(type){return _listeners(this,type,false)};EventEmitter.listenerCount=function(emitter,type){if(typeof emitter.listenerCount==="function"){return emitter.listenerCount(type)}else{return listenerCount.call(emitter,type)}};EventEmitter.prototype.listenerCount=listenerCount;function listenerCount(type){var events=this._events;if(events){var evlistener=events[type];if(typeof evlistener==="function"){return 1}else if(evlistener){return evlistener.length}}return 0}EventEmitter.prototype.eventNames=function eventNames(){return this._eventsCount>0?Reflect.ownKeys(this._events):[]};function spliceOne(list,index){for(var i=index,k=i+1,n=list.length;k1){for(var i=1;i=len)return x;switch(x){case"%s":return String(args[i++]);case"%d":return Number(args[i++]);case"%j":try{return JSON.stringify(args[i++])}catch(_){return"[Circular]"}default:return x}}));for(var x=args[i];i=3)ctx.depth=arguments[2];if(arguments.length>=4)ctx.colors=arguments[3];if(isBoolean(opts)){ctx.showHidden=opts}else if(opts){exports._extend(ctx,opts)}if(isUndefined(ctx.showHidden))ctx.showHidden=false;if(isUndefined(ctx.depth))ctx.depth=2;if(isUndefined(ctx.colors))ctx.colors=false;if(isUndefined(ctx.customInspect))ctx.customInspect=true;if(ctx.colors)ctx.stylize=stylizeWithColor;return formatValue(ctx,obj,ctx.depth)}exports.inspect=inspect;inspect.colors={bold:[1,22],italic:[3,23],underline:[4,24],inverse:[7,27],white:[37,39],grey:[90,39],black:[30,39],blue:[34,39],cyan:[36,39],green:[32,39],magenta:[35,39],red:[31,39],yellow:[33,39]};inspect.styles={special:"cyan",number:"yellow",boolean:"yellow",undefined:"grey",null:"bold",string:"green",date:"magenta",regexp:"red"};function stylizeWithColor(str,styleType){var style=inspect.styles[styleType];if(style){return"["+inspect.colors[style][0]+"m"+str+"["+inspect.colors[style][1]+"m"}else{return str}}function stylizeNoColor(str,styleType){return str}function arrayToHash(array){var hash={};array.forEach((function(val,idx){hash[val]=true}));return hash}function formatValue(ctx,value,recurseTimes){if(ctx.customInspect&&value&&isFunction(value.inspect)&&value.inspect!==exports.inspect&&!(value.constructor&&value.constructor.prototype===value)){var ret=value.inspect(recurseTimes,ctx);if(!isString(ret)){ret=formatValue(ctx,ret,recurseTimes)}return ret}var primitive=formatPrimitive(ctx,value);if(primitive){return primitive}var keys=Object.keys(value);var visibleKeys=arrayToHash(keys);if(ctx.showHidden){keys=Object.getOwnPropertyNames(value)}if(isError(value)&&(keys.indexOf("message")>=0||keys.indexOf("description")>=0)){return formatError(value)}if(keys.length===0){if(isFunction(value)){var name=value.name?": "+value.name:"";return ctx.stylize("[Function"+name+"]","special")}if(isRegExp(value)){return ctx.stylize(RegExp.prototype.toString.call(value),"regexp")}if(isDate(value)){return ctx.stylize(Date.prototype.toString.call(value),"date")}if(isError(value)){return formatError(value)}}var base="",array=false,braces=["{","}"];if(isArray(value)){array=true;braces=["[","]"]}if(isFunction(value)){var n=value.name?": "+value.name:"";base=" [Function"+n+"]"}if(isRegExp(value)){base=" "+RegExp.prototype.toString.call(value)}if(isDate(value)){base=" "+Date.prototype.toUTCString.call(value)}if(isError(value)){base=" "+formatError(value)}if(keys.length===0&&(!array||value.length==0)){return braces[0]+base+braces[1]}if(recurseTimes<0){if(isRegExp(value)){return ctx.stylize(RegExp.prototype.toString.call(value),"regexp")}else{return ctx.stylize("[Object]","special")}}ctx.seen.push(value);var output;if(array){output=formatArray(ctx,value,recurseTimes,visibleKeys,keys)}else{output=keys.map((function(key){return formatProperty(ctx,value,recurseTimes,visibleKeys,key,array)}))}ctx.seen.pop();return reduceToSingleString(output,base,braces)}function formatPrimitive(ctx,value){if(isUndefined(value))return ctx.stylize("undefined","undefined");if(isString(value)){var simple="'"+JSON.stringify(value).replace(/^"|"$/g,"").replace(/'/g,"\\'").replace(/\\"/g,'"')+"'";return ctx.stylize(simple,"string")}if(isNumber(value))return ctx.stylize(""+value,"number");if(isBoolean(value))return ctx.stylize(""+value,"boolean");if(isNull(value))return ctx.stylize("null","null")}function formatError(value){return"["+Error.prototype.toString.call(value)+"]"}function formatArray(ctx,value,recurseTimes,visibleKeys,keys){var output=[];for(var i=0,l=value.length;i-1){if(array){str=str.split("\n").map((function(line){return" "+line})).join("\n").substr(2)}else{str="\n"+str.split("\n").map((function(line){return" "+line})).join("\n")}}}else{str=ctx.stylize("[Circular]","special")}}if(isUndefined(name)){if(array&&key.match(/^\d+$/)){return str}name=JSON.stringify(""+key);if(name.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)){name=name.substr(1,name.length-2);name=ctx.stylize(name,"name")}else{name=name.replace(/'/g,"\\'").replace(/\\"/g,'"').replace(/(^"|"$)/g,"'");name=ctx.stylize(name,"string")}}return name+": "+str}function reduceToSingleString(output,base,braces){var numLinesEst=0;var length=output.reduce((function(prev,cur){numLinesEst++;if(cur.indexOf("\n")>=0)numLinesEst++;return prev+cur.replace(/\u001b\[\d\d?m/g,"").length+1}),0);if(length>60){return braces[0]+(base===""?"":base+"\n ")+" "+output.join(",\n ")+" "+braces[1]}return braces[0]+base+" "+output.join(", ")+" "+braces[1]}function isArray(ar){return Array.isArray(ar)}exports.isArray=isArray;function isBoolean(arg){return typeof arg==="boolean"}exports.isBoolean=isBoolean;function isNull(arg){return arg===null}exports.isNull=isNull;function isNullOrUndefined(arg){return arg==null}exports.isNullOrUndefined=isNullOrUndefined;function isNumber(arg){return typeof arg==="number"}exports.isNumber=isNumber;function isString(arg){return typeof arg==="string"}exports.isString=isString;function isSymbol(arg){return typeof arg==="symbol"}exports.isSymbol=isSymbol;function isUndefined(arg){return arg===void 0}exports.isUndefined=isUndefined;function isRegExp(re){return isObject(re)&&objectToString(re)==="[object RegExp]"}exports.isRegExp=isRegExp;function isObject(arg){return typeof arg==="object"&&arg!==null}exports.isObject=isObject;function isDate(d){return isObject(d)&&objectToString(d)==="[object Date]"}exports.isDate=isDate;function isError(e){return isObject(e)&&(objectToString(e)==="[object Error]"||e instanceof Error)}exports.isError=isError;function isFunction(arg){return typeof arg==="function"}exports.isFunction=isFunction;function isPrimitive(arg){return arg===null||typeof arg==="boolean"||typeof arg==="number"||typeof arg==="string"||typeof arg==="symbol"||typeof arg==="undefined"}exports.isPrimitive=isPrimitive;exports.isBuffer=require("./support/isBuffer");function objectToString(o){return Object.prototype.toString.call(o)}function pad(n){return n<10?"0"+n.toString(10):n.toString(10)}var months=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];function timestamp(){var d=new Date;var time=[pad(d.getHours()),pad(d.getMinutes()),pad(d.getSeconds())].join(":");return[d.getDate(),months[d.getMonth()],time].join(" ")}exports.log=function(){console.log("%s - %s",timestamp(),exports.format.apply(exports,arguments))};exports.inherits=require("inherits");exports._extend=function(origin,add){if(!add||!isObject(add))return origin;var keys=Object.keys(add);var i=keys.length;while(i--){origin[keys[i]]=add[keys[i]]}return origin};function hasOwnProperty(obj,prop){return Object.prototype.hasOwnProperty.call(obj,prop)}}).call(this,require("_process"),typeof global!=="undefined"?global:typeof self!=="undefined"?self:typeof window!=="undefined"?window:{})},{"./support/isBuffer":46,_process:45,inherits:43}]},{},[33])(33)})); \ No newline at end of file diff --git a/index.d.ts b/index.d.ts index 96c54ce..dba5d73 100644 --- a/index.d.ts +++ b/index.d.ts @@ -100,6 +100,56 @@ export interface ICustomStorageNodeValue { } export interface ICustomStorageNode extends ICustomStorageNodeMetaData, ICustomStorageNodeValue {} +export abstract class CustomStorageTransaction { + /** + * @param target Which path the transaction is taking place on, and whether it is a read or read/write lock. If your storage backend does not support transactions, is synchronous, or if you are able to lock resources based on path: use storage.nodeLocker to ensure threadsafe transactions + */ + constructor(target: { path: string, write: boolean }); + + readonly target: { path: string, readonly originalPath: string, readonly write: boolean }; + + /** Function that gets the node with given path from your custom data store, must return null if it doesn't exist */ + abstract get(path: string): Promise; + /** Function that inserts or updates a node with given path in your custom data store */ + abstract set(path: string, value: ICustomStorageNode): Promise; + /** Function that removes the node with given path from your custom data store */ + abstract remove(path: string): Promise; + + /** + * Function that streams all stored nodes that are direct children of the given path. For path "parent/path", results must include paths such as "parent/path/key" AND "parent/path[0]". 👉🏻 You can use CustomStorageHelpers for logic. Keep calling the add callback for each node until it returns false. + * @param path Parent path to load children of + * @param include + * @param include.metadata Whether metadata needs to be loaded + * @param include.value Whether value needs to be loaded + * @param checkCallback callback method to precheck if child needs to be added, perform before loading metadata/value if possible + * @param addCallback callback method that adds the child node. Returns whether or not to keep calling with more children + * @returns Returns a promise that resolves when there are no more children to be streamed + */ + abstract childrenOf(path: string, include: { value: boolean, metadata: boolean }, checkCallback: (childPath: string) => boolean, addCallback: (childPath: string, node?: ICustomStorageNodeMetaData|ICustomStorageNode) => boolean): Promise; + + /** + * Function that streams all stored nodes that are descendants of the given path. For path "parent/path", results must include paths such as "parent/path/key", "parent/path/key/subkey", "parent/path[0]", "parent/path[12]/key" etc. 👉🏻 You can use CustomStorageHelpers for logic. Keep calling the add callback for each node until it returns false. + * @param path Parent path to load descendants of + * @param include + * @param include.metadata Whether metadata needs to be loaded + * @param include.value Whether value needs to be loaded + * @param checkCallback callback method to precheck if descendant needs to be added, perform before loading metadata/value if possible + * @param addCallback callback method that adds the descendant node. Returns whether or not to keep calling with more children + * @returns Returns a promise that resolves when there are no more descendants to be streamed + */ + abstract descendantsOf(path: string, include: { value: boolean, metadata: boolean }, checkCallback: (childPath: string) => boolean, addCallback: (descPath: string, node?: ICustomStorageNodeMetaData|ICustomStorageNode) => boolean): Promise; + + /** (optional, not used yet) Function that gets multiple nodes (metadata AND value) from your custom data store at once. Must return a Promise that resolves with Map */ + getMultiple?(paths: string[]): Promise>; + /** (optional, not used yet) Function that sets multiple nodes at once */ + setMultiple?(nodes: Array<{ path: string, node: ICustomStorageNode }>): Promise; + /** (optional) Function that removes multiple nodes from your custom data store at once */ + removeMultiple?(paths: string[]): Promise; + + abstract commit(): Promise; + abstract rollback(reason: Error): Promise; +} + /** * Allows data to be stored in a custom storage backend of your choice! Simply provide a couple of functions * to get, set and remove data and you're done. @@ -108,22 +158,15 @@ export class CustomStorageSettings extends StorageSettings { constructor(settings: CustomStorageSettings); /** Name of the custom storage adapter */ name?: string; + /** Whether default node locking should be used (default). Set to false if your storage backend disallows multiple simultanious write transactions (eg IndexedDB). Set to true if your storage backend does not support transactions (eg LocalStorage) or allows multiple simultanious write transactions (eg AceBase binary). */ + locking?: boolean; /** Function that returns a Promise that resolves once your data store backend is ready for use */ ready(): Promise; - /** Function that gets the node with given path from your custom data store, must return null if it doesn't exist */ - get(path: string): Promise; - /** Function that inserts or updates a node with given path in your custom data store */ - set(path: string, value: ICustomStorageNode): Promise; - /** Function that removes the node with given path from your custom data store */ - remove(path: string): Promise; - /** Function that streams all stored nodes that are direct children of the given path. For path "parent/path", results must include paths such as "parent/path/key" AND "parent/path[0]". 👉🏻 You can use CustomStorageHelpers for logic. Keep calling the add callback for each node until it returns false. */ - childrenOf(path: string, include: { value: boolean, metadata: boolean }, checkCallback: (childPath: string) => boolean, addCallback: (childPath: string, node?: ICustomStorageNodeMetaData|ICustomStorageNode) => boolean): Promise; - /** Function that streams all stored nodes that are descendants of the given path. For path "parent/path", results must include paths such as "parent/path/key", "parent/path/key/subkey", "parent/path[0]", "parent/path[12]/key" etc. 👉🏻 You can use CustomStorageHelpers for logic. Keep calling the add callback for each node until it returns false. */ - descendantsOf(path: string, include: { value: boolean, metadata: boolean }, checkCallback: (childPath: string) => boolean, addCallback: (descPath: string, node?: ICustomStorageNodeMetaData|ICustomStorageNode) => boolean): Promise; - // /** (optional, not used yet) Function that gets multiple nodes (metadata AND value) from your custom data store at once. Must return a Promise that resolves with Map */ - // getMultiple?(paths: string[]): Promise>; - /** (optional) Function that removes multiple nodes from your custom data store at once */ - removeMultiple?(paths: string[]): Promise; + /** + * Function that starts a transaction for read/write operations on a specific path and/or child paths + * @param target target path and mode to start transaction on + */ + getTransaction(target: { path: string, write: boolean }): Promise } export class CustomStorageHelpers { diff --git a/package.json b/package.json index c5cef8c..83ed5c0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "acebase", - "version": "0.9.25", + "version": "0.9.26", "description": "AceBase realtime database engine", "main": "./src/index.js", "browser": "./dist/browser.js", @@ -49,7 +49,7 @@ "author": "Ewout Stortenbeker (http://appy.one)", "license": "MIT", "dependencies": { - "acebase-core": "~0.9.8", + "acebase-core": "~0.9.9", "colors": "^1.3.2", "unidecode": "^0.1.8" } diff --git a/src/acebase-browser.js b/src/acebase-browser.js index bd2af94..027a2c5 100644 --- a/src/acebase-browser.js +++ b/src/acebase-browser.js @@ -1,5 +1,5 @@ const { AceBase, AceBaseLocalSettings } = require('./acebase-local'); -const { CustomStorageSettings, CustomStorageHelpers, ICustomStorageNode, ICustomStorageNodeMetaData } = require('./storage-custom'); +const { CustomStorageSettings, CustomStorageTransaction, CustomStorageHelpers, ICustomStorageNode, ICustomStorageNodeMetaData } = require('./storage-custom'); /** * @typedef {Object} IIndexedDBNodeData @@ -71,211 +71,27 @@ class BrowserAceBase extends AceBase { request.onsuccess = e => { db = request.result; readyResolve(); - } + }; request.onerror = e => { readyReject(e); }; const storageSettings = new CustomStorageSettings({ name: 'IndexedDB', + locking: false, ready() { return readyPromise; }, - get(path) { - // Get metadata from "nodes" object store - const tx = db.transaction(['nodes', 'content'], 'readonly'); - const request = tx.objectStore('nodes').get(path); - return new Promise((resolve, reject) => { - request.onsuccess = event => { - /** @type {IIndexedDBNodeData} */ - const data = request.result; - if (!data) { - return resolve(null); - } - /** @type {ICustomStorageNode} */ - const node = data.metadata; - - // Node exists, get content from "content" object store - const contentReq = tx.objectStore('content').get(path); - contentReq.onsuccess = e => { - node.value = contentReq.result; - resolve(node); - }; - contentReq.onerror = e => reject(e); - } - request.onerror = e => { - console.error(`IndexedDB get error`, e); - reject(e); - } - }); - }, - set(path, node) { - /** @type {ICustomStorageNode} */ - const copy = {}; - const value = node.value; - Object.assign(copy, node); - delete copy.value; - /** @type {ICustomStorageNodeMetaData} */ - const metadata = copy; - /** @type {IIndexedDBNodeData} */ - const obj = { - path, - metadata + async getTransaction(target) { + const context = { + debug: true, + db } - return new Promise((resolve, reject) => { - const tx = db.transaction(['nodes', 'content'], 'readwrite'); - // Insert into "nodes" object store first - const request = tx.objectStore('nodes').put(obj); - request.onerror = e => reject(e); - request.onsuccess = e => { - // Now add to "content" object store - const contentReq = tx.objectStore('content').put(value, path); - contentReq.onsuccess = e => resolve(); - contentReq.onerror = e => { - tx.abort(); // rollback transaction - reject(e); - } - }; - }); - }, - remove(path) { - const tx = db.transaction(['nodes','content'], 'readwrite'); - return new Promise((resolve, reject) => { - // First, remove from "content" object store - const r1 = tx.objectStore('content').delete(path); - r1.onerror = e => reject(e); - r1.onsuccess = e => { - // Now, remove from "nodes" data store - const r2 = tx.objectStore('nodes').delete(path); - r2.onerror = e => { - tx.abort(); // rollback transaction - reject(e); - }; - r2.onsuccess = e => resolve(); - } - }); - }, - childrenOf(path, include, checkCallback, addCallback) { - // Use cursor to loop from path on - const pathInfo = CustomStorageHelpers.PathInfo.get(path); - const lockStores = include.value ? ['nodes','content'] : 'nodes'; - const tx = db.transaction(lockStores, 'readonly'); - const store = tx.objectStore('nodes'); - const query = IDBKeyRange.lowerBound(path, true); - return new Promise((resolve, reject) => { - /** @type {IDBRequest|IDBRequest} */ - const cursor = include.metadata ? store.openCursor(query) : store.openKeyCursor(query); - cursor.onerror = e => reject(e); - cursor.onsuccess = async e => { - /** type {string} */ - const otherPath = cursor.result ? cursor.result.key : null; - let keepGoing = true; - if (otherPath === null) { - // No more results - keepGoing = false; - } - else if (!pathInfo.isAncestorOf(otherPath)) { - // Paths are sorted, no more children to be expected! - keepGoing = false; - } - else if (pathInfo.isParentOf(otherPath) && checkCallback(otherPath)) { - /** @type {ICustomStorageNode|ICustomStorageNodeMetaData} */ - let node; - if (include.metadata) { - /** @type {IDBRequest} */ - const valueCursor = cursor; - /** @type {IIndexedDBNodeData} */ - const data = valueCursor.result.value; - node = data.metadata; - if (include.value) { - // Load value! - const req = tx.objectStore('content').get(otherPath); - await new Promise((resolve, reject) => { - req.onerror = e => reject(e); - req.onsuccess = e => { - node.value = req.result; - resolve(); - } - }); - } - } - keepGoing = addCallback(otherPath, node); - } - if (keepGoing) { - try { cursor.result.continue(); } - catch(err) { - // We reached the end of the cursor? - keepGoing = false; - } - } - if (!keepGoing) { - resolve(); - } - }; - }); - }, - descendantsOf(path, include, checkCallback, addCallback) { - // Use cursor to loop from path on - // NOTE: Implementation is almost identical to childrenOf, consider merging them - const pathInfo = CustomStorageHelpers.PathInfo.get(path); - const lockStores = include.value ? ['nodes','content'] : 'nodes'; - const tx = db.transaction(lockStores, 'readonly'); - const store = tx.objectStore('nodes'); - const query = IDBKeyRange.lowerBound(path, true); - return new Promise((resolve, reject) => { - /** @type {IDBRequest|IDBRequest} */ - const cursor = include.metadata ? store.openCursor(query) : store.openKeyCursor(query); - cursor.onerror = e => reject(e); - cursor.onsuccess = async e => { - /** @type {string} */ - const otherPath = cursor.result ? cursor.result.key : null; - let keepGoing = true; - if (otherPath === null) { - // No more results - keepGoing = false; - } - else if (!pathInfo.isAncestorOf(otherPath)) { - // Paths are sorted, no more ancestors to be expected! - keepGoing = false; - } - else if (checkCallback(otherPath)) { - /** @type {ICustomStorageNode|ICustomStorageNodeMetaData} */ - let node; - if (include.metadata) { - /** @type {IDBRequest} */ - const valueCursor = cursor; - /** @type {IIndexedDBNodeData} */ - const data = valueCursor.result.value; - node = data.metadata; - if (include.value) { - // Load value! - const req = tx.objectStore('content').get(otherPath); - await new Promise((resolve, reject) => { - req.onerror = e => reject(e); - req.onsuccess = e => { - node.value = req.result; - resolve(); - } - }); - } - } - keepGoing = addCallback(otherPath, node); - } - if (keepGoing) { - try { cursor.result.continue(); } - catch(err) { - // We reached the end of the cursor? - keepGoing = false; - } - } - if (!keepGoing) { - resolve(); - } - }; - }); - } + const transaction = new IndexedDBStorageTransaction(context, target); + await transaction.start(); + return transaction; + } }); - return new AceBase(dbname, { logLevel: settings.logLevel, storage: storageSettings }); } @@ -295,92 +111,367 @@ class BrowserAceBase extends AceBase { // Determine whether to use localStorage or sessionStorage const localStorage = settings.provider ? settings.provider : settings.temp ? window.localStorage : window.sessionStorage; - // Helper functions to prefix all localStorage keys with dbname - // to allows multiple db's in localStorage - const storageKeysPrefix = `${dbname}.acebase::`; // Prefix all localStorage keys with dbname - function getPathFromStorageKey(key) { - return key.slice(storageKeysPrefix.length); - } - function getStorageKeyForPath(path) { - return `${storageKeysPrefix}${path}`; - } - // Setup our CustomStorageSettings const storageSettings = new CustomStorageSettings({ name: 'LocalStorage', + locking: true, ready() { + // LocalStorage is always ready return Promise.resolve(); }, - get(path) { - // Gets value from localStorage, wrapped in Promise - return new Promise(resolve => { - const json = localStorage.getItem(getStorageKeyForPath(path)); - const val = JSON.parse(json); - resolve(val); - }); - }, - set(path, val) { - // Sets value in localStorage, wrapped in Promise - return new Promise(resolve => { - const json = JSON.stringify(val); - localStorage.setItem(getStorageKeyForPath(path), json); - resolve(); - }); - }, - remove(path) { - // Removes a value from localStorage, wrapped in Promise - return new Promise(resolve => { - localStorage.removeItem(getStorageKeyForPath(path)); - resolve(); - }); - }, - childrenOf(path, include, checkCallback, addCallback) { - // Gets all child paths - // Cannot query localStorage, so loop through all stored keys to find children - return new Promise(resolve => { - const pathInfo = CustomStorageHelpers.PathInfo.get(path); - for (let i = 0; i < localStorage.length; i++) { - const key = localStorage.key(i); - if (!key.startsWith(storageKeysPrefix)) { continue; } - let otherPath = getPathFromStorageKey(key); - if (pathInfo.isParentOf(otherPath) && checkCallback(otherPath)) { - let node; - if (include.metadata || include.value) { - const json = localStorage.getItem(key); - node = JSON.parse(json); - } - const keepGoing = addCallback(otherPath, node); - if (!keepGoing) { break; } - } - } - resolve(); - }); - }, - descendantsOf(path, include, checkCallback, addCallback) { - // Gets all descendant paths - // Cannot query localStorage, so loop through all stored keys to find descendants - return new Promise(resolve => { - const pathInfo = CustomStorageHelpers.PathInfo.get(path); - for (let i = 0; i < localStorage.length; i++) { - const key = localStorage.key(i); - if (!key.startsWith(storageKeysPrefix)) { continue; } - let otherPath = getPathFromStorageKey(key); - if (pathInfo.isAncestorOf(otherPath) && checkCallback(otherPath)) { - let node; - if (include.metadata || include.value) { - const json = localStorage.getItem(key); - node = JSON.parse(json); - } - const keepGoing = addCallback(otherPath, node); - if (!keepGoing) { break; } - } - } - resolve(); - }); + getTransaction(target) { + // Create an instance of our transaction class + const context = { + debug: true, + dbname, + localStorage + } + const transaction = new LocalStorageTransaction(context, target); + return Promise.resolve(transaction); } }); return new AceBase(dbname, { logLevel: settings.logLevel, storage: storageSettings }); } } +class IndexedDBStorageTransaction extends CustomStorageTransaction { + + /** + * @param {{debug: boolean, db: typeof IndexedDB }} context + * @param {{path: string, write: boolean}} target + * @param {NodeLocker} nodeLocker + */ + constructor(context, target) { + super(target); + this.context = context; + } + + start() { + // Start transaction + return new Promise((resolve, reject) => { + this._tx = this.context.db.transaction(['nodes', 'content'], this.target.write ? 'readwrite' : 'readonly'); + resolve(); + }) + } + + commit() { + const tx = this._tx; + tx.commit && tx.commit(); + } + + rollback(err) { + const tx = this._tx; + tx.abort && tx.abort(); + } + + get(path) { + // Get metadata from "nodes" object store + const tx = this._tx; + const request = tx.objectStore('nodes').get(path); + return new Promise((resolve, reject) => { + request.onsuccess = event => { + /** @type {IIndexedDBNodeData} */ + const data = request.result; + if (!data) { + return resolve(null); + } + /** @type {ICustomStorageNode} */ + const node = data.metadata; + + // Node exists, get content from "content" object store + const contentReq = tx.objectStore('content').get(path); + contentReq.onsuccess = e => { + node.value = contentReq.result; + resolve(node); + }; + contentReq.onerror = e => reject(e); + } + request.onerror = e => { + console.error(`IndexedDB get error`, e); + reject(e); + } + }); + } + + set(path, node) { + /** @type {ICustomStorageNode} */ + const copy = {}; + const value = node.value; + Object.assign(copy, node); + delete copy.value; + /** @type {ICustomStorageNodeMetaData} */ + const metadata = copy; + /** @type {IIndexedDBNodeData} */ + const obj = { + path, + metadata + } + return new Promise((resolve, reject) => { + const tx = this._tx; + // Insert into "nodes" object store first + const request = tx.objectStore('nodes').put(obj); + request.onerror = e => reject(e); + request.onsuccess = e => { + // Now add to "content" object store + const contentReq = tx.objectStore('content').put(value, path); + contentReq.onsuccess = e => resolve(); + contentReq.onerror = e => { + tx.abort(); // rollback transaction + reject(e); + } + }; + }); + } + + remove(path) { + const tx = this._tx; + return new Promise((resolve, reject) => { + // First, remove from "content" object store + const r1 = tx.objectStore('content').delete(path); + r1.onerror = e => reject(e); + r1.onsuccess = e => { + // Now, remove from "nodes" data store + const r2 = tx.objectStore('nodes').delete(path); + r2.onerror = e => { + tx.abort(); // rollback transaction + reject(e); + }; + r2.onsuccess = e => resolve(); + } + }); + } + + childrenOf(path, include, checkCallback, addCallback) { + // Use cursor to loop from path on + const pathInfo = CustomStorageHelpers.PathInfo.get(path); + const tx = this._tx; + const store = tx.objectStore('nodes'); + const query = IDBKeyRange.lowerBound(path, true); + return new Promise((resolve, reject) => { + /** @type {IDBRequest|IDBRequest} */ + const cursor = include.metadata ? store.openCursor(query) : store.openKeyCursor(query); + cursor.onerror = e => reject(e); + cursor.onsuccess = async e => { + /** type {string} */ + const otherPath = cursor.result ? cursor.result.key : null; + let keepGoing = true; + if (otherPath === null) { + // No more results + keepGoing = false; + } + else if (!pathInfo.isAncestorOf(otherPath)) { + // Paths are sorted, no more children to be expected! + keepGoing = false; + } + else if (pathInfo.isParentOf(otherPath) && checkCallback(otherPath)) { + /** @type {ICustomStorageNode|ICustomStorageNodeMetaData} */ + let node; + if (include.metadata) { + /** @type {IDBRequest} */ + const valueCursor = cursor; + /** @type {IIndexedDBNodeData} */ + const data = valueCursor.result.value; + node = data.metadata; + if (include.value) { + // Load value! + const req = tx.objectStore('content').get(otherPath); + await new Promise((resolve, reject) => { + req.onerror = e => reject(e); + req.onsuccess = e => { + node.value = req.result; + resolve(); + } + }); + } + } + keepGoing = addCallback(otherPath, node); + } + if (keepGoing) { + try { cursor.result.continue(); } + catch(err) { + // We reached the end of the cursor? + keepGoing = false; + } + } + if (!keepGoing) { + resolve(); + } + }; + }); + } + + descendantsOf(path, include, checkCallback, addCallback) { + // Use cursor to loop from path on + // NOTE: Implementation is almost identical to childrenOf, consider merging them + const pathInfo = CustomStorageHelpers.PathInfo.get(path); + const tx = this._tx; + const store = tx.objectStore('nodes'); + const query = IDBKeyRange.lowerBound(path, true); + return new Promise((resolve, reject) => { + /** @type {IDBRequest|IDBRequest} */ + const cursor = include.metadata ? store.openCursor(query) : store.openKeyCursor(query); + cursor.onerror = e => reject(e); + cursor.onsuccess = async e => { + /** @type {string} */ + const otherPath = cursor.result ? cursor.result.key : null; + let keepGoing = true; + if (otherPath === null) { + // No more results + keepGoing = false; + } + else if (!pathInfo.isAncestorOf(otherPath)) { + // Paths are sorted, no more ancestors to be expected! + keepGoing = false; + } + else if (checkCallback(otherPath)) { + /** @type {ICustomStorageNode|ICustomStorageNodeMetaData} */ + let node; + if (include.metadata) { + /** @type {IDBRequest} */ + const valueCursor = cursor; + /** @type {IIndexedDBNodeData} */ + const data = valueCursor.result.value; + node = data.metadata; + if (include.value) { + // Load value! + const req = tx.objectStore('content').get(otherPath); + await new Promise((resolve, reject) => { + req.onerror = e => reject(e); + req.onsuccess = e => { + node.value = req.result; + resolve(); + } + }); + } + } + keepGoing = addCallback(otherPath, node); + } + if (keepGoing) { + try { cursor.result.continue(); } + catch(err) { + // We reached the end of the cursor? + keepGoing = false; + } + } + if (!keepGoing) { + resolve(); + } + }; + }); + } + +} + +// Setup CustomStorageTransaction for browser's LocalStorage +class LocalStorageTransaction extends CustomStorageTransaction { + + /** + * @param {{debug: boolean, dbname: string, localStorage: typeof window.localStorage}} context + * @param {{path: string, write: boolean}} target + */ + constructor(context, target) { + super(target); + this.context = context; + this._storageKeysPrefix = `${this.context.dbname}.acebase::`; + } + + commit() { + // All changes have already been committed + return Promise.resolve(); + } + + rollback(err) { + // Not able to rollback changes, because we did not keep track + return Promise.resolve(); + } + + get(path) { + // Gets value from localStorage, wrapped in Promise + return new Promise(resolve => { + const json = this.context.localStorage.getItem(this.getStorageKeyForPath(path)); + const val = JSON.parse(json); + resolve(val); + }); + } + + set(path, val) { + // Sets value in localStorage, wrapped in Promise + return new Promise(resolve => { + const json = JSON.stringify(val); + this.context.localStorage.setItem(this.getStorageKeyForPath(path), json); + resolve(); + }); + } + + remove(path) { + // Removes a value from localStorage, wrapped in Promise + return new Promise(resolve => { + this.context.localStorage.removeItem(this.getStorageKeyForPath(path)); + resolve(); + }); + } + + childrenOf(path, include, checkCallback, addCallback) { + // Streams all child paths + // Cannot query localStorage, so loop through all stored keys to find children + return new Promise(resolve => { + const pathInfo = CustomStorageHelpers.PathInfo.get(path); + for (let i = 0; i < this.context.localStorage.length; i++) { + const key = this.context.localStorage.key(i); + if (!key.startsWith(this._storageKeysPrefix)) { continue; } + let otherPath = this.getPathFromStorageKey(key); + if (pathInfo.isParentOf(otherPath) && checkCallback(otherPath)) { + let node; + if (include.metadata || include.value) { + const json = this.context.localStorage.getItem(key); + node = JSON.parse(json); + } + const keepGoing = addCallback(otherPath, node); + if (!keepGoing) { break; } + } + } + resolve(); + }); + } + + descendantsOf(path, include, checkCallback, addCallback) { + // Streams all descendant paths + // Cannot query localStorage, so loop through all stored keys to find descendants + return new Promise(resolve => { + const pathInfo = CustomStorageHelpers.PathInfo.get(path); + for (let i = 0; i < this.context.localStorage.length; i++) { + const key = this.context.localStorage.key(i); + if (!key.startsWith(this._storageKeysPrefix)) { continue; } + let otherPath = this.getPathFromStorageKey(key); + if (pathInfo.isAncestorOf(otherPath) && checkCallback(otherPath)) { + let node; + if (include.metadata || include.value) { + const json = this.context.localStorage.getItem(key); + node = JSON.parse(json); + } + const keepGoing = addCallback(otherPath, node); + if (!keepGoing) { break; } + } + } + resolve(); + }); + } + + /** + * Helper function to get the path from a localStorage key + * @param {string} key + */ + getPathFromStorageKey(key) { + return key.slice(this._storageKeysPrefix.length); + } + + /** + * Helper function to get the localStorage key for a path + * @param {string} path + */ + getStorageKeyForPath(path) { + return `${this._storageKeysPrefix}${path}`; + } +} + module.exports = { BrowserAceBase }; \ No newline at end of file diff --git a/src/browser.js b/src/browser.js index 98578db..100f8d2 100644 --- a/src/browser.js +++ b/src/browser.js @@ -22,7 +22,7 @@ const { DataReference, DataSnapshot, EventSubscription, PathReference, TypeMappi const { AceBaseLocalSettings } = require('./acebase-local'); const { BrowserAceBase } = require('./acebase-browser'); const { LocalStorageSettings } = require('./storage-localstorage'); -const { CustomStorageSettings, CustomStorageHelpers } = require('./storage-custom'); +const { CustomStorageSettings, CustomStorageTransaction, CustomStorageHelpers } = require('./storage-custom'); const acebase = { AceBase: BrowserAceBase, @@ -35,6 +35,7 @@ const acebase = { TypeMappingOptions, LocalStorageSettings, CustomStorageSettings, + CustomStorageTransaction, CustomStorageHelpers }; diff --git a/src/index.js b/src/index.js index 5b57177..7ac3db5 100644 --- a/src/index.js +++ b/src/index.js @@ -4,7 +4,7 @@ const { AceBaseStorageSettings } = require('./storage-acebase'); const { SQLiteStorageSettings } = require('./storage-sqlite'); const { MSSQLStorageSettings } = require('./storage-mssql'); const { LocalStorageSettings } = require('./storage-localstorage'); -const { CustomStorageSettings, CustomStorageHelpers } = require('./storage-custom'); +const { CustomStorageTransaction, CustomStorageSettings, CustomStorageHelpers } = require('./storage-custom'); module.exports = { AceBase, @@ -19,6 +19,7 @@ module.exports = { SQLiteStorageSettings, MSSQLStorageSettings, LocalStorageSettings, + CustomStorageTransaction, CustomStorageSettings, CustomStorageHelpers }; \ No newline at end of file diff --git a/src/storage-custom.js b/src/storage-custom.js index 985a4cf..d49405a 100644 --- a/src/storage-custom.js +++ b/src/storage-custom.js @@ -1,5 +1,6 @@ const { debug, ID, PathReference, PathInfo, ascii85 } = require('acebase-core'); const { NodeInfo } = require('./node-info'); +const { NodeLocker } = require('./node-lock'); const { VALUE_TYPES } = require('./node-value-types'); const { Storage, StorageSettings, NodeNotFoundError } = require('./storage'); @@ -31,34 +32,122 @@ class ICustomStorageNode extends ICustomStorageNodeMetaData { /** Enables get/set/remove operations to be wrapped in transactions to improve performance and reliability. */ class CustomStorageTransaction { + + /** + * @param {{ path: string, write: boolean }} target Which path the transaction is taking place on, and whether it is a read or read/write lock. If your storage backend does not support transactions, is synchronous, or if you are able to lock resources based on path: use storage.nodeLocker to ensure threadsafe transactions + */ + constructor(target) { + this.target = { + get originalPath() { return target.path; }, + path: target.path, + get write() { return target.write; } + }; + /** @type {string} Transaction ID */ + this.id = ID.generate(); + } + /** * @param {string} path * @returns {Promise} */ get(path) { throw new Error(`CustomStorageTransaction.get must be overridden by subclass`); } + /** * @param {string} path * @param {ICustomStorageNode} node * @returns {Promise} */ set(path, node) { throw new Error(`CustomStorageTransaction.set must be overridden by subclass`); } + /** * @param {string} path * @returns {Promise} */ remove(path) { throw new Error(`CustomStorageTransaction.remove must be overridden by subclass`); } + /** - * @param {string} reason + * + * @param {string} path Parent path to load children of + * @param {object} include + * @param {boolean} include.metadata Whether metadata needs to be loaded + * @param {boolean} include.value Whether value needs to be loaded + * @param {(childPath: string) => boolean} checkCallback callback method to precheck if child needs to be added, perform before loading metadata/value if possible + * @param {(childPath: string, node?: ICustomStorageNodeMetaData|ICustomStorageNode) => boolean} addCallback callback method that adds the child node. Returns whether or not to keep calling with more children + * @returns {Promise} Returns a promise that resolves when there are no more children to be streamed + */ + childrenOf(path, include, checkCallback, addCallback) { throw new Error(`CustomStorageTransaction.childrenOf must be overridden by subclass`); } + + /** + * + * @param {string} path Parent path to load descendants of + * @param {object} include + * @param {boolean} include.metadata Whether metadata needs to be loaded + * @param {boolean} include.value Whether value needs to be loaded + * @param {(childPath: string) => boolean} checkCallback callback method to precheck if descendant needs to be added, perform before loading metadata/value if possible + * @param {(childPath: string, node?: ICustomStorageNodeMetaData|ICustomStorageNode) => boolean} addCallback callback method that adds the descendant node. Returns whether or not to keep calling with more children + * @returns {Promise} Returns a promise that resolves when there are no more descendants to be streamed + */ + descendantsOf(path, include, checkCallback, addCallback) { throw new Error(`CustomStorageTransaction.descendantsOf must be overridden by subclass`); } + + /** + * Default implementation of getMultiple that executes .get for each given path. Override for custom logic + * @param {string[]} paths + * @returns {Promise>} Returns promise with a Map of paths to nodes + */ + getMultiple(paths) { + const map = new Map(); + return Promise.all(paths.map(path => this.get(path).then(val => map.set(path, val)))) + .then(done => map); + } + + /** + * Default implementation of setMultiple that executes .set for each given path. Override for custom logic + * @param {Array<{ path: string, node: ICustomStorageNode }>} nodes + */ + async setMultiple(nodes) { + await Promise.all(paths.map(({ path, node }) => this.set(path, node))); + } + + /** + * Default implementation of removeMultiple that executes .remove for each given path. Override for custom logic + * @param {string[]} paths + */ + async removeMultiple(paths) { + await Promise.all(paths.map(path => this.remove(path))); + } + + /** + * @param {Error} reason * @returns {Promise} */ rollback(reason) { throw new Error(`CustomStorageTransaction.rollback must be overridden by subclass`); } + /** * @returns {Promise} */ commit() { throw new Error(`CustomStorageTransaction.rollback must be overridden by subclass`); } - constructor() { - /** @type {string} Transaction ID */ - this.id = ID.generate(); + + /** + * Moves the transaction path to the parent node. If node locking is used, it will request a new lock + * Used internally, must not be overridden unless custom locking mechanism is required + * @param {string} targetPath; + */ + async moveToParentPath(targetPath) { + const currentPath = (this._lock && this._lock.path) || this.target.path; + if (currentPath === targetPath) { + return targetPath; // Already on the right path + } + const pathInfo = CustomStorageHelpers.PathInfo.get(targetPath); + if (pathInfo.isParentOf(currentPath)) { + if (this._lock) { + this._lock = await this._lock.moveToParent(); + } + } + else { + throw new Error(`Locking issue. Locked path "${this._lock.path}" is not a child/descendant of "${targetPath}"`); + } + this.target.path = targetPath; + return targetPath; } } @@ -67,59 +156,67 @@ class CustomStorageTransaction { * to get, set and remove data and you're done. */ class CustomStorageSettings extends StorageSettings { + /** * * @param {object} settings - * @param {string} settings.name Name of the custom storage adapter + * @param {string} [settings.name] Name of the custom storage adapter + * @param {boolean} [settings.locking=true] Whether default node locking should be used. Set to false if your storage backend disallows multiple simultanious write transactions (eg IndexedDB). Set to true if your storage backend does not support transactions (eg LocalStorage) or allows multiple simultanious write transactions (eg AceBase binary). * @param {() => Promise} settings.ready Function that returns a Promise that resolves once your data store backend is ready for use - * @param {(path: string) => Promise} settings.get Function that gets the node with given path from your custom data store, must return null if it doesn't exist - * @param {(path: string, value: ICustomStorageNode) => Promise} settings.set Function that inserts or updates a node with given path in your custom data store - * @param {(path: string) => Promise} settings.remove Function that removes the node with given path from your custom data store - * @param {(path: string, include: { value: boolean, metadata: boolean }, checkCallback: (childPath: string) => boolean, addCallback: (childPath: string, node?: ICustomStorageNodeMetaData|ICustomStorageNode) => boolean) => Promise} settings.childrenOf Function that streams all stored nodes that are direct children of the given path. For path "parent/path", results must include paths such as "parent/path/key" AND "parent/path[0]". 👉🏻 You can use CustomStorageHelpers for logic. Keep calling the add callback for each node until it returns false. - * @param {(path: string, include: { value: boolean, metadata: boolean }, checkCallback: (childPath: string) => boolean, addCallback: (descPath: string, node?: ICustomStorageNodeMetaData|ICustomStorageNode) => boolean) => Promise} settings.descendantsOf Function that streams all stored nodes that are descendants of the given path. For path "parent/path", results must include paths such as "parent/path/key", "parent/path/key/subkey", "parent/path[0]", "parent/path[12]/key" etc. 👉🏻 You can use CustomStorageHelpers for logic. Keep calling the add callback for each node until it returns false. - * @param {(paths: string[]) => Promise>} [settings.getMultiple] (optional, not used yet) Function that gets multiple nodes (metadata AND value) from your custom data store at once. Must return a Promise that resolves with Map - * @param {(paths: string[]) => Promise} [settings.removeMultiple] (optional) Function that removes multiple nodes from your custom data store at once + * @param {(target: { path: string, write: boolean }, nodeLocker: NodeLocker) => Promise} settings.getTransaction Function that starts a transaction for read/write operations on a specific path and/or child paths */ - constructor(settings) { + constructor(settings) { super(settings); settings = settings || {}; if (typeof settings.ready !== 'function') { throw new Error(`ready must be a function`); } - if (typeof settings.get !== 'function') { - throw new Error(`get must be a function`); - } - if (typeof settings.set !== 'function') { - throw new Error(`set must be a function`); - } - if (typeof settings.remove !== 'function') { - throw new Error(`remove must be a function`); - } - if (typeof settings.childrenOf !== 'function') { - throw new Error(`childrenOf must be a function`); - } - if (typeof settings.descendantsOf !== 'function') { - throw new Error(`descendantsOf must be a function`); + if (typeof settings.getTransaction !== 'function') { + throw new Error(`getTransaction must be a function`); } this.name = settings.name; this.info = `${this.name || 'CustomStorage'} realtime database`; + this.locking = settings.locking !== false; this.ready = settings.ready; - this.get = settings.get; - this.getMultiple = settings.getMultiple - || (paths => { - const map = new Map(); - return Promise.all(paths.map(path => this.get(path).then(val => map.set(path, val)))) - .then(done => map); - }); - this.set = settings.set; - this.remove = settings.remove; - this.removeMultiple = settings.removeMultiple - || (paths => { - return Promise.all(paths.map(path => this.remove(path))) - .then(done => true); - }); - this.childrenOf = settings.childrenOf; - this.descendantsOf = settings.descendantsOf; + + // Hijack getTransaction to add locking + const useLocking = this.locking; + const nodeLocker = useLocking ? new NodeLocker() : null; + this.getTransaction = async ({ path, write }) => { + // console.log(`${write ? 'WRITE' : 'READ'} transaction requested for path "${path}"`) + const transaction = await settings.getTransaction({ path, write }); + console.assert(typeof transaction.id === 'string', `transaction id not set`); + // console.log(`Got transaction ${transaction.id} for ${write ? 'WRITE' : 'READ'} on path "${path}"`); + + // Hijack rollback and commit + const rollback = transaction.rollback; + const commit = transaction.commit; + transaction.commit = async () => { + // console.log(`COMMIT ${transaction.id} for ${write ? 'WRITE' : 'READ'} on path "${path}"`); + const ret = await commit.call(transaction); + // console.log(`COMMIT DONE ${transaction.id} for ${write ? 'WRITE' : 'READ'} on path "${path}"`); + if (useLocking) { + await transaction._lock.release('commit'); + } + return ret; + } + transaction.rollback = async (reason) => { + // const reasonText = reason instanceof Error ? reason.message : reason.toString(); + // console.error(`ROLLBACK ${transaction.id} for ${write ? 'WRITE' : 'READ'} on path "${path}":`, reason); + const ret = await rollback.call(transaction, reason); + // console.log(`ROLLBACK DONE ${transaction.id} for ${write ? 'WRITE' : 'READ'} on path "${path}"`); + if (useLocking) { + await transaction._lock.release('rollback'); + } + return ret; + } + + if (useLocking) { + // Lock the path before continuing + transaction._lock = await nodeLocker.lock(path, transaction.id, write, `${this.name}::getTransaction`); + } + return transaction; + } } }; @@ -226,8 +323,7 @@ class CustomStorage extends Storage { this._init(); } - - _init() { + async _init() { /** @type {CustomStorageSettings} */ this._customImplementation = this.settings; this.debug.log(`Database "${this.name}" details:`.intro); @@ -237,34 +333,28 @@ class CustomStorage extends Storage { this.debug.log(`- Autoremove undefined props: ${this.settings.removeVoidProperties}`); // Create root node if it's not there yet - return this._customImplementation.ready() - .then(ready => this.getNodeInfo('')) - .then(info => { - if (!info.exists) { - return this._writeNode('', {}); - } - }) - .then(() => { - return this.indexes.supported && this.indexes.load(); - }) - .then(() => { - this.emit('ready'); - }); + await this._customImplementation.ready(); + const transaction = await this._customImplementation.getTransaction({ path: '', write: true }); + const info = await this.getNodeInfo('', { transaction }); + if (!info.exists) { + await this._writeNode('', {}, { transaction }); + } + await transaction.commit(); + if (this.indexes.supported) { + await this.indexes.load(); + } + this.emit('ready'); } /** * * @param {string} path - * @param {object} info - * @param {number} info.type - * @param {any} info.value - * @param {string} info.revision - * @param {number} info.revision_nr - * @param {number} info.created - * @param {number} info.modified + * @param {ICustomStorageNode} node + * @param {object} options + * @param {CustomStorageTransaction} options.transaction * @returns {Promise} */ - _storeNode(path, info) { + _storeNode(path, node, options) { // serialize the value to store const getTypedChildValue = val => { if (val === null) { @@ -289,34 +379,34 @@ class CustomStorage extends Storage { } const unprocessed = `Caller should have pre-processed the value by converting it to a string`; - if (info.type === VALUE_TYPES.ARRAY && info.value instanceof Array) { + if (node.type === VALUE_TYPES.ARRAY && node.value instanceof Array) { // Convert array to object with numeric properties // NOTE: caller should have done this already console.warn(`Unprocessed array. ${unprocessed}`); const obj = {}; - for (let i = 0; i < info.value.length; i++) { - obj[i] = info.value[i]; + for (let i = 0; i < node.value.length; i++) { + obj[i] = node.value[i]; } - info.value = obj; + node.value = obj; } - if (info.type === VALUE_TYPES.BINARY && typeof info.value !== 'string') { + if (node.type === VALUE_TYPES.BINARY && typeof node.value !== 'string') { console.warn(`Unprocessed binary value. ${unprocessed}`); - info.value = ascii85.encode(info.value); + node.value = ascii85.encode(node.value); } - if (info.type === VALUE_TYPES.REFERENCE && info.value instanceof PathReference) { + if (node.type === VALUE_TYPES.REFERENCE && node.value instanceof PathReference) { console.warn(`Unprocessed path reference. ${unprocessed}`); - info.value = info.value.path; + node.value = node.value.path; } - if ([VALUE_TYPES.OBJECT, VALUE_TYPES.ARRAY].includes(info.type)) { - const original = info.value; - info.value = {}; + if ([VALUE_TYPES.OBJECT, VALUE_TYPES.ARRAY].includes(node.type)) { + const original = node.value; + node.value = {}; // If original is an array, it'll automatically be converted to an object now Object.keys(original).forEach(key => { - info.value[key] = getTypedChildValue(original[key]); + node.value[key] = getTypedChildValue(original[key]); }); } - return this._customImplementation.set(path, info); + return options.transaction.set(path, node); } /** @@ -382,9 +472,14 @@ class CustomStorage extends Storage { } } - async _readNode(path) { + /** + * @param {string} path + * @param {object} options + * @param {CustomStorageTransaction} options.transaction + */ + async _readNode(path, options) { // deserialize a stored value (always an object with "type", "value", "revision", "revision_nr", "created", "modified") - let node = await this._customImplementation.get(path); + let node = await options.transaction.get(path); if (node === null) { return null; } if (typeof node !== 'object') { throw new Error(`CustomStorage get function must return an ICustomStorageNode object. Use JSON.parse if your set function stored it as a string`); @@ -434,11 +529,13 @@ class CustomStorage extends Storage { * Creates or updates a node in its own record. DOES NOT CHECK if path exists in parent node, or if parent paths exist! Calling code needs to do this * @param {string} path * @param {any} value - * @param {object} [options] - * @param {CustomStorageTransaction} [options.transaction] TODO: implement + * @param {object} options] + * @param {CustomStorageTransaction} options.transaction + * @param {boolean} [options.merge=false] + * @param {string} [options.revision] * @returns {Promise} */ - async _writeNode(path, value, options = { merge: false, revision: null, transaction: null }) { + async _writeNode(path, value, options) { if (this.valueFitsInline(value) && path !== '') { throw new Error(`invalid value to store in its own node`); } @@ -446,9 +543,11 @@ class CustomStorage extends Storage { throw new Error(`Invalid root node value. Must be an object`); } + const transaction = options.transaction; + // Get info about current node at path - const currentRow = await this._readNode(path); - const newRevision = (options && options.revision) || ID.generate(); + const currentRow = await this._readNode(path, { transaction }); + const revision = options.revision || ID.generate(); let mainNode = { type: VALUE_TYPES.OBJECT, value: {} @@ -558,7 +657,7 @@ class CustomStorage extends Storage { keys.push(key); return true; // Keep streaming } - await this._customImplementation.childrenOf(path, { metadata: false, value: false }, includeChildCheck, addChildPath); + await transaction.childrenOf(path, { metadata: false, value: false }, includeChildCheck, addChildPath); children.current = children.current.concat(keys); if (newIsObjectOrArray) { @@ -582,7 +681,7 @@ class CustomStorage extends Storage { const writePromises = Object.keys(childNodeValues).map(key => { const childPath = pathInfo.childPath(key); // PathInfo.getChildPath(path, key); const childValue = childNodeValues[key]; - return this._writeNode(childPath, childValue, { revision: newRevision, merge: false }); + return this._writeNode(childPath, childValue, { transaction, revision, merge: false }); }); // Delete all child nodes that were stored in their own record, but are being removed @@ -591,7 +690,7 @@ class CustomStorage extends Storage { const deleteDedicatedKeys = changes.delete.concat(movingNodes); const deletePromises = deleteDedicatedKeys.map(key => { const childPath = pathInfo.childPath(key); //PathInfo.getChildPath(path, key); - return this._deleteNode(childPath); + return this._deleteNode(childPath, { transaction }); }); const promises = writePromises.concat(deletePromises); @@ -599,6 +698,7 @@ class CustomStorage extends Storage { } // Update main node + // TODO: Check if revision should change? return await this._storeNode(path, { type: mainNode.type, value: mainNode.value, @@ -606,6 +706,8 @@ class CustomStorage extends Storage { revision_nr: currentRow.revision_nr + 1, created: currentRow.created, modified: Date.now() + }, { + transaction }); } else { @@ -616,17 +718,19 @@ class CustomStorage extends Storage { const promises = Object.keys(childNodeValues).map(key => { const childPath = PathInfo.getChildPath(path, key); const childValue = childNodeValues[key]; - return this._writeNode(childPath, childValue, { revision: newRevision, merge: false }); + return this._writeNode(childPath, childValue, { transaction, revision, merge: false }); }); // Create current node const p = this._storeNode(path, { type: mainNode.type, value: mainNode.value, - revision: newRevision, + revision, revision_nr: 1, created: Date.now(), modified: Date.now() + }, { + transaction }); promises.push(p); return Promise.all(promises); @@ -636,8 +740,10 @@ class CustomStorage extends Storage { /** * Deletes (dedicated) node and all subnodes without checking for existence. Use with care - all removed nodes will lose their revision stats! DOES NOT REMOVE INLINE CHILD NODES! * @param {string} path + * @param {object} options + * @param {CustomStorageTransaction} options.transaction */ - async _deleteNode(path) { + async _deleteNode(path, options) { const pathInfo = PathInfo.get(path); this.debug.log(`Node "/${path}" is being deleted`.cyan); @@ -658,19 +764,23 @@ class CustomStorage extends Storage { deletePaths.push(descPath); return true; }; - await this._customImplementation.descendantsOf(path, { metadata: false, value: false }, includeDescendantCheck, addDescendant); + const transaction = options.transaction; + await transaction.descendantsOf(path, { metadata: false, value: false }, includeDescendantCheck, addDescendant); this.debug.log(`Nodes ${deletePaths.map(p => `"/${p}"`).join(',')} are being deleted`.cyan); - return this._customImplementation.removeMultiple(deletePaths); + return transaction.removeMultiple(deletePaths); } /** * Enumerates all children of a given Node for reflection purposes * @param {string} path + * @param {object} [options] + * @param {CustomStorageTransaction} [options.transaction] * @param {string[]|number[]} [options.keyFilter] */ - getChildren(path, options = { keyFilter: undefined, tid: undefined }) { + getChildren(path, options) { // return generator + options = options || {}; var callback; //, resolve, reject; const generator = { /** @@ -683,128 +793,159 @@ class CustomStorage extends Storage { return start(); } }; - const start = () => { - let lock, canceled = false; - const tid = (options && options.tid) || ID.generate(); - return this.nodeLocker.lock(path, tid, false, 'getChildren') - .then(async l => { - lock = l; + const start = async () => { + // let lock; + const transaction = options.transaction || await this._customImplementation.getTransaction({ path, write: false }); + // return this.nodeLocker.lock(path, transaction.id, false, 'getChildren') + // .then(async l => { + // lock = l; + try { + let canceled = false; + await (async () => { + let node = await this._readNode(path, { transaction }); + if (!node) { throw new NodeNotFoundError(`Node "/${path}" does not exist`); } + // node = JSON.parse(node); - let node = await this._readNode(path); - if (!node) { throw new NodeNotFoundError(`Node "/${path}" does not exist`); } - // node = JSON.parse(node); - - if (![VALUE_TYPES.OBJECT, VALUE_TYPES.ARRAY].includes(node.type)) { - // No children - return; - } - const isArray = node.type === VALUE_TYPES.ARRAY; - const value = node.value; - let keys = Object.keys(value); - if (options.keyFilter) { - keys = keys.filter(key => options.keyFilter.includes(key)); - } - const pathInfo = PathInfo.get(path); - keys.length > 0 && keys.every(key => { - let child = this._getTypeFromStoredValue(value[key]); - - const info = new CustomStorageNodeInfo({ - path: pathInfo.childPath(key), - key: isArray ? null : key, - index: isArray ? key : null, - type: child.type, - address: null, - exists: true, - value: child.value, - revision: node.revision, - revision_nr: node.revision_nr, - created: node.created, - modified: node.modified - }); - - canceled = callback(info) === false; - return !canceled; // stop .every loop if canceled - }); - if (canceled) { - return; - } - - // Go on... get other children - let checkExecuted = false; - const includeChildCheck = (childPath) => { - checkExecuted = true; - if (!pathInfo.isParentOf(childPath)) { - // Double check failed - throw new Error(`"${childPath}" is not a child of "${path}" - childrenOf must only check and return paths that are children`); + if (![VALUE_TYPES.OBJECT, VALUE_TYPES.ARRAY].includes(node.type)) { + // No children + return; } + const isArray = node.type === VALUE_TYPES.ARRAY; + const value = node.value; + let keys = Object.keys(value); if (options.keyFilter) { - const key = PathInfo.get(childPath).key; - return options.keyFilter.includes(key); + keys = keys.filter(key => options.keyFilter.includes(key)); } - return true; - }; + const pathInfo = PathInfo.get(path); + keys.length > 0 && keys.every(key => { + let child = this._getTypeFromStoredValue(value[key]); - /** - * - * @param {string} childPath - * @param {ICustomStorageNodeMetaData} node - */ - const addChildNode = (childPath, node) => { - if (!checkExecuted) { - throw new Error(`${this._customImplementation.info} childrenOf did not call checkCallback before addCallback`); - } - const key = PathInfo.get(childPath).key; - const info = new CustomStorageNodeInfo({ - path: childPath, - type: node.type, - key: isArray ? null : key, - index: isArray ? key : null, - address: new CustomStorageNodeAddress(childPath), - exists: true, - value: null, // not loaded - revision: node.revision, - revision_nr: node.revision_nr, - created: new Date(node.created), - modified: new Date(node.modified) + const info = new CustomStorageNodeInfo({ + path: pathInfo.childPath(key), + key: isArray ? null : key, + index: isArray ? key : null, + type: child.type, + address: null, + exists: true, + value: child.value, + revision: node.revision, + revision_nr: node.revision_nr, + created: node.created, + modified: node.modified + }); + + canceled = callback(info) === false; + return !canceled; // stop .every loop if canceled }); + if (canceled) { + return; + } - canceled = callback(info) === false; - return !canceled; - }; - return this._customImplementation.childrenOf(path, { metadata: true, value: false }, includeChildCheck, addChildNode); - }) - .then(() => { - lock.release(); + // Go on... get other children + let checkExecuted = false; + const includeChildCheck = (childPath) => { + checkExecuted = true; + if (!pathInfo.isParentOf(childPath)) { + // Double check failed + throw new Error(`"${childPath}" is not a child of "${path}" - childrenOf must only check and return paths that are children`); + } + if (options.keyFilter) { + const key = PathInfo.get(childPath).key; + return options.keyFilter.includes(key); + } + return true; + }; + + /** + * + * @param {string} childPath + * @param {ICustomStorageNodeMetaData} node + */ + const addChildNode = (childPath, node) => { + if (!checkExecuted) { + throw new Error(`${this._customImplementation.info} childrenOf did not call checkCallback before addCallback`); + } + const key = PathInfo.get(childPath).key; + const info = new CustomStorageNodeInfo({ + path: childPath, + type: node.type, + key: isArray ? null : key, + index: isArray ? key : null, + address: new CustomStorageNodeAddress(childPath), + exists: true, + value: null, // not loaded + revision: node.revision, + revision_nr: node.revision_nr, + created: new Date(node.created), + modified: new Date(node.modified) + }); + + canceled = callback(info) === false; + return !canceled; + }; + await transaction.childrenOf(path, { metadata: true, value: false }, includeChildCheck, addChildNode); + })(); + if (!options.transaction) { + // transaction was created by us, commit + await transaction.commit(); + } return canceled; - }) - .catch(err => { - lock.release(); + } + catch (err) { + if (!options.transaction) { + // transaction was created by us, rollback + await transaction.rollback(err); + } throw err; - }); + } + + // }) + // .then(() => { + // lock.release(); + // return canceled; + // }) + // .catch(err => { + // lock.release(); + // throw err; + // }); }; // start() return generator; } - getNode(path, options = { include: undefined, exclude: undefined, child_objects: true, tid: undefined }) { + /** + * + * @param {string} path + * @param {object} [options] + * @param {string[]} [options.include] + * @param {string[]} [options.exclude] + * @param {boolean} [options.child_objects=true] + * @param {CustomStorageTransaction} [options.transaction] + * @returns {Promise} + */ + async getNode(path, options) { // path = path.replace(/'/g, ''); // prevent sql injection, remove single quotes - const tid = (options && options.tid )|| ID.generate(); - let lock; - return this.nodeLocker.lock(path, tid, false, 'getNode') - .then(async l => { - lock = l; - - // Get path, path/* and path[* - const filtered = options && (options.include || options.exclude || options.child_objects === false); - const pathInfo = PathInfo.get(path); - const targetNode = await this._readNode(path); - if (!targetNode) { - // Lookup parent node - if (path === '') { return { value: null }; } // path is root. There is no parent. - return lock.moveToParent() - .then(async parentLock => { - lock = parentLock; - let parentNode = await this._readNode(pathInfo.parentPath); + options = options || {}; + const transaction = options.transaction || await this._customImplementation.getTransaction({ path, write: false }); + // let lock; + // return this.nodeLocker.lock(path, tid, false, 'getNode') + // .then(async l => { + // lock = l; + try { + const node = await (async () => { + // Get path, path/* and path[* + const filtered = options.include || options.exclude || options.child_objects === false; + const pathInfo = PathInfo.get(path); + const targetNode = await this._readNode(path, { transaction }); + if (!targetNode) { + // Lookup parent node + if (path === '') { return { value: null }; } // path is root. There is no parent. + const lockPath = await transaction.moveToParentPath(pathInfo.parentPath); + console.assert(lockPath === pathInfo.parentPath, `transaction.moveToParentPath() did not move to the right parent path of "${path}"`) + // return lock.moveToParent() + // .then(async parentLock => { + // lock = parentLock; + let parentNode = await this._readNode(pathInfo.parentPath, { transaction }); if (parentNode && [VALUE_TYPES.OBJECT, VALUE_TYPES.ARRAY].includes(parentNode.type) && pathInfo.key in parentNode) { const childValueInfo = this._getTypeFromStoredValue(parentNode.value[pathInfo.key]); return { @@ -817,198 +958,215 @@ class CustomStorage extends Storage { }; } return { value: null }; - }); - } - - const includeCheck = options.include - ? new RegExp('^' + options.include.map(p => '(?:' + p.replace(/\*/g, '[^/\\[]+') + ')').join('|') + '(?:$|[/\\[])') - : null; - const excludeCheck = options.exclude - ? new RegExp('^' + options.exclude.map(p => '(?:' + p.replace(/\*/g, '[^/\\[]+') + ')').join('|') + '(?:$|[/\\[])') - : null; - - let checkExecuted = false; - const includeDescendantCheck = (descPath) => { - checkExecuted = true; - if (!pathInfo.isAncestorOf(descPath)) { - // Double check failed - throw new Error(`"${descPath}" is not a descendant of "${path}" - descendantsOf must only check and return paths that are descendants`); + // }); } - if (!filtered) { return true; } - // Apply include & exclude filters - let checkPath = descPath.slice(path.length); - if (checkPath[0] === '/') { checkPath = checkPath.slice(1); } - let include = (includeCheck ? includeCheck.test(checkPath) : true) - && (excludeCheck ? !excludeCheck.test(checkPath) : true); + const includeCheck = options.include + ? new RegExp('^' + options.include.map(p => '(?:' + p.replace(/\*/g, '[^/\\[]+') + ')').join('|') + '(?:$|[/\\[])') + : null; + const excludeCheck = options.exclude + ? new RegExp('^' + options.exclude.map(p => '(?:' + p.replace(/\*/g, '[^/\\[]+') + ')').join('|') + '(?:$|[/\\[])') + : null; - // Apply child_objects filter - if (include - && options.child_objects === false - && (pathInfo.isParentOf(descPath) && [VALUE_TYPES.OBJECT, VALUE_TYPES.ARRAY].includes(childNode.type) - || PathInfo.getPathKeys(descPath).length > pathInfo.pathKeys.length + 1) - ) { - include = false; + let checkExecuted = false; + const includeDescendantCheck = (descPath) => { + checkExecuted = true; + if (!pathInfo.isAncestorOf(descPath)) { + // Double check failed + throw new Error(`"${descPath}" is not a descendant of "${path}" - descendantsOf must only check and return paths that are descendants`); + } + if (!filtered) { return true; } + + // Apply include & exclude filters + let checkPath = descPath.slice(path.length); + if (checkPath[0] === '/') { checkPath = checkPath.slice(1); } + let include = (includeCheck ? includeCheck.test(checkPath) : true) + && (excludeCheck ? !excludeCheck.test(checkPath) : true); + + // Apply child_objects filter + if (include + && options.child_objects === false + && (pathInfo.isParentOf(descPath) && [VALUE_TYPES.OBJECT, VALUE_TYPES.ARRAY].includes(childNode.type) + || PathInfo.getPathKeys(descPath).length > pathInfo.pathKeys.length + 1) + ) { + include = false; + } + return include; } - return include; - } - const descRows = []; - /** - * - * @param {string} descPath - * @param {ICustomStorageNode} node - */ - const addDescendant = (descPath, node) => { - if (!checkExecuted) { - throw new Error(`${this._customImplementation.info} descendantsOf did not call checkCallback before addCallback`); + const descRows = []; + /** + * + * @param {string} descPath + * @param {ICustomStorageNode} node + */ + const addDescendant = (descPath, node) => { + if (!checkExecuted) { + throw new Error(`${this._customImplementation.info} descendantsOf did not call checkCallback before addCallback`); + } + + // Process the value + this._processReadNodeValue(node); + + // Add node + node.path = descPath; + descRows.push(node); + + return true; // Keep streaming + }; + + await transaction.descendantsOf(path, { metadata: true, value: true }, includeDescendantCheck, addDescendant); + + this.debug.log(`Read node "/${path}" and ${filtered ? '(filtered) ' : ''}descendants from ${descRows.length + 1} records`.magenta); + + const result = targetNode; + + const objectToArray = obj => { + // Convert object value to array + const arr = []; + Object.keys(obj).forEach(key => { + let index = parseInt(key); + arr[index] = obj[index]; + }); + return arr; + }; + + if (targetNode.type === VALUE_TYPES.ARRAY) { + result.value = objectToArray(result.value); } - - // Process the value - this._processReadNodeValue(node); - - // Add node - node.path = descPath; - descRows.push(node); - return true; // Keep streaming - }; - - await this._customImplementation.descendantsOf(path, { metadata: true, value: true }, includeDescendantCheck, addDescendant); - - this.debug.log(`Read node "/${path}" and ${filtered ? '(filtered) ' : ''}children from ${descRows.length + 1} records`.magenta); - - const result = targetNode; - - const objectToArray = obj => { - // Convert object value to array - const arr = []; - Object.keys(obj).forEach(key => { - let index = parseInt(key); - arr[index] = obj[index]; - }); - return arr; - }; - - if (targetNode.type === VALUE_TYPES.ARRAY) { - result.value = objectToArray(result.value); - } - - if (targetNode.type === VALUE_TYPES.OBJECT || targetNode.type === VALUE_TYPES.ARRAY) { - // target node is an object or array - // merge with other found (child) nodes - const targetPathKeys = PathInfo.getPathKeys(path); - let value = targetNode.value; - for (let i = 0; i < descRows.length; i++) { - const otherNode = descRows[i]; - const pathKeys = PathInfo.getPathKeys(otherNode.path); - const trailKeys = pathKeys.slice(targetPathKeys.length); - let parent = value; - for (let j = 0 ; j < trailKeys.length; j++) { - console.assert(typeof parent === 'object', 'parent must be an object/array to have children!!'); - const key = trailKeys[j]; - const isLast = j === trailKeys.length-1; - const nodeType = isLast - ? otherNode.type - : typeof trailKeys[j+1] === 'number' - ? VALUE_TYPES.ARRAY - : VALUE_TYPES.OBJECT; - let nodeValue; - if (!isLast) { - nodeValue = nodeType === VALUE_TYPES.OBJECT ? {} : []; - } - else { - nodeValue = otherNode.value; - if (nodeType === VALUE_TYPES.ARRAY) { - nodeValue = objectToArray(nodeValue); + if (targetNode.type === VALUE_TYPES.OBJECT || targetNode.type === VALUE_TYPES.ARRAY) { + // target node is an object or array + // merge with other found (child) nodes + const targetPathKeys = PathInfo.getPathKeys(path); + let value = targetNode.value; + for (let i = 0; i < descRows.length; i++) { + const otherNode = descRows[i]; + const pathKeys = PathInfo.getPathKeys(otherNode.path); + const trailKeys = pathKeys.slice(targetPathKeys.length); + let parent = value; + for (let j = 0 ; j < trailKeys.length; j++) { + console.assert(typeof parent === 'object', 'parent must be an object/array to have children!!'); + const key = trailKeys[j]; + const isLast = j === trailKeys.length-1; + const nodeType = isLast + ? otherNode.type + : typeof trailKeys[j+1] === 'number' + ? VALUE_TYPES.ARRAY + : VALUE_TYPES.OBJECT; + let nodeValue; + if (!isLast) { + nodeValue = nodeType === VALUE_TYPES.OBJECT ? {} : []; } + else { + nodeValue = otherNode.value; + if (nodeType === VALUE_TYPES.ARRAY) { + nodeValue = objectToArray(nodeValue); + } + } + if (key in parent) { + // Merge with parent + console.assert(typeof parent[key] === typeof nodeValue && [VALUE_TYPES.OBJECT, VALUE_TYPES.ARRAY].includes(nodeType), 'Merging child values can only be done if existing and current values are both an array or object'); + Object.keys(nodeValue).forEach(childKey => { + console.assert(!(childKey in parent[key]), 'child key is in parent value already?! HOW?!'); + parent[key][childKey] = nodeValue[childKey]; + }); + } + else { + parent[key] = nodeValue; + } + parent = parent[key]; } - if (key in parent) { - // Merge with parent - console.assert(typeof parent[key] === typeof nodeValue && [VALUE_TYPES.OBJECT, VALUE_TYPES.ARRAY].includes(nodeType), 'Merging child values can only be done if existing and current values are both an array or object'); - Object.keys(nodeValue).forEach(childKey => { - console.assert(!(childKey in parent[key]), 'child key is in parent value already?! HOW?!'); - parent[key][childKey] = nodeValue[childKey]; + } + } + else if (descRows.length > 0) { + throw new Error(`multiple records found for non-object value!`); + } + + // Post process filters to remove any data that got though because they were + // not stored in dedicated records. This will happen with smaller values because + // they are stored inline in their parent node. + // eg: + // { number: 1, small_string: 'small string', bool: true, obj: {}, arr: [] } + // All properties of this object are stored inline, + // if exclude: ['obj'], or child_objects: false was passed, these will still + // have to be removed from the value + + if (options.child_objects === false) { + Object.keys(result.value).forEach(key => { + if (typeof result.value[key] === 'object' && result.value[key].constructor === Object) { + // This can only happen if the object was empty + console.assert(Object.keys(result.value[key]).length === 0); + delete result.value[key]; + } + }) + } + + if (options.exclude) { + const process = (obj, keys) => { + if (typeof obj !== 'object') { return; } + const key = keys[0]; + if (key === '*') { + Object.keys(obj).forEach(k => { + process(obj[k], keys.slice(1)); }); } - else { - parent[key] = nodeValue; + else if (keys.length > 1) { + key in obj && process(obj[key], keys.slice(1)); } - parent = parent[key]; - } + else { + delete obj[key]; + } + }; + options.exclude.forEach(path => { + const checkKeys = PathInfo.getPathKeys(path); + process(result.value, checkKeys); + }); } + + return result; + })(); + if (!options.transaction) { + // transaction was created by us, commit + await transaction.commit(); + } + return node; + } + catch (err) { + if (!options.transaction) { + // transaction was created by us, rollback + await transaction.rollback(err); } - else if (descRows.length > 0) { - throw new Error(`multiple records found for non-object value!`); - } - - // Post process filters to remove any data that got though because they were - // not stored in dedicated records. This will happen with smaller values because - // they are stored inline in their parent node. - // eg: - // { number: 1, small_string: 'small string', bool: true, obj: {}, arr: [] } - // All properties of this object are stored inline, - // if exclude: ['obj'], or child_objects: false was passed, these will still - // have to be removed from the value - - if (options.child_objects === false) { - Object.keys(result.value).forEach(key => { - if (typeof result.value[key] === 'object' && result.value[key].constructor === Object) { - // This can only happen if the object was empty - console.assert(Object.keys(result.value[key]).length === 0); - delete result.value[key]; - } - }) - } - - if (options.exclude) { - const process = (obj, keys) => { - if (typeof obj !== 'object') { return; } - const key = keys[0]; - if (key === '*') { - Object.keys(obj).forEach(k => { - process(obj[k], keys.slice(1)); - }); - } - else if (keys.length > 1) { - key in obj && process(obj[key], keys.slice(1)); - } - else { - delete obj[key]; - } - }; - options.exclude.forEach(path => { - const checkKeys = PathInfo.getPathKeys(path); - process(result.value, checkKeys); - }); - } - return result; - }) - .then(result => { - lock.release(); - return result; - }) - .catch(err => { - lock.release(); throw err; - }); + } + // }) + // .then(result => { + // lock.release(); + // return result; + // }) + // .catch(err => { + // lock.release(); + // throw err; + // }); } /** * * @param {string} path - * @param {*} options + * @param {object} [options] + * @param {CustomStorageTransaction} [options.transaction] * @returns {Promise} */ - getNodeInfo(path, options = { tid: undefined }) { + async getNodeInfo(path, options) { + options = options || {}; const pathInfo = PathInfo.get(path); - const tid = (options && options.tid) || ID.generate(); - let lock; - return this.nodeLocker.lock(path, tid, false, 'getNodeInfo') - .then(async l => { - lock = l; - - const node = await this._readNode(path); + // let lock; + const transaction = options.transaction || await this._customImplementation.getTransaction({ path, write: true }); + // return this.nodeLocker.lock(path, transaction.id, false, 'getNodeInfo') + // .then(async l => { + // lock = l; + try { + const node = await this._readNode(path, { transaction }); const info = new CustomStorageNodeInfo({ path, key: typeof pathInfo.key === 'string' ? pathInfo.key : null, @@ -1022,15 +1180,16 @@ class CustomStorage extends Storage { revision_nr: node ? node.revision_nr : null }); - if (node || path === '') { - return info; - } + if (!node && path !== '') { + // Try parent node - // Try parent node - return lock.moveToParent() - .then(async parentLock => { - lock = parentLock; - const parent = await this._readNode(pathInfo.parentPath); + // return lock.moveToParent() + // .then(async parentLock => { + // lock = parentLock; + + const lockPath = await transaction.moveToParentPath(pathInfo.parentPath); + console.assert(lockPath === pathInfo.parentPath, `transaction.moveToParentPath() did not move to the right parent path of "${path}"`) + const parent = await this._readNode(pathInfo.parentPath, { transaction }); if (parent && [VALUE_TYPES.OBJECT, VALUE_TYPES.ARRAY].includes(parent.type) && pathInfo.key in parent.value) { // Stored in parent node info.exists = true; @@ -1046,146 +1205,236 @@ class CustomStorage extends Storage { // Parent doesn't exist, so the node we're looking for cannot exist either info.address = null; } - return info; - }) - }) - .then(info => { - lock.release(); + } + + if (!options.transaction) { + // transaction was created by us, commit + await transaction.commit(); + } return info; - }) - .catch(err => { - lock && lock.release(); + // }) + } + catch (err) { + if (!options.transaction) { + // transaction was created by us, rollback + await transaction.rollback(err); + } throw err; - }); + } + // }) + // .then(info => { + // lock.release(); + // return info; + // }) + // .catch(err => { + // lock && lock.release(); + // throw err; + // }); } // TODO: Move to Storage base class? - removeNode(path, options = { tid: undefined }) { + /** + * + * @param {string} path + * @param {object} [options] + * @param {CustomStorageTransaction} [options.transaction] + * @returns {Promise} + */ + async removeNode(path, options) { if (path === '') { return Promise.reject(new Error(`Cannot remove the root node`)); } + options = options || {}; const pathInfo = PathInfo.get(path); - const tid = (options && options.tid) || ID.generate(); - return this.nodeLocker.lock(pathInfo.parentPath, tid, true, 'removeNode') - .then(lock => { - return this.updateNode(pathInfo.parentPath, { [pathInfo.key]: null }, { tid }) - .then(result => { - lock.release(); - return result; - }) - .catch(err => { - lock.release(); - throw err; - }); - }); + const transaction = options.transaction || this._customImplementation.getTransaction({ path, write: true }); + // return this.nodeLocker.lock(pathInfo.parentPath, transaction.id, true, 'removeNode') + // .then(lock => { + try { + await this.updateNode(pathInfo.parentPath, { [pathInfo.key]: null }, { transaction }); + if (!options.transaction) { + // transaction was created by us, commit + await transaction.commit(); + } + } + catch (err) { + if (!options.transaction) { + // transaction was created by us, rollback + await transaction.rollback(err); + } + throw err; + } + // .then(result => { + // lock.release(); + // return result; + // }) + // .catch(err => { + // lock.release(); + // throw err; + // }); + // }); } // TODO: Move to Storage base class? - setNode(path, value, options = { assert_revision: undefined, tid: undefined }) { + /** + * + * @param {string} path + * @param {any} value + * @param {object} [options] + * @param {string} [options.assert_revision] + * @param {CustomStorageTransaction} [options.transaction] + * @returns {Promise} + */ + async setNode(path, value, options) { const pathInfo = PathInfo.get(path); - let lock; - const tid = (options && options.tid) || ID.generate(); - return this.nodeLocker.lock(path, tid, true, 'setNode') - .then(l => { - lock = l; + // let lock; + options = options || {}; + const transaction = options.transaction || await this._customImplementation.getTransaction({ path, write: true }); + // return this.nodeLocker.lock(path, transaction.id, true, 'setNode') + // .then(l => { + // lock = l; + try { if (path === '') { if (value === null || typeof value !== 'object' || value instanceof Array || value instanceof ArrayBuffer || ('buffer' in value && value.buffer instanceof ArrayBuffer)) { - return Promise.reject(new Error(`Invalid value for root node: ${value}`)); + throw new Error(`Invalid value for root node: ${value}`); } - - return this._writeNodeWithTracking('', value, { merge: false, tid }) + await this._writeNodeWithTracking('', value, { merge: false, transaction }) } - - if (options && typeof options.assert_revision !== 'undefined') { - return this.getNodeInfo(path, { tid: lock.tid }) - .then(info => { - if (info.revision !== options.assert_revision) { - throw new NodeRevisionError(`revision '${info.revision}' does not match requested revision '${options.assert_revision}'`); - } - if (info.address && info.address.path === path && !this.valueFitsInline(value)) { - // Overwrite node - return this._writeNodeWithTracking(path, value, { merge: false, tid }); - } - else { - // Update parent node - return lock.moveToParent() - .then(parentLock => { - lock = parentLock; - return this._writeNodeWithTracking(pathInfo.parentPath, { [pathInfo.key]: value }, { merge: true, tid }); - }); - } - }) + else if ( typeof options.assert_revision !== 'undefined') { + const info = await this.getNodeInfo(path, { transaction }) + // .then(info => { + if (info.revision !== options.assert_revision) { + throw new NodeRevisionError(`revision '${info.revision}' does not match requested revision '${options.assert_revision}'`); + } + if (info.address && info.address.path === path && !this.valueFitsInline(value)) { + // Overwrite node + await this._writeNodeWithTracking(path, value, { merge: false, transaction }); + } + else { + // Update parent node + // return lock.moveToParent() + // .then(parentLock => { + // lock = parentLock; + const lockPath = await transaction.moveToParentPath(pathInfo.parentPath); + console.assert(lockPath === pathInfo.parentPath, `transaction.moveToParentPath() did not move to the right parent path of "${path}"`) + await this._writeNodeWithTracking(pathInfo.parentPath, { [pathInfo.key]: value }, { merge: true, transaction }); + // }); + } + // }) } else { // Delegate operation to update on parent node - return lock.moveToParent() - .then(parentLock => { - lock = parentLock; - return this.updateNode(pathInfo.parentPath, { [pathInfo.key]: value }, { tid }); - }); + // return lock.moveToParent() + // .then(parentLock => { + // lock = parentLock; + const lockPath = await transaction.moveToParentPath(pathInfo.parentPath); + console.assert(lockPath === pathInfo.parentPath, `transaction.moveToParentPath() did not move to the right parent path of "${path}"`) + await this.updateNode(pathInfo.parentPath, { [pathInfo.key]: value }, { transaction }); + // }); + } + if (!options.transaction) { + // transaction was created by us, commit + await transaction.commit(); + } + } + catch (err) { + if (!options.transaction) { + // transaction was created by us, rollback + await transaction.rollback(err); } - }) - .then(result => { - lock.release(); - return result; - }) - .catch(err => { - lock.release(); throw err; - }); + } + // }) + // .then(result => { + // lock.release(); + // return result; + // }) + // .catch(err => { + // lock.release(); + // throw err; + // }); } // TODO: Move to Storage base class? - updateNode(path, updates, options = { tid: undefined }) { + /** + * + * @param {string} path + * @param {*} updates + * @param {object} [options] + * @param {CustomStorageTransaction} [options.transaction] + */ + async updateNode(path, updates, options) { if (typeof updates !== 'object') { // || Object.keys(updates).length === 0 - return Promise.reject(new Error(`invalid updates argument`)); //. Must be a non-empty object or array + throw new Error(`invalid updates argument`); //. Must be a non-empty object or array } - const tid = (options && options.tid) || ID.generate(); - let lock; - return this.nodeLocker.lock(path, tid, true, 'updateNode') - .then(l => { - lock = l; + options = options || {}; + const transaction = options.transaction || await this._customImplementation.getTransaction({ path, write: true }); + // const tid = (options && options.tid) || ID.generate(); + // let lock; + // return this.nodeLocker.lock(path, tid, true, 'updateNode') + // .then(l => { + // lock = l; + + try { // Get info about current node - return this.getNodeInfo(path, { tid: lock.tid }); - }) - .then(nodeInfo => { + const nodeInfo = await this.getNodeInfo(path, { transaction }); + + // }) + // .then(nodeInfo => { + const pathInfo = PathInfo.get(path); if (nodeInfo.exists && nodeInfo.address && nodeInfo.address.path === path) { // Node exists and is stored in its own record. // Update it - return this._writeNodeWithTracking(path, updates, { merge: true, tid }); + await this._writeNodeWithTracking(path, updates, { transaction, merge: true }); } else if (nodeInfo.exists) { // Node exists, but is stored in its parent node. const pathInfo = PathInfo.get(path); - return lock.moveToParent() - .then(parentLock => { - lock = parentLock; - return this._writeNodeWithTracking(pathInfo.parentPath, { [pathInfo.key]: value }, { merge: true, tid }); - }); + // return lock.moveToParent() + // .then(parentLock => { + // lock = parentLock; + const lockPath = await transaction.moveToParentPath(pathInfo.parentPath); + console.assert(lockPath === pathInfo.parentPath, `transaction.moveToParentPath() did not move to the right parent path of "${path}"`) + await this._writeNodeWithTracking(pathInfo.parentPath, { [pathInfo.key]: value }, { transaction, merge: true }); + // }); } else { // The node does not exist, it's parent doesn't have it either. Update the parent instead - return lock.moveToParent() - .then(parentLock => { - lock = parentLock; - return this.updateNode(pathInfo.parentPath, { [pathInfo.key]: updates }, { tid }); - }); + // return lock.moveToParent() + // .then(parentLock => { + // lock = parentLock; + const lockPath = await transaction.moveToParentPath(pathInfo.parentPath); + console.assert(lockPath === pathInfo.parentPath, `transaction.moveToParentPath() did not move to the right parent path of "${path}"`) + await this.updateNode(pathInfo.parentPath, { [pathInfo.key]: updates }, { transaction }); + // }); + } + if (!options.transaction) { + // transaction was created by us, commit + await transaction.commit(); + } + } + catch (err) { + if (!options.transaction) { + // transaction was created by us, rollback + await transaction.rollback(err); } - }) - .then(result => { - lock.release(); - return result; - }) - .catch(err => { - lock.release(); throw err; - }); + } + + // }) + // .then(result => { + // lock.release(); + // return result; + // }) + // .catch(err => { + // lock.release(); + // throw err; + // }); } } @@ -1196,6 +1445,7 @@ module.exports = { CustomStorage, CustomStorageSettings, CustomStorageHelpers, + CustomStorageTransaction, ICustomStorageNodeMetaData, ICustomStorageNode } \ No newline at end of file diff --git a/src/storage.js b/src/storage.js index 9321a2c..1ccaac2 100644 --- a/src/storage.js +++ b/src/storage.js @@ -606,10 +606,12 @@ class Storage extends EventEmitter { * @param {object} [options] * @returns {Promise} */ - _writeNodeWithTracking(path, value, options = { merge: false, tid: undefined, _customWriteFunction: undefined, waitForIndexUpdates: true }) { - if (!options || !options.tid) { throw new Error(`_writeNodeWithTracking MUST be executed with a tid!`); } + _writeNodeWithTracking(path, value, options = { merge: false, transaction: undefined, tid: undefined, _customWriteFunction: undefined, waitForIndexUpdates: true }) { + options = options || {}; + if (!options.tid && !options.transaction) { throw new Error(`_writeNodeWithTracking MUST be executed with a tid OR transaction!`); } options.merge = options.merge === true; const tid = options.tid; + const transaction = options.transaction; // Is anyone interested in the values changing on this path? let topEventData = null; @@ -708,13 +710,13 @@ class Storage extends EventEmitter { } } - return this.getNodeInfo(topEventPath, { tid }) + return this.getNodeInfo(topEventPath, { transaction, tid }) .then(eventNodeInfo => { if (!eventNodeInfo.exists) { // Node doesn't exist return null; } - let valueOptions = { tid }; + let valueOptions = { transaction, tid }; // if (!hasValueSubscribers && options.merge === true) { // // Only load current value for properties being updated // valueOptions.include = Object.keys(value);