diff --git a/dist/browser.js b/dist/browser.js index 0a82cba..33b8fa2 100644 --- a/dist/browser.js +++ b/dist/browser.js @@ -1,12183 +1,12183 @@ (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 { + // console.log(`database "${dbname}" (${this.constructor.name}) is ready to use`); + this._ready = true; + }); } /** - * Creates an AceBase database instance using IndexedDB as storage engine - * @param dbname Name of the database - * @param settings optional settings + * Waits for the database to be ready before running your callback. + * @param callback (optional) callback function that is called when the database is ready to be used. You can also use the returned promise. + * @returns returns a promise that resolves when ready */ - static WithIndexedDB(dbname, init = {}) { - return (0, indexed_db_1.createIndexedDBInstance)(dbname, init); - } -} -exports.BrowserAceBase = BrowserAceBase; - -},{"./acebase-local":2,"./storage/custom/indexed-db":22}],2:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.AceBase = exports.AceBaseLocalSettings = exports.IndexedDBStorageSettings = exports.LocalStorageSettings = void 0; -const acebase_core_1 = require("acebase-core"); -const binary_1 = require("./storage/binary"); -const api_local_1 = require("./api-local"); -const local_storage_1 = require("./storage/custom/local-storage"); -Object.defineProperty(exports, "LocalStorageSettings", { enumerable: true, get: function () { return local_storage_1.LocalStorageSettings; } }); -const settings_1 = require("./storage/custom/indexed-db/settings"); -Object.defineProperty(exports, "IndexedDBStorageSettings", { enumerable: true, get: function () { return settings_1.IndexedDBStorageSettings; } }); -class AceBaseLocalSettings extends acebase_core_1.AceBaseBaseSettings { - constructor(options = {}) { - super(options); - if (options.storage) { - this.storage = options.storage; - // If they were set on global settings, copy IPC and transaction settings to storage settings - if (options.ipc) { - this.storage.ipc = options.ipc; - } - if (options.transactions) { - this.storage.transactions = options.transactions; - } + async ready(callback) { + if (!this._ready) { + // Wait for ready event + await new Promise(resolve => this.on('ready', resolve)); } + callback === null || callback === void 0 ? void 0 : callback(); + } + get isReady() { + return this._ready; } -} -exports.AceBaseLocalSettings = AceBaseLocalSettings; -class AceBase extends acebase_core_1.AceBaseBase { /** - * @param dbname Name of the database to open or create + * Allow specific observable implementation to be used + * @param ObservableImpl Implementation to use */ - constructor(dbname, init = {}) { - const settings = new AceBaseLocalSettings(init); - super(dbname, settings); - this.recovery = { + setObservable(ObservableImpl) { + (0, optional_observable_1.setObservable)(ObservableImpl); + } + /** + * Creates a reference to a node + * @param path + * @returns reference to the requested node + */ + ref(path) { + return new data_reference_1.DataReference(this, path); + } + /** + * Get a reference to the root database node + * @returns reference to root node + */ + get root() { + return this.ref(''); + } + /** + * Creates a query on the requested node + * @param path + * @returns query for the requested node + */ + query(path) { + const ref = new data_reference_1.DataReference(this, path); + return new data_reference_1.DataReferenceQuery(ref); + } + get indexes() { + return { /** - * Repairs a node that cannot be loaded by removing the reference from its parent, or marking it as removed + * Gets all indexes */ - repairNode: async (path, options) => { - await this.ready(); - if (this.api.storage instanceof binary_1.AceBaseStorage) { - await this.api.storage.repairNode(path, options); - } - else if (!this.api.storage.repairNode) { - throw new Error(`repairNode is not supported with chosen storage engine`); - } + get: () => { + return this.api.getIndexes(); }, /** - * Repairs a node that uses a B+Tree for its keys (100+ children). - * See https://github.com/appy-one/acebase/issues/183 - * @param path Target path to fix + * Creates an index on "key" for all child nodes at "path". If the index already exists, nothing happens. + * Example: creating an index on all "name" keys of child objects of path "system/users", + * will index "system/users/user1/name", "system/users/user2/name" etc. + * You can also use wildcard paths to enable indexing and quering of fragmented data. + * Example: path "users/*\/posts", key "title": will index all "title" keys in all posts of all users. + * @param path path to the container node + * @param key name of the key to index every container child node + * @param options any additional options */ - repairNodeTree: async (path) => { - await this.ready(); - const storage = this.api.storage; - await storage.repairNodeTree(path); + create: (path, key, options) => { + return this.api.createIndex(path, key, options); + }, + /** + * Deletes an existing index from the database + */ + delete: async (filePath) => { + return this.api.deleteIndex(filePath); }, }; - const apiSettings = { - db: this, - settings, - }; - this.api = new api_local_1.LocalApi(dbname, apiSettings, () => { - this.emit('ready'); - }); - } - async close() { - // Close the database by calling exit on the ipc channel, which will emit an 'exit' event when the database can be safely closed. - await this.api.storage.close(); } - get settings() { - const ipc = this.api.storage.ipc, debug = this.debug; + get schema() { return { - get logLevel() { return debug.level; }, - set logLevel(level) { debug.setLevel(level); }, - get ipcEvents() { return ipc.eventsEnabled; }, - set ipcEvents(enabled) { ipc.eventsEnabled = enabled; }, - }; - } - /** - * Creates an AceBase database instance using LocalStorage or SessionStorage as storage engine. When running in non-browser environments, set - * settings.provider to a custom LocalStorage provider, eg 'node-localstorage' - * @param dbname Name of the database - * @param settings optional settings - */ - static WithLocalStorage(dbname, settings = {}) { - const db = (0, local_storage_1.createLocalStorageInstance)(dbname, settings); - return db; - } - /** - * Creates an AceBase database instance using IndexedDB as storage engine. Only available in browser contexts! - * @param dbname Name of the database - * @param settings optional settings - */ - static WithIndexedDB(dbname, init = {}) { - throw new Error(`IndexedDB storage can only be used in browser contexts`); + get: (path) => { + return this.api.getSchema(path); + }, + set: (path, schema, warnOnly = false) => { + return this.api.setSchema(path, schema, warnOnly); + }, + all: () => { + return this.api.getSchemas(); + }, + check: (path, value, isUpdate) => { + return this.api.validateSchema(path, value, isUpdate); + }, + }; } } -exports.AceBase = AceBase; +exports.AceBaseBase = AceBaseBase; -},{"./api-local":3,"./storage/binary":18,"./storage/custom/indexed-db/settings":23,"./storage/custom/local-storage":25,"acebase-core":46}],3:[function(require,module,exports){ +},{"./data-reference":8,"./debug":10,"./optional-observable":14,"./simple-colors":21,"./simple-event-emitter":22,"./type-mappings":26}],2:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.LocalApi = void 0; -const acebase_core_1 = require("acebase-core"); -const binary_1 = require("./storage/binary"); -const sqlite_1 = require("./storage/sqlite"); -const mssql_1 = require("./storage/mssql"); -const custom_1 = require("./storage/custom"); -const node_value_types_1 = require("./node-value-types"); -const query_1 = require("./query"); -const node_errors_1 = require("./node-errors"); -class LocalApi extends acebase_core_1.Api { - constructor(dbname = 'default', init, readyCallback) { +exports.Api = void 0; +/* eslint-disable @typescript-eslint/no-unused-vars */ +const simple_event_emitter_1 = require("./simple-event-emitter"); +class NotImplementedError extends Error { + constructor(name) { super(`${name} is not implemented`); } +} +/** + * Refactor to type/interface once acebase and acebase-client have been ported to TS + */ +class Api extends simple_event_emitter_1.SimpleEventEmitter { + constructor() { super(); - this.db = init.db; - this.logger = init.db.logger; - const storageEnv = { logLevel: init.settings.logLevel, logColors: init.settings.logColors, logger: init.settings.logger }; - if (typeof init.settings.storage === 'object') { - // settings.storage.logLevel = settings.logLevel; - if (sqlite_1.SQLiteStorageSettings && (init.settings.storage instanceof sqlite_1.SQLiteStorageSettings)) { // || env.settings.storage.type === 'sqlite' - this.storage = new sqlite_1.SQLiteStorage(dbname, init.settings.storage, storageEnv); - } - else if (mssql_1.MSSQLStorageSettings && (init.settings.storage instanceof mssql_1.MSSQLStorageSettings)) { // || env.settings.storage.type === 'mssql' - this.storage = new mssql_1.MSSQLStorage(dbname, init.settings.storage, storageEnv); - } - else if (custom_1.CustomStorageSettings && (init.settings.storage instanceof custom_1.CustomStorageSettings)) { // || settings.storage.type === 'custom' - this.storage = new custom_1.CustomStorage(dbname, init.settings.storage, storageEnv); - } - else { - const storageSettings = init.settings.storage instanceof binary_1.AceBaseStorageSettings - ? init.settings.storage - : new binary_1.AceBaseStorageSettings(init.settings.storage); - this.storage = new binary_1.AceBaseStorage(dbname, storageSettings, storageEnv); - } - } - else { - this.storage = new binary_1.AceBaseStorage(dbname, new binary_1.AceBaseStorageSettings(), storageEnv); - } - this.storage.on('ready', readyCallback); - } - async stats(options) { - return this.storage.stats; - } - subscribe(path, event, callback) { - this.storage.subscriptions.add(path, event, callback); - } - unsubscribe(path, event, callback) { - this.storage.subscriptions.remove(path, event, callback); - } - /** - * Creates a new node or overwrites an existing node - * @param path - * @param value Any value will do. If the value is small enough to be stored in a parent record, it will take care of it - * @returns returns a promise with the new cursor (if transaction logging is enabled) - */ - async set(path, value, options = { - suppress_events: false, - context: null, - }) { - const cursor = await this.storage.setNode(path, value, { suppress_events: options.suppress_events, context: options.context }); - return Object.assign({}, (cursor && { cursor })); } /** - * Updates an existing node, or creates a new node. - * @returns returns a promise with the new cursor (if transaction logging is enabled) + * Provides statistics + * @param options */ - async update(path, updates, options = { - suppress_events: false, - context: null, - }) { - const cursor = await this.storage.updateNode(path, updates, { suppress_events: options.suppress_events, context: options.context }); - return Object.assign({}, (cursor && { cursor })); - } - get transactionLoggingEnabled() { - return this.storage.settings.transactions && this.storage.settings.transactions.log === true; - } + stats(options) { throw new NotImplementedError('stats'); } /** - * Gets the value of a node - * @param options when omitted retrieves all nested data. If `include` is set to an array of keys it will only return those children. - * If `exclude` is set to an array of keys, those values will not be included + * @param path + * @param event event to subscribe to ("value", "child_added" etc) + * @param callback callback function */ - async get(path, options) { - 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`); + subscribe(path, event, callback, settings) { throw new NotImplementedError('subscribe'); } + unsubscribe(path, event, callback) { throw new NotImplementedError('unsubscribe'); } + update(path, updates, options) { throw new NotImplementedError('update'); } + set(path, value, options) { throw new NotImplementedError('set'); } + get(path, options) { throw new NotImplementedError('get'); } + transaction(path, callback, options) { throw new NotImplementedError('transaction'); } + exists(path) { throw new NotImplementedError('exists'); } + query(path, query, options) { throw new NotImplementedError('query'); } + reflect(path, type, args) { throw new NotImplementedError('reflect'); } + export(path, write, options) { throw new NotImplementedError('export'); } + import(path, read, options) { throw new NotImplementedError('import'); } + /** Creates an index on key for all child nodes at path */ + createIndex(path, key, options) { throw new NotImplementedError('createIndex'); } + getIndexes() { throw new NotImplementedError('getIndexes'); } + deleteIndex(filePath) { throw new NotImplementedError('deleteIndex'); } + setSchema(path, schema, warnOnly) { throw new NotImplementedError('setSchema'); } + getSchema(path) { throw new NotImplementedError('getSchema'); } + getSchemas() { throw new NotImplementedError('getSchemas'); } + validateSchema(path, value, isUpdate) { throw new NotImplementedError('validateSchema'); } + getMutations(filter) { throw new NotImplementedError('getMutations'); } + getChanges(filter) { throw new NotImplementedError('getChanges'); } +} +exports.Api = Api; + +},{"./simple-event-emitter":22}],3:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ascii85 = void 0; +function c(input, length, result) { + const b = [0, 0, 0, 0, 0]; + for (let i = 0; i < length; i += 4) { + let n = ((input[i] * 256 + input[i + 1]) * 256 + input[i + 2]) * 256 + input[i + 3]; + if (!n) { + result.push('z'); } - if (['undefined', 'boolean'].indexOf(typeof options.child_objects) < 0) { - throw new TypeError(`options.child_objects must be a boolean`); + else { + for (let j = 0; j < 5; b[j++] = n % 85 + 33, n = Math.floor(n / 85)) { } + result.push(String.fromCharCode(b[4], b[3], b[2], b[1], b[0])); } - const node = await this.storage.getNode(path, options); - return { value: node.value, context: { acebase_cursor: node.cursor }, cursor: node.cursor }; - } - /** - * Performs a transaction on a Node - * @param path - * @param callback callback is called with the current value. The returned value (or promise) will be used as the new value. When the callbacks returns undefined, the transaction will be canceled. When callback returns null, the node will be removed. - * @returns returns a promise with the new cursor (if transaction logging is enabled) - */ - async transaction(path, callback, options = { - suppress_events: false, - context: null, - }) { - const cursor = await this.storage.transactNode(path, callback, { suppress_events: options.suppress_events, context: options.context }); - return Object.assign({}, (cursor && { cursor })); - } - async exists(path) { - const nodeInfo = await this.storage.getNodeInfo(path); - return nodeInfo.exists; - } - // query2(path, query, options = { snapshots: false, include: undefined, exclude: undefined, child_objects: undefined }) { - // /* - // Now that we're using indexes to filter data and order upon, each query requires a different strategy - // to get the results the quickest. - // So, we'll analyze the query first, build a strategy and then execute the strategy - // Analyze stage: - // - what path is being queried (wildcard path or single parent) - // - which indexes are available for the path - // - which indexes can be used for filtering - // - which indexes can be used for sorting - // - is take/skip used to limit the result set - // Strategy stage: - // - chain index filtering - // - .... - // TODO! - // */ - // } - /** - * @returns Returns a promise that resolves with matching data or paths in `results` - */ - async query(path, query, options = { snapshots: false }) { - const results = await (0, query_1.executeQuery)(this, path, query, options); - return results; - } - /** - * Creates an index on key for all child nodes at path - */ - createIndex(path, key, options) { - return this.storage.indexes.create(path, key, options); - } - /** - * Gets all indexes - */ - async getIndexes() { - return this.storage.indexes.list(); } - /** - * Deletes an existing index from the database - */ - async deleteIndex(filePath) { - return this.storage.indexes.delete(filePath); +} +function encode(arr) { + // summary: encodes input data in ascii85 string + // input: ArrayLike + const input = arr, result = [], remainder = input.length % 4, length = input.length - remainder; + c(input, length, result); + if (remainder) { + const t = new Uint8Array(4); + t.set(input.slice(length), 0); + c(t, 4, result); + let x = result.pop(); + if (x == 'z') { + x = '!!!!!'; + } + result.push(x.substr(0, remainder + 1)); } - async reflect(path, type, args) { - args = args || {}; - const getChildren = async (path, limit = 50, skip = 0, from = null) => { - if (typeof limit === 'string') { - limit = parseInt(limit); + let ret = result.join(''); // String + ret = '<~' + ret + '~>'; + return ret; +} +exports.ascii85 = { + encode: function (arr) { + if (arr instanceof ArrayBuffer) { + arr = new Uint8Array(arr, 0, arr.byteLength); + } + return encode(arr); + }, + decode: function (input) { + // summary: decodes the input string back to an ArrayBuffer + // input: String: the input string to decode + if (!input.startsWith('<~') || !input.endsWith('~>')) { + throw new Error('Invalid input string'); + } + input = input.substr(2, input.length - 4); + const n = input.length, r = [], b = [0, 0, 0, 0, 0]; + let t, x, y, d; + for (let i = 0; i < n; ++i) { + if (input.charAt(i) == 'z') { + r.push(0, 0, 0, 0); + continue; } - if (typeof skip === 'string') { - skip = parseInt(skip); + for (let j = 0; j < 5; ++j) { + b[j] = input.charCodeAt(i + j) - 33; } - const children = []; // Array<{ key: string | number; type: string; value: any; address?: any }>; - let n = 0, stop = false, more = false; //stop = skip + limit, - await this.storage.getChildren(path, Object.assign({}, (['number', 'string'].includes(typeof from) && { fromKey: from }))) - .next(childInfo => { - if (stop) { - // Stop 1 child too late on purpose to make sure there's more - more = true; - return false; // Stop iterating - } - n++; - // const include = from !== null ? childInfo.key > from : skip === 0 || n > skip; - const include = skip === 0 || n > skip; - if (include) { - children.push(Object.assign({ key: typeof childInfo.key === 'string' ? childInfo.key : childInfo.index, type: childInfo.valueTypeName, value: childInfo.value }, (typeof childInfo.address === 'object' && 'pageNr' in childInfo.address && { - address: { - pageNr: childInfo.address.pageNr, - recordNr: childInfo.address.recordNr, - }, - }))); - } - stop = limit > 0 && children.length === limit; // flag, but don't stop now. Otherwise we won't know if there's more - }) - .catch(err => { - // Node doesn't exist? No children.. - if (!(err instanceof node_errors_1.NodeNotFoundError)) { - throw err; - } - }); - return { - more, - list: children, - }; - }; - switch (type) { - case 'children': { - const result = await getChildren(path, args.limit, args.skip, args.from); - return result; - } - case 'info': { - const info = { - key: '', - exists: false, - type: 'unknown', - value: undefined, - address: undefined, - children: { - count: 0, - more: false, - list: [], - }, - }; - const nodeInfo = await this.storage.getNodeInfo(path, { include_child_count: args.child_count === true }); - info.key = typeof nodeInfo.key !== 'undefined' ? nodeInfo.key : nodeInfo.index; - info.exists = nodeInfo.exists; - info.type = nodeInfo.exists ? nodeInfo.valueTypeName : undefined; - info.value = nodeInfo.value; - info.address = typeof nodeInfo.address === 'object' && 'pageNr' in nodeInfo.address - ? { - pageNr: nodeInfo.address.pageNr, - recordNr: nodeInfo.address.recordNr, - } - : undefined; - const isObjectOrArray = nodeInfo.exists && nodeInfo.address && [node_value_types_1.VALUE_TYPES.OBJECT, node_value_types_1.VALUE_TYPES.ARRAY].includes(nodeInfo.type); - if (args.child_count === true) { - // set child count instead of enumerating - info.children = { count: isObjectOrArray ? nodeInfo.childCount : 0 }; - } - else if (typeof args.child_limit === 'number' && args.child_limit > 0) { - if (isObjectOrArray) { - info.children = await getChildren(path, args.child_limit, args.child_skip, args.child_from); - } - } - return info; + d = n - i; + if (d < 5) { + for (let j = d; j < 4; b[++j] = 0) { } + b[d] = 85; } + t = (((b[0] * 85 + b[1]) * 85 + b[2]) * 85 + b[3]) * 85 + b[4]; + x = t & 255; + t >>>= 8; + y = t & 255; + t >>>= 8; + r.push(t >>> 8, t & 255, y, x); + for (let j = d; j < 5; ++j, r.pop()) { } + i += 4; } - } - export(path, stream, options = { - format: 'json', - type_safe: true, - }) { - return this.storage.exportNode(path, stream, options); - } - import(path, read, options = { - format: 'json', - suppress_events: false, - method: 'set', - }) { - return this.storage.importNode(path, read, options); - } - async setSchema(path, schema, warnOnly = false) { - return this.storage.setSchema(path, schema, warnOnly); - } - async getSchema(path) { - return this.storage.getSchema(path); - } - async getSchemas() { - return this.storage.getSchemas(); - } - async validateSchema(path, value, isUpdate) { - return this.storage.validateSchema(path, value, { updates: isUpdate }); - } - /** - * Gets all relevant mutations for specific events on a path and since specified cursor - */ - async getMutations(filter) { - if (typeof this.storage.getMutations !== 'function') { - throw new Error('Used storage type does not support getMutations'); - } - if (typeof filter !== 'object') { - throw new Error('No filter specified'); - } - if (typeof filter.cursor !== 'string' && typeof filter.timestamp !== 'number') { - throw new Error('No cursor or timestamp given'); - } - return this.storage.getMutations(filter); - } - /** - * Gets all relevant effective changes for specific events on a path and since specified cursor - */ - async getChanges(filter) { - if (typeof this.storage.getChanges !== 'function') { - throw new Error('Used storage type does not support getChanges'); - } - if (typeof filter !== 'object') { - throw new Error('No filter specified'); - } - if (typeof filter.cursor !== 'string' && typeof filter.timestamp !== 'number') { - throw new Error('No cursor or timestamp given'); - } - return this.storage.getChanges(filter); - } -} -exports.LocalApi = LocalApi; + const data = new Uint8Array(r); + return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength); + }, +}; -},{"./node-errors":11,"./node-value-types":14,"./query":17,"./storage/binary":18,"./storage/custom":21,"./storage/mssql":31,"./storage/sqlite":32,"acebase-core":46}],4:[function(require,module,exports){ +},{}],4:[function(require,module,exports){ "use strict"; +var _a, _b; Object.defineProperty(exports, "__esModule", { value: true }); -exports.assert = void 0; +const pad_1 = require("../pad"); +const env = typeof window === 'object' ? window : self, globalCount = Object.keys(env).length, mimeTypesLength = (_b = (_a = navigator.mimeTypes) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0, clientId = (0, pad_1.default)((mimeTypesLength + + navigator.userAgent.length).toString(36) + + globalCount.toString(36), 4); +function fingerprint() { + return clientId; +} +exports.default = fingerprint; + +},{"../pad":6}],5:[function(require,module,exports){ +"use strict"; /** -* Replacement for console.assert, throws an error if condition is not met. -* @param condition 'truthy' condition -* @param error -*/ -function assert(condition, error) { - if (!condition) { - throw new Error(`Assertion failed: ${error !== null && error !== void 0 ? error : 'check your code'}`); - } + * cuid.js + * Collision-resistant UID generator for browsers and node. + * Sequential for fast db lookups and recency sorting. + * Safe for element IDs and server-side lookups. + * + * Extracted from CLCTR + * + * Copyright (c) Eric Elliott 2012 + * MIT License + * + * time biasing added by Ewout Stortenbeker for AceBase + */ +Object.defineProperty(exports, "__esModule", { value: true }); +const fingerprint_1 = require("./fingerprint"); +const pad_1 = require("./pad"); +let c = 0; +const blockSize = 4, base = 36, discreteValues = Math.pow(base, blockSize); +function randomBlock() { + return (0, pad_1.default)((Math.random() * discreteValues << 0).toString(base), blockSize); } -exports.assert = assert; +function safeCounter() { + c = c < discreteValues ? c : 0; + c++; // this is not subliminal + return c - 1; +} +function cuid(timebias = 0) { + // Starting with a lowercase letter makes + // it HTML element ID friendly. + const letter = 'c', // hard-coded allows for sequential access + // timestamp + // warning: this exposes the exact date and time + // that the uid was created. + // NOTES Ewout: + // - added timebias + // - at '2059/05/25 19:38:27.456', timestamp will become 1 character larger! + timestamp = (new Date().getTime() + timebias).toString(base), + // Prevent same-machine collisions. + counter = (0, pad_1.default)(safeCounter().toString(base), blockSize), + // A few chars to generate distinct ids for different + // clients (so different computers are far less + // likely to generate the same id) + print = (0, fingerprint_1.default)(), + // Grab some more chars from Math.random() + random = randomBlock() + randomBlock(); + return letter + timestamp + counter + print + random; +} +exports.default = cuid; +// Not using slugs, removed code -},{}],5:[function(require,module,exports){ +},{"./fingerprint":4,"./pad":6}],6:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.AsyncTaskBatch = void 0; -class AsyncTaskBatch { +function pad(num, size) { + const s = '000000000' + num; + return s.substr(s.length - size); +} +exports.default = pad; + +},{}],7:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.OrderedCollectionProxy = exports.proxyAccess = exports.LiveDataProxy = void 0; +const utils_1 = require("./utils"); +const data_reference_1 = require("./data-reference"); +const data_snapshot_1 = require("./data-snapshot"); +const path_reference_1 = require("./path-reference"); +const id_1 = require("./id"); +const optional_observable_1 = require("./optional-observable"); +const process_1 = require("./process"); +const path_info_1 = require("./path-info"); +const simple_event_emitter_1 = require("./simple-event-emitter"); +class RelativeNodeTarget extends Array { + static areEqual(t1, t2) { + return t1.length === t2.length && t1.every((key, i) => t2[i] === key); + } + static isAncestor(ancestor, other) { + return ancestor.length < other.length && ancestor.every((key, i) => other[i] === key); + } + static isDescendant(descendant, other) { + return descendant.length > other.length && other.every((key, i) => descendant[i] === key); + } +} +const isProxy = Symbol('isProxy'); +class LiveDataProxy { /** - * Creates a new batch: runs a maximum amount of async tasks simultaniously and waits until they are all resolved. - * If all tasks succeed, returns the results in the same order tasks were added (like `Promise.all` would do), but - * cancels any waiting tasks upon failure of one task. Note that the execution order of tasks added after the set - * limit is unknown. - * @param limit Max amount of async functions to execute simultaniously. Default is `1000` - * @param options Additional options + * Creates a live data proxy for the given reference. The data of the reference's path will be loaded, and kept in-sync + * with live data by listening for 'mutations' events. Any changes made to the value by the client will be synced back + * to the database. + * @param ref DataReference to create proxy for. + * @param options proxy initialization options + * be written to the database. */ - constructor(limit = 1000, options) { - this.limit = limit; - this.options = options; - this.added = 0; - this.scheduled = []; - this.running = 0; - this.results = []; - this.done = false; - } - async execute(task, index) { - var _a, _b; - try { - this.running++; - const result = await task(); - this.results[index] = result; - this.running--; - if (this.running === 0 && this.scheduled.length === 0) { - // Finished - this.done = true; - (_a = this.doneCallback) === null || _a === void 0 ? void 0 : _a.call(this, this.results); - } - else if (this.scheduled.length > 0) { - // Run next scheduled task - const next = this.scheduled.shift(); - this.execute(next.task, next.index); - } - } - catch (err) { - this.done = true; - (_b = this.errorCallback) === null || _b === void 0 ? void 0 : _b.call(this, err); - } - } - add(task) { + static async create(ref, options) { var _a; - if (this.done) { - throw new Error(`Cannot add to a batch that has already finished. Use wait option and start batch processing manually if you are adding tasks in an async loop`); - } - const index = this.added++; - if (((_a = this.options) === null || _a === void 0 ? void 0 : _a.wait) !== true && this.running < this.limit) { - this.execute(task, index); - } - else { - this.scheduled.push({ task, index }); - } - } - /** - * Manually starts batch processing, mus be done if the `wait` option was used - */ - start() { - while (this.running < this.limit) { - const next = this.scheduled.shift(); - this.execute(next.task, next.index); - } - } - async finish() { - if (this.running === 0 && this.scheduled.length === 0) { - return this.results; - } - await new Promise((resolve, reject) => { - this.doneCallback = resolve; - this.errorCallback = reject; - }); - return this.results; - } -} -exports.AsyncTaskBatch = AsyncTaskBatch; - -},{}],6:[function(require,module,exports){ -"use strict"; -/** - ________________________________________________________________________________ - - ___ ______ - / _ \ | ___ \ - / /_\ \ ___ ___| |_/ / __ _ ___ ___ - | _ |/ __/ _ \ ___ \/ _` / __|/ _ \ - | | | | (_| __/ |_/ / (_| \__ \ __/ - \_| |_/\___\___\____/ \__,_|___/\___| - realtime database - - Copyright 2018-2022 by Ewout Stortenbeker (me@appy.one) - Published under MIT license - - See docs at https://github.com/appy-one/acebase - ________________________________________________________________________________ - -*/ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.SchemaValidationError = exports.StorageSettings = exports.ICustomStorageNodeMetaData = exports.ICustomStorageNode = exports.CustomStorageHelpers = exports.CustomStorageSettings = exports.CustomStorageTransaction = exports.MSSQLStorageSettings = exports.SQLiteStorageSettings = exports.AceBaseStorageSettings = exports.IndexedDBStorageSettings = exports.LocalStorageSettings = exports.AceBaseLocalSettings = exports.AceBase = exports.PartialArray = exports.proxyAccess = exports.ID = exports.ObjectCollection = exports.TypeMappings = exports.PathReference = exports.EventSubscription = exports.EventStream = exports.DataReferencesArray = exports.DataSnapshotsArray = exports.DataReference = exports.DataSnapshot = void 0; -const acebase_core_1 = require("acebase-core"); -Object.defineProperty(exports, "DataReference", { enumerable: true, get: function () { return acebase_core_1.DataReference; } }); -Object.defineProperty(exports, "DataSnapshot", { enumerable: true, get: function () { return acebase_core_1.DataSnapshot; } }); -Object.defineProperty(exports, "EventSubscription", { enumerable: true, get: function () { return acebase_core_1.EventSubscription; } }); -Object.defineProperty(exports, "PathReference", { enumerable: true, get: function () { return acebase_core_1.PathReference; } }); -Object.defineProperty(exports, "TypeMappings", { enumerable: true, get: function () { return acebase_core_1.TypeMappings; } }); -Object.defineProperty(exports, "ID", { enumerable: true, get: function () { return acebase_core_1.ID; } }); -Object.defineProperty(exports, "proxyAccess", { enumerable: true, get: function () { return acebase_core_1.proxyAccess; } }); -Object.defineProperty(exports, "DataSnapshotsArray", { enumerable: true, get: function () { return acebase_core_1.DataSnapshotsArray; } }); -Object.defineProperty(exports, "ObjectCollection", { enumerable: true, get: function () { return acebase_core_1.ObjectCollection; } }); -Object.defineProperty(exports, "DataReferencesArray", { enumerable: true, get: function () { return acebase_core_1.DataReferencesArray; } }); -Object.defineProperty(exports, "EventStream", { enumerable: true, get: function () { return acebase_core_1.EventStream; } }); -Object.defineProperty(exports, "PartialArray", { enumerable: true, get: function () { return acebase_core_1.PartialArray; } }); -const acebase_local_1 = require("./acebase-local"); -const acebase_browser_1 = require("./acebase-browser"); -Object.defineProperty(exports, "AceBase", { enumerable: true, get: function () { return acebase_browser_1.BrowserAceBase; } }); -const custom_1 = require("./storage/custom"); -const acebase = { - AceBase: acebase_browser_1.BrowserAceBase, - AceBaseLocalSettings: acebase_local_1.AceBaseLocalSettings, - DataReference: acebase_core_1.DataReference, - DataSnapshot: acebase_core_1.DataSnapshot, - EventSubscription: acebase_core_1.EventSubscription, - PathReference: acebase_core_1.PathReference, - TypeMappings: acebase_core_1.TypeMappings, - CustomStorageSettings: custom_1.CustomStorageSettings, - CustomStorageTransaction: custom_1.CustomStorageTransaction, - CustomStorageHelpers: custom_1.CustomStorageHelpers, - ID: acebase_core_1.ID, - proxyAccess: acebase_core_1.proxyAccess, - DataSnapshotsArray: acebase_core_1.DataSnapshotsArray, -}; -if (typeof window !== 'undefined') { - // Expose classes to window.acebase: - window.acebase = acebase; - // Expose BrowserAceBase class as window.AceBase: - window.AceBase = acebase_browser_1.BrowserAceBase; -} -// Expose classes for module imports: -exports.default = acebase; -var acebase_local_2 = require("./acebase-local"); -Object.defineProperty(exports, "AceBaseLocalSettings", { enumerable: true, get: function () { return acebase_local_2.AceBaseLocalSettings; } }); -Object.defineProperty(exports, "LocalStorageSettings", { enumerable: true, get: function () { return acebase_local_2.LocalStorageSettings; } }); -Object.defineProperty(exports, "IndexedDBStorageSettings", { enumerable: true, get: function () { return acebase_local_2.IndexedDBStorageSettings; } }); -var binary_1 = require("./storage/binary"); -Object.defineProperty(exports, "AceBaseStorageSettings", { enumerable: true, get: function () { return binary_1.AceBaseStorageSettings; } }); -var sqlite_1 = require("./storage/sqlite"); -Object.defineProperty(exports, "SQLiteStorageSettings", { enumerable: true, get: function () { return sqlite_1.SQLiteStorageSettings; } }); -var mssql_1 = require("./storage/mssql"); -Object.defineProperty(exports, "MSSQLStorageSettings", { enumerable: true, get: function () { return mssql_1.MSSQLStorageSettings; } }); -var custom_2 = require("./storage/custom"); -Object.defineProperty(exports, "CustomStorageTransaction", { enumerable: true, get: function () { return custom_2.CustomStorageTransaction; } }); -Object.defineProperty(exports, "CustomStorageSettings", { enumerable: true, get: function () { return custom_2.CustomStorageSettings; } }); -Object.defineProperty(exports, "CustomStorageHelpers", { enumerable: true, get: function () { return custom_2.CustomStorageHelpers; } }); -Object.defineProperty(exports, "ICustomStorageNode", { enumerable: true, get: function () { return custom_2.ICustomStorageNode; } }); -Object.defineProperty(exports, "ICustomStorageNodeMetaData", { enumerable: true, get: function () { return custom_2.ICustomStorageNodeMetaData; } }); -var storage_1 = require("./storage"); -Object.defineProperty(exports, "StorageSettings", { enumerable: true, get: function () { return storage_1.StorageSettings; } }); -Object.defineProperty(exports, "SchemaValidationError", { enumerable: true, get: function () { return storage_1.SchemaValidationError; } }); - -},{"./acebase-browser":1,"./acebase-local":2,"./storage":29,"./storage/binary":18,"./storage/custom":21,"./storage/mssql":31,"./storage/sqlite":32,"acebase-core":46}],7:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.ArrayIndex = exports.GeoIndex = exports.FullTextIndex = exports.DataIndex = void 0; -const not_supported_1 = require("../not-supported"); -/** - * Not supported in browser context - */ -class DataIndex extends not_supported_1.NotSupported { -} -exports.DataIndex = DataIndex; -/** - * Not supported in browser context - */ -class FullTextIndex extends not_supported_1.NotSupported { -} -exports.FullTextIndex = FullTextIndex; -/** - * Not supported in browser context - */ -class GeoIndex extends not_supported_1.NotSupported { -} -exports.GeoIndex = GeoIndex; -/** - * Not supported in browser context - */ -class ArrayIndex extends not_supported_1.NotSupported { -} -exports.ArrayIndex = ArrayIndex; - -},{"../not-supported":15}],8:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.NetIPCServer = exports.IPCSocketPeer = exports.RemoteIPCPeer = exports.IPCPeer = void 0; -const acebase_core_1 = require("acebase-core"); -const ipc_1 = require("./ipc"); -const not_supported_1 = require("../not-supported"); -Object.defineProperty(exports, "RemoteIPCPeer", { enumerable: true, get: function () { return not_supported_1.NotSupported; } }); -Object.defineProperty(exports, "IPCSocketPeer", { enumerable: true, get: function () { return not_supported_1.NotSupported; } }); -Object.defineProperty(exports, "NetIPCServer", { enumerable: true, get: function () { return not_supported_1.NotSupported; } }); -/** - * Browser tabs IPC. Database changes and events will be synchronized automatically. - * Locking of resources will be done by the election of a single locking master: - * the one with the lowest id. - */ -class IPCPeer extends ipc_1.AceBaseIPCPeer { - constructor(storage) { - super(storage, acebase_core_1.ID.generate()); - this.masterPeerId = this.id; // We don't know who the master is yet... - this.ipcType = 'browser.bcc'; - // Setup process exit handler - // Monitor onbeforeunload event to say goodbye when the window is closed - addEventListener('beforeunload', () => { - this.exit(); + ref = new data_reference_1.DataReference(ref.db, ref.path); // Use copy to prevent context pollution on original reference + let cache, loaded = false; + let latestCursor = options === null || options === void 0 ? void 0 : options.cursor; + let proxy; + const proxyId = id_1.ID.generate(); //ref.push().key; + // let onMutationCallback: ProxyObserveMutationsCallback; + // let onErrorCallback: ProxyObserveErrorCallback = err => { + // console.error(err.message, err.details); + // }; + const clientSubscriptions = []; + const clientEventEmitter = new simple_event_emitter_1.SimpleEventEmitter(); + clientEventEmitter.on('cursor', (cursor) => latestCursor = cursor); + clientEventEmitter.on('error', (err) => { + console.error(err.message, err.details); }); - // Create BroadcastChannel to allow multi-tab communication - // This allows other tabs to make changes to the database, notifying us of those changes. - if (typeof BroadcastChannel !== 'undefined') { - this.channel = new BroadcastChannel(`acebase:${storage.name}`); - } - else if (typeof localStorage !== 'undefined') { - // Use localStorage as polyfill for Safari & iOS WebKit - const listeners = [null]; // first callback reserved for onmessage handler - const notImplemented = () => { throw new Error('Not implemented'); }; - this.channel = { - name: `acebase:${storage.name}`, - postMessage: (message) => { - const messageId = acebase_core_1.ID.generate(), key = `acebase:${storage.name}:${this.id}:${messageId}`, payload = JSON.stringify(acebase_core_1.Transport.serialize(message)); - // Store message, triggers 'storage' event in other tabs - localStorage.setItem(key, payload); - // Remove after 10ms - setTimeout(() => localStorage.removeItem(key), 10); - }, - set onmessage(handler) { listeners[0] = handler; }, - set onmessageerror(handler) { notImplemented(); }, - close() { notImplemented(); }, - addEventListener(event, callback) { - if (event !== 'message') { - notImplemented(); - } - listeners.push(callback); - }, - removeEventListener(event, callback) { - const i = listeners.indexOf(callback); - i >= 1 && listeners.splice(i, 1); - }, - dispatchEvent(event) { - listeners.forEach(callback => { - try { - callback && callback(event); - } - catch (err) { - console.error(err); - } - }); - return true; - }, - }; - // Listen for storage events to intercept possible messages - addEventListener('storage', event => { - const [acebase, dbname, peerId, messageId] = event.key.split(':'); - if (acebase !== 'acebase' || dbname !== storage.name || peerId === this.id || event.newValue === null) { - return; - } - const message = acebase_core_1.Transport.deserialize(JSON.parse(event.newValue)); - this.channel.dispatchEvent({ data: message }); - }); - } - else { - // No localStorage either, this is probably an old browser running in a webworker - this.logger.warn(`[BroadcastChannel] not supported`); - this.sendMessage = () => { }; - return; - } - // Monitor incoming messages - this.channel.addEventListener('message', async (event) => { - const message = event.data; - if (message.to && message.to !== this.id) { - // Message is for somebody else. Ignore - return; + const applyChange = (keys, newValue) => { + // Make changes to cache + if (keys.length === 0) { + cache = newValue; + return true; } - this.logger.trace(`[BroadcastChannel] received: `, message); - if (message.type === 'hello' && message.from < this.masterPeerId) { - // This peer was created before other peer we thought was the master - this.masterPeerId = message.from; - this.logger.info(`[BroadcastChannel] Tab ${this.masterPeerId} is the master.`); + const allowCreation = false; //cache === null; // If the proxy'd target did not exist upon load, we must allow it to be created now. + if (allowCreation) { + cache = typeof keys[0] === 'number' ? [] : {}; } - else if (message.type === 'bye' && message.from === this.masterPeerId) { - // The master tab is leaving - this.logger.info(`[BroadcastChannel] Master tab ${this.masterPeerId} is leaving`); - // Elect new master - const allPeerIds = this.peers.map(peer => peer.id).concat(this.id).filter(id => id !== this.masterPeerId); // All peers, including us, excluding the leaving master peer - this.masterPeerId = allPeerIds.sort()[0]; - this.logger.info(`[BroadcastChannel] ${this.masterPeerId === this.id ? 'We are' : `tab ${this.masterPeerId} is`} the new master. Requesting ${this._locks.length} locks (${this._locks.filter(r => !r.granted).length} pending)`); - // Let the new master take over any locks and lock requests. - const requests = this._locks.splice(0); // Copy and clear current lock requests before granted locks are requested again. - // Request previously granted locks again - await Promise.all(requests.filter(req => req.granted).map(async (req) => { - // Prevent race conditions: if the existing lock is released or moved to parent before it was - // moved to the new master peer, we'll resolve their promises after releasing/moving the new lock - let released, movedToParent; - req.lock.release = () => { return new Promise(resolve => released = resolve); }; - req.lock.moveToParent = () => { return new Promise(resolve => movedToParent = resolve); }; - // Request lock again: - const lock = await this.lock({ path: req.lock.path, write: req.lock.forWriting, tid: req.lock.tid, comment: req.lock.comment }); - if (movedToParent) { - const newLock = await lock.moveToParent(); - movedToParent(newLock); + let target = cache; + const trailKeys = keys.slice(); + while (trailKeys.length > 1) { + const key = trailKeys.shift(); + if (!(key in target)) { + if (allowCreation) { + target[key] = typeof key === 'number' ? [] : {}; } - if (released) { - await lock.release(); - released(); + else { + // Have we missed an event, or are local pending mutations creating this conflict? + return false; // Do not proceed } - })); - // Now request pending locks again - await Promise.all(requests.filter(req => !req.granted).map(async (req) => { - await this.lock(req.request); - })); + } + target = target[key]; } - return this.handleMessage(message); - }); - // // Schedule periodic "pulse" to let others know we're still around - // setInterval(() => { - // sendMessage({ from: tabId, type: 'pulse' }); - // }, 30000); - // Send hello to other peers - const helloMsg = { type: 'hello', from: this.id, data: undefined }; - this.sendMessage(helloMsg); - } - sendMessage(message) { - this.logger.trace(`[BroadcastChannel] sending: `, message); - this.channel.postMessage(message); - } -} -exports.IPCPeer = IPCPeer; - -},{"../not-supported":15,"./ipc":9,"acebase-core":46}],9:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.AceBaseIPCPeer = exports.AceBaseIPCPeerExitingError = void 0; -const acebase_core_1 = require("acebase-core"); -const node_lock_1 = require("../node-lock"); -class AceBaseIPCPeerExitingError extends Error { - constructor(message) { super(`Exiting: ${message}`); } -} -exports.AceBaseIPCPeerExitingError = AceBaseIPCPeerExitingError; -/** - * Base class for Inter Process Communication, enables vertical scaling: using more CPU's on the same machine to share workload. - * These processes will have to communicate with eachother because they are reading and writing to the same database file - */ -class AceBaseIPCPeer extends acebase_core_1.SimpleEventEmitter { - get isMaster() { return this.masterPeerId === this.id; } - constructor(storage, id, dbname = storage.name) { - super(); - this.storage = storage; - this.id = id; - this.dbname = dbname; - this.ipcType = 'ipc'; - this.ourSubscriptions = []; - this.remoteSubscriptions = []; - this.peers = []; - this._exiting = false; - this._locks = []; - this._requests = new Map(); - this._eventsEnabled = true; - this._nodeLocker = new node_lock_1.NodeLocker(storage.logger, storage.settings.lockTimeout); - this.logger = storage.logger; - // Setup db event listeners - storage.on('subscribe', (subscription) => { - // Subscription was added to db - this.logger.trace(`database subscription being added on peer ${this.id}`); - const remoteSubscription = this.remoteSubscriptions.find(sub => sub.callback === subscription.callback); - if (remoteSubscription) { - // Send ack - // return sendMessage({ type: 'subscribe_ack', from: tabId, to: remoteSubscription.for, data: { path: subscription.path, event: subscription.event } }); + const prop = trailKeys.shift(); + if (newValue === null) { + // Remove it + target instanceof Array ? target.splice(prop, 1) : delete target[prop]; + } + else { + // Set or update it + target[prop] = newValue; + } + return true; + }; + // Subscribe to mutations events on the target path + const syncFallback = async () => { + if (!loaded) { return; } - const othersAlreadyNotifying = this.ourSubscriptions.some(sub => sub.event === subscription.event && sub.path === subscription.path); - // Add subscription - this.ourSubscriptions.push(subscription); - if (othersAlreadyNotifying) { - // Same subscription as other previously added. Others already know we want to be notified + await reload(); + }; + const subscription = ref.on('mutations', { syncFallback }).subscribe(async (snap) => { + var _a; + if (!loaded) { return; } - // Request other tabs to keep us updated of this event - const message = { type: 'subscribe', from: this.id, data: { path: subscription.path, event: subscription.event } }; - this.sendMessage(message); + const context = snap.context(); + const isRemote = ((_a = context.acebase_proxy) === null || _a === void 0 ? void 0 : _a.id) !== proxyId; + if (!isRemote) { + return; // Update was done through this proxy, no need to update cache or trigger local value subscriptions + } + const mutations = snap.val(false); + const proceed = mutations.every(mutation => { + if (!applyChange(mutation.target, mutation.val)) { + return false; + } + // if (onMutationCallback) { + const changeRef = mutation.target.reduce((ref, key) => ref.child(key), ref); + const changeSnap = new data_snapshot_1.DataSnapshot(changeRef, mutation.val, false, mutation.prev, snap.context()); + // onMutationCallback(changeSnap, isRemote); // onMutationCallback uses try/catch for client callback + clientEventEmitter.emit('mutation', { snapshot: changeSnap, isRemote }); + // } + return true; + }); + if (proceed) { + clientEventEmitter.emit('cursor', context.acebase_cursor); // // NOTE: cursor is only present in mutations done remotely. For our own updates, server cursors are returned by ref.set and ref.update + localMutationsEmitter.emit('mutations', { origin: 'remote', snap }); + } + else { + console.warn(`Cached value of live data proxy on "${ref.path}" appears outdated, will be reloaded`); + await reload(); + } }); - storage.on('unsubscribe', (subscription) => { - // Subscription was removed from db - const remoteSubscription = this.remoteSubscriptions.find(sub => sub.callback === subscription.callback); - if (remoteSubscription) { - // Remove - this.remoteSubscriptions.splice(this.remoteSubscriptions.indexOf(remoteSubscription), 1); - // Send ack - // return sendMessage({ type: 'unsubscribe_ack', from: tabId, to: remoteSubscription.for, data: { path: subscription.path, event: subscription.event } }); + // Setup updating functionality: enqueue all updates, process them at next tick in the order they were issued + let processPromise = Promise.resolve(); + const mutationQueue = []; + const transactions = []; + const pushLocalMutations = async () => { + // Sync all local mutations that are not in a transaction + const mutations = []; + for (let i = 0, m = mutationQueue[0]; i < mutationQueue.length; i++, m = mutationQueue[i]) { + if (!transactions.find(t => RelativeNodeTarget.areEqual(t.target, m.target) || RelativeNodeTarget.isAncestor(t.target, m.target))) { + mutationQueue.splice(i, 1); + i--; + mutations.push(m); + } + } + if (mutations.length === 0) { return; } - this.ourSubscriptions - .filter(sub => sub.path === subscription.path && (!subscription.event || sub.event === subscription.event) && (!subscription.callback || sub.callback === subscription.callback)) - .forEach(sub => { - // Remove from our subscriptions - this.ourSubscriptions.splice(this.ourSubscriptions.indexOf(sub), 1); - // Request other tabs to stop notifying - const message = { type: 'unsubscribe', from: this.id, data: { path: sub.path, event: sub.event } }; - this.sendMessage(message); + // Add current (new) values to mutations + mutations.forEach(mutation => { + mutation.value = (0, utils_1.cloneObject)(getTargetValue(cache, mutation.target)); }); - }); - } - /** - * Requests the peer to shut down. Resolves once its locks are cleared and 'exit' event has been emitted. - * Has to be overridden by the IPC implementation to perform custom shutdown tasks - * @param code optional exit code (eg one provided by SIGINT event) - */ - async exit(code = 0) { - if (this._exiting) { - // Already exiting... - return this.once('exit'); - } - this._exiting = true; - this.logger.warn(`Received ${this.isMaster ? 'master' : 'worker ' + this.id} process exit request`); - if (this._locks.length > 0) { - this.logger.warn(`Waiting for ${this.isMaster ? 'master' : 'worker'} ${this.id} locks to clear`); - await this.once('locks-cleared'); - } - // Send "bye" - this.sayGoodbye(this.id); - this.logger.warn(`${this.isMaster ? 'Master' : 'Worker ' + this.id} will now exit`); - this.emitOnce('exit', code); - } - sayGoodbye(forPeerId) { - // Send "bye" message on their behalf - const bye = { type: 'bye', from: forPeerId, data: undefined }; - this.sendMessage(bye); - } - addPeer(id, sendReply = true) { - if (this._exiting) { - return; - } - const peer = this.peers.find(w => w.id === id); - if (!peer) { - this.peers.push({ id, lastSeen: Date.now() }); - } - if (sendReply) { - // Send hello back to sender - const helloMessage = { type: 'hello', from: this.id, to: id, data: undefined }; - this.sendMessage(helloMessage); - // Send our active subscriptions through - this.ourSubscriptions.forEach(sub => { - // Request to keep us updated - const message = { type: 'subscribe', from: this.id, to: id, data: { path: sub.path, event: sub.event } }; - this.sendMessage(message); + // Run local onMutation & onChange callbacks in the next tick + process_1.default.nextTick(() => { + // Run onMutation callback for each changed node + const context = { acebase_proxy: { id: proxyId, source: 'update' } }; + // if (onMutationCallback) { + mutations.forEach(mutation => { + const mutationRef = mutation.target.reduce((ref, key) => ref.child(key), ref); + const mutationSnap = new data_snapshot_1.DataSnapshot(mutationRef, mutation.value, false, mutation.previous, context); + // onMutationCallback(mutationSnap, false); + clientEventEmitter.emit('mutation', { snapshot: mutationSnap, isRemote: false }); + }); + // } + // Notify local subscribers + const snap = new data_snapshot_1.MutationsDataSnapshot(ref, mutations.map(m => ({ target: m.target, val: m.value, prev: m.previous })), context); + localMutationsEmitter.emit('mutations', { origin: 'local', snap }); }); - } - } - removePeer(id, ignoreUnknown = false) { - if (this._exiting) { - return; - } - const peer = this.peers.find(peer => peer.id === id); - if (!peer) { - if (!ignoreUnknown) { - throw new Error(`We are supposed to know this peer!`); - } - return; - } - this.peers.splice(this.peers.indexOf(peer), 1); - // Remove their subscriptions - const subscriptions = this.remoteSubscriptions.filter(sub => sub.for === id); - subscriptions.forEach(sub => { - // Remove & stop their subscription - this.remoteSubscriptions.splice(this.remoteSubscriptions.indexOf(sub), 1); - this.storage.subscriptions.remove(sub.path, sub.event, sub.callback); - }); - } - addRemoteSubscription(peerId, details) { - if (this._exiting) { - return; - } - // this.logger.debug(`remote subscription being added`); - if (this.remoteSubscriptions.some(sub => sub.for === peerId && sub.event === details.event && sub.path === details.path)) { - // We're already serving this event for the other peer. Ignore - return; - } - // Add remote subscription - const subscribeCallback = (err, path, val, previous, context) => { - // db triggered an event, send notification to remote subscriber - const eventMessage = { - type: 'event', - from: this.id, - to: peerId, - path: details.path, - event: details.event, - data: { - path, - val, - previous, - context, - }, - }; - this.sendMessage(eventMessage); - }; - this.remoteSubscriptions.push({ for: peerId, event: details.event, path: details.path, callback: subscribeCallback }); - this.storage.subscriptions.add(details.path, details.event, subscribeCallback); - } - cancelRemoteSubscription(peerId, details) { - // Other tab requests to remove previously subscribed event - const sub = this.remoteSubscriptions.find(sub => sub.for === peerId && sub.event === details.event && sub.path === details.event); - if (!sub) { - // We don't know this subscription so we weren't notifying in the first place. Ignore - return; - } - // Stop subscription - this.storage.subscriptions.remove(details.path, details.event, sub.callback); - } - async handleMessage(message) { - switch (message.type) { - case 'hello': return this.addPeer(message.from, message.to !== this.id); - case 'bye': return this.removePeer(message.from, true); - case 'subscribe': return this.addRemoteSubscription(message.from, message.data); - case 'unsubscribe': return this.cancelRemoteSubscription(message.from, message.data); - case 'event': { - if (!this._eventsEnabled) { - // IPC event handling is disabled for this client. Ignore message. - break; + // Update database async + // const batchId = ID.generate(); + processPromise = mutations + .reduce((mutations, m, i, arr) => { + // Only keep top path mutations to prevent unneccessary child path updates + if (!arr.some(other => RelativeNodeTarget.isAncestor(other.target, m.target))) { + mutations.push(m); } - const eventMessage = message; - const context = eventMessage.data.context || {}; - context.acebase_ipc = { type: this.ipcType, origin: eventMessage.from }; // Add IPC details - // Other peer raised an event we are monitoring - const subscriptions = this.ourSubscriptions.filter(sub => sub.event === eventMessage.event && sub.path === eventMessage.path); - subscriptions.forEach(sub => { - sub.callback(null, eventMessage.data.path, eventMessage.data.val, eventMessage.data.previous, context); - }); - break; - } - case 'lock-request': { - // Lock request sent by worker to master - if (!this.isMaster) { - throw new Error(`Workers are not supposed to receive lock requests!`); + return mutations; + }, []) + .reduce((updates, m) => { + // Prepare db updates + const target = m.target; + if (target.length === 0) { + // Overwrite this proxy's root value + updates.push({ ref, target, value: cache, type: 'set', previous: m.previous }); } - const request = message; - const result = { type: 'lock-result', id: request.id, from: this.id, to: request.from, ok: true, data: undefined }; - try { - const lock = await this.lock(request.data); - result.data = { - id: lock.id, - path: lock.path, - tid: lock.tid, - write: lock.forWriting, - expires: lock.expires, - comment: lock.comment, - }; + else { + const parentTarget = target.slice(0, -1); + const key = target.slice(-1)[0]; + const parentRef = parentTarget.reduce((ref, key) => ref.child(key), ref); + const parentUpdate = updates.find(update => update.ref.path === parentRef.path); + const cacheValue = getTargetValue(cache, target); // m.value? + const prevValue = m.previous; + if (parentUpdate) { + parentUpdate.value[key] = cacheValue; + parentUpdate.previous[key] = prevValue; + } + else { + updates.push({ ref: parentRef, target: parentTarget, value: { [key]: cacheValue }, type: 'update', previous: { [key]: prevValue } }); + } } - catch (err) { - result.ok = false; - result.reason = err.stack || err.message || err; + return updates; + }, []) + .reduce(async (promise, update /*, i, updates */) => { + // Execute db update + // i === 0 && console.log(`Proxy: processing ${updates.length} db updates to paths:`, updates.map(update => update.ref.path)); + const context = { + acebase_proxy: { + id: proxyId, + source: update.type, + // update_id: ID.generate(), + // batch_id: batchId, + // batch_updates: updates.length + }, + }; + await promise; + await update.ref + .context(context)[update.type](update.value) // .set or .update + .catch(async (err) => { + // console.warn(`Proxy could not update DB, should rollback (${update.type}) the proxy value of "${update.ref.path}" to: `, update.previous); + if (options === null || options === void 0 ? void 0 : options.shouldRollback) { + const rollback = await options.shouldRollback(err, { type: update.type, ref: update.ref, value: update.value, previous: update.previous }); + if (rollback === false) { + // Cancel rollback + return; + } + } + clientEventEmitter.emit('error', { source: 'update', message: `Error processing update of "/${ref.path}"`, details: err }); + const context = { acebase_proxy: { id: proxyId, source: 'update-rollback' } }; + const mutations = []; + if (update.type === 'set') { + setTargetValue(cache, update.target, update.previous); + const mutationSnap = new data_snapshot_1.DataSnapshot(update.ref, update.previous, false, update.value, context); + clientEventEmitter.emit('mutation', { snapshot: mutationSnap, isRemote: false }); + mutations.push({ target: update.target, val: update.previous, prev: update.value }); + } + else { + // update + Object.keys(update.previous).forEach(key => { + setTargetValue(cache, update.target.concat(key), update.previous[key]); + const mutationSnap = new data_snapshot_1.DataSnapshot(update.ref.child(key), update.previous[key], false, update.value[key], context); + clientEventEmitter.emit('mutation', { snapshot: mutationSnap, isRemote: false }); + mutations.push({ target: update.target.concat(key), val: update.previous[key], prev: update.value[key] }); + }); + } + // Run onMutation callback for each node being rolled back + mutations.forEach(m => { + const mutationRef = m.target.reduce((ref, key) => ref.child(key), ref); + const mutationSnap = new data_snapshot_1.DataSnapshot(mutationRef, m.val, false, m.prev, context); + clientEventEmitter.emit('mutation', { snapshot: mutationSnap, isRemote: false }); + }); + // Notify local subscribers: + const snap = new data_snapshot_1.MutationsDataSnapshot(update.ref, mutations, context); + localMutationsEmitter.emit('mutations', { origin: 'local', snap }); + }); + if (update.ref.cursor) { + // Should also be available in context.acebase_cursor now + clientEventEmitter.emit('cursor', update.ref.cursor); } - return this.sendMessage(result); + }, processPromise); + await processPromise; + }; + let syncInProgress = false; + const syncPromises = []; + const syncCompleted = () => { + let resolve; + const promise = new Promise(rs => resolve = rs); + syncPromises.push({ resolve }); + return promise; + }; + let processQueueTimeout = null; + const scheduleSync = () => { + if (!processQueueTimeout) { + processQueueTimeout = setTimeout(async () => { + syncInProgress = true; + processQueueTimeout = null; + await pushLocalMutations(); + syncInProgress = false; + syncPromises.splice(0).forEach(p => p.resolve()); + }, 0); } - case 'lock-result': { - // Lock result sent from master to worker - if (this.isMaster) { - throw new Error(`Masters are not supposed to receive results for lock requests!`); - } - const result = message; - const request = this._requests.get(result.id); - if (typeof request !== 'object') { - throw new Error(`The request must be known to us!`); - } - if (result.ok) { - request.resolve(result.data); - } - else { - request.reject(new Error(result.reason)); - } - return; + }; + const flagOverwritten = (target) => { + if (!mutationQueue.find(m => RelativeNodeTarget.areEqual(m.target, target))) { + mutationQueue.push({ target, previous: (0, utils_1.cloneObject)(getTargetValue(cache, target)) }); } - case 'unlock-request': { - // lock release request sent from worker to master - if (!this.isMaster) { - throw new Error(`Workers are not supposed to receive unlock requests!`); + // schedule database updates + scheduleSync(); + }; + const localMutationsEmitter = new simple_event_emitter_1.SimpleEventEmitter(); + const addOnChangeHandler = (target, callback) => { + const isObject = (val) => val !== null && typeof val === 'object'; + const mutationsHandler = async (details) => { + var _a; + const { snap, origin } = details; + const context = snap.context(); + const causedByOurProxy = ((_a = context.acebase_proxy) === null || _a === void 0 ? void 0 : _a.id) === proxyId; + if (details.origin === 'remote' && causedByOurProxy) { + // Any local changes already triggered subscription callbacks + console.error('DEV ISSUE: mutationsHandler was called from remote event originating from our own proxy'); + return; } - const request = message; - const result = { type: 'unlock-result', id: request.id, from: this.id, to: request.from, ok: true, data: { id: request.data.id } }; - try { - const lockInfo = this._locks.find(l => { var _a; return ((_a = l.lock) === null || _a === void 0 ? void 0 : _a.id) === request.data.id; }); // this._locks.get(request.data.id); - await lockInfo.lock.release(); //this.unlock(request.data.id); + const mutations = snap.val(false).filter(mutation => { + // Keep mutations impacting the subscribed target: mutations on target, or descendant or ancestor of target + return mutation.target.slice(0, target.length).every((key, i) => target[i] === key); + }); + if (mutations.length === 0) { + return; } - catch (err) { - result.ok = false; - result.reason = err.stack || err.message || err; - } - return this.sendMessage(result); - } - case 'unlock-result': { - // lock release result sent from master to worker - if (this.isMaster) { - throw new Error(`Masters are not supposed to receive results for unlock requests!`); - } - const result = message; - const request = this._requests.get(result.id); - if (typeof request !== 'object') { - throw new Error(`The request must be known to us!`); - } - if (result.ok) { - request.resolve(result.data); + let newValue, previousValue; + // If there is a mutation on the target itself, or parent/ancestor path, there can only be one. We can take a shortcut + const singleMutation = mutations.find(m => m.target.length <= target.length); + if (singleMutation) { + const trailKeys = target.slice(singleMutation.target.length); + newValue = trailKeys.reduce((val, key) => !isObject(val) || !(key in val) ? null : val[key], singleMutation.val); + previousValue = trailKeys.reduce((val, key) => !isObject(val) || !(key in val) ? null : val[key], singleMutation.prev); } else { - request.reject(new Error(result.reason)); - } - return; - } - case 'move-lock-request': { - // move lock request sent from worker to master - if (!this.isMaster) { - throw new Error(`Workers are not supposed to receive move lock requests!`); + // All mutations are on children/descendants of our target + // Construct new & previous values by combining cache and snapshot + const currentValue = getTargetValue(cache, target); + newValue = (0, utils_1.cloneObject)(currentValue); + previousValue = (0, utils_1.cloneObject)(newValue); + mutations.forEach(mutation => { + // mutation.target is relative to proxy root + const trailKeys = mutation.target.slice(target.length); + for (let i = 0, val = newValue, prev = previousValue; i < trailKeys.length; i++) { // arr = PathInfo.getPathKeys(mutationPath).slice(PathInfo.getPathKeys(targetRef.path).length) + const last = i + 1 === trailKeys.length, key = trailKeys[i]; + if (last) { + val[key] = mutation.val; + if (val[key] === null) { + delete val[key]; + } + prev[key] = mutation.prev; + if (prev[key] === null) { + delete prev[key]; + } + } + else { + val = val[key] = key in val ? val[key] : {}; + prev = prev[key] = key in prev ? prev[key] : {}; + } + } + }); } - const request = message; - const result = { type: 'lock-result', id: request.id, from: this.id, to: request.from, ok: true, data: undefined }; - try { - let movedLock; - // const lock = this._locks.get(request.data.id); - const lockRequest = this._locks.find(r => { var _a; return ((_a = r.lock) === null || _a === void 0 ? void 0 : _a.id) === request.data.id; }); - if (request.data.move_to === 'parent') { - movedLock = await lockRequest.lock.moveToParent(); + process_1.default.nextTick(() => { + // Run callback with read-only (frozen) values in next tick + let keepSubscription = true; + try { + keepSubscription = false !== callback(Object.freeze(newValue), Object.freeze(previousValue), !causedByOurProxy, context); } - else { - throw new Error(`Unknown lock move_to "${request.data.move_to}"`); + catch (err) { + clientEventEmitter.emit('error', { source: origin === 'remote' ? 'remote_update' : 'local_update', message: 'Error running subscription callback', details: err }); } - // this._locks.delete(request.data.id); - // this._locks.set(movedLock.id, movedLock); - lockRequest.lock = movedLock; - result.data = { - id: movedLock.id, - path: movedLock.path, - tid: movedLock.tid, - write: movedLock.forWriting, - expires: movedLock.expires, - comment: movedLock.comment, - }; - } - catch (err) { - result.ok = false; - result.reason = err.stack || err.message || err; - } - return this.sendMessage(result); - } - case 'notification': { - // Custom notification received - raise event - return this.emit('notification', message); + if (keepSubscription === false) { + stop(); + } + }); + }; + localMutationsEmitter.on('mutations', mutationsHandler); + const stop = () => { + localMutationsEmitter.off('mutations', mutationsHandler); + clientSubscriptions.splice(clientSubscriptions.findIndex(cs => cs.stop === stop), 1); + }; + clientSubscriptions.push({ target, stop }); + return { stop }; + }; + const handleFlag = (flag, target, args) => { + if (flag === 'write') { + return flagOverwritten(target); } - case 'request': { - // Custom message received - raise event - return this.emit('request', message); + else if (flag === 'onChange') { + return addOnChangeHandler(target, args.callback); } - case 'result': { - // Result of custom request received - raise event - const result = message; - const request = this._requests.get(result.id); - if (typeof request !== 'object') { - throw new Error(`Result of unknown request received`); - } - if (result.ok) { - request.resolve(result.data); + else if (flag === 'subscribe' || flag === 'observe') { + const subscribe = (subscriber) => { + const currentValue = getTargetValue(cache, target); + subscriber.next(currentValue); + const subscription = addOnChangeHandler(target, (value /*, previous, isRemote, context */) => { + subscriber.next(value); + }); + return function unsubscribe() { + subscription.stop(); + }; + }; + if (flag === 'subscribe') { + return subscribe; } - else { - request.reject(new Error(result.reason)); + // Try to load Observable + const Observable = (0, optional_observable_1.getObservable)(); + return new Observable(subscribe); + } + else if (flag === 'transaction') { + const hasConflictingTransaction = transactions.some(t => RelativeNodeTarget.areEqual(target, t.target) || RelativeNodeTarget.isAncestor(target, t.target) || RelativeNodeTarget.isDescendant(target, t.target)); + if (hasConflictingTransaction) { + // TODO: Wait for this transaction to finish, then try again + return Promise.reject(new Error('Cannot start transaction because it conflicts with another transaction')); } + return new Promise(async (resolve) => { + // If there are pending mutations on target (or deeper), wait until they have been synchronized + const hasPendingMutations = mutationQueue.some(m => RelativeNodeTarget.areEqual(target, m.target) || RelativeNodeTarget.isAncestor(target, m.target)); + if (hasPendingMutations) { + if (!syncInProgress) { + scheduleSync(); + } + await syncCompleted(); + } + const tx = { target, status: 'started', transaction: null }; + transactions.push(tx); + tx.transaction = { + get status() { return tx.status; }, + get completed() { return tx.status !== 'started'; }, + get mutations() { + return mutationQueue.filter(m => RelativeNodeTarget.areEqual(tx.target, m.target) || RelativeNodeTarget.isAncestor(tx.target, m.target)); + }, + get hasMutations() { + return this.mutations.length > 0; + }, + async commit() { + if (this.completed) { + throw new Error(`Transaction has completed already (status '${tx.status}')`); + } + tx.status = 'finished'; + transactions.splice(transactions.indexOf(tx), 1); + if (syncInProgress) { + // Currently syncing without our mutations + await syncCompleted(); + } + scheduleSync(); + await syncCompleted(); + }, + rollback() { + // Remove mutations from queue + if (this.completed) { + throw new Error(`Transaction has completed already (status '${tx.status}')`); + } + tx.status = 'canceled'; + const mutations = []; + for (let i = 0; i < mutationQueue.length; i++) { + const m = mutationQueue[i]; + if (RelativeNodeTarget.areEqual(tx.target, m.target) || RelativeNodeTarget.isAncestor(tx.target, m.target)) { + mutationQueue.splice(i, 1); + i--; + mutations.push(m); + } + } + // Replay mutations in reverse order + mutations.reverse() + .forEach(m => { + if (m.target.length === 0) { + cache = m.previous; + } + else { + setTargetValue(cache, m.target, m.previous); + } + }); + // Remove transaction + transactions.splice(transactions.indexOf(tx), 1); + }, + }; + resolve(tx.transaction); + }); } + }; + const snap = await ref.get({ cache_mode: 'allow', cache_cursor: options === null || options === void 0 ? void 0 : options.cursor }); + // const gotOfflineStartValue = snap.context().acebase_origin === 'cache'; + // if (gotOfflineStartValue) { + // console.warn(`Started data proxy with cached value of "${ref.path}", check if its value is reloaded on next connection!`); + // } + if (snap.context().acebase_origin !== 'cache') { + clientEventEmitter.emit('cursor', (_a = ref.cursor) !== null && _a !== void 0 ? _a : null); // latestCursor = snap.context().acebase_cursor ?? null; } - } - /** - * Acquires a lock. If this peer is a worker, it will request the lock from the master - * @param details - */ - async lock(details) { - if (this._exiting) { - // Peer is exiting. Do we have an existing lock with requested tid? If not, deny request. - const tidApproved = this._locks.find(l => l.tid === details.tid && l.granted); - if (!tidApproved) { - // We have no previously granted locks for this transaction. Deny. - throw new AceBaseIPCPeerExitingError('new transaction lock denied because the IPC peer is exiting'); - } + loaded = true; + cache = snap.val(); + if (cache === null && typeof (options === null || options === void 0 ? void 0 : options.defaultValue) !== 'undefined') { + cache = options.defaultValue; + const context = { + acebase_proxy: { + id: proxyId, + source: 'default', + // update_id: ID.generate() + }, + }; + await ref.context(context).set(cache); } - const removeLock = (lockDetails) => { - this._locks.splice(this._locks.indexOf(lockDetails), 1); - if (this._locks.length === 0) { - // this.logger.debug(`No more locks in worker ${this.id}`); - this.emit('locks-cleared'); + proxy = createProxy({ root: { ref, get cache() { return cache; } }, target: [], id: proxyId, flag: handleFlag }); + const assertProxyAvailable = () => { + if (proxy === null) { + throw new Error('Proxy was destroyed'); } }; - if (this.isMaster) { - // Master - const lockInfo = { tid: details.tid, granted: false, request: details, lock: null }; - this._locks.push(lockInfo); - const lock = await this._nodeLocker.lock(details.path, details.tid, details.write, details.comment); - lockInfo.tid = lock.tid; - lockInfo.granted = true; - const createIPCLock = (lock) => { - return { - get id() { return lock.id; }, - get tid() { return lock.tid; }, - get path() { return lock.path; }, - get forWriting() { return lock.forWriting; }, - get expires() { return lock.expires; }, - get comment() { return lock.comment; }, - get state() { return lock.state; }, - release: async () => { - await lock.release(); - removeLock(lockInfo); - }, - moveToParent: async () => { - const parentLock = await lock.moveToParent(); - lockInfo.lock = createIPCLock(parentLock); - return lockInfo.lock; - }, - }; - }; - lockInfo.lock = createIPCLock(lock); - return lockInfo.lock; - } - else { - // Worker - const lockInfo = { tid: details.tid, granted: false, request: details, lock: null }; - this._locks.push(lockInfo); - const createIPCLock = (result) => { - lockInfo.granted = true; - lockInfo.tid = result.tid; - lockInfo.lock = { - id: result.id, - tid: result.tid, - path: result.path, - forWriting: result.write, - state: node_lock_1.LOCK_STATE.LOCKED, - expires: result.expires, - comment: result.comment, - release: async () => { - const req = { type: 'unlock-request', id: acebase_core_1.ID.generate(), from: this.id, to: this.masterPeerId, data: { id: lockInfo.lock.id } }; - await this.request(req); - lockInfo.lock.state = node_lock_1.LOCK_STATE.DONE; - this.logger.trace(`Worker ${this.id} released lock ${lockInfo.lock.id} (tid ${lockInfo.lock.tid}, ${lockInfo.lock.comment}, "/${lockInfo.lock.path}", ${lockInfo.lock.forWriting ? 'write' : 'read'})`); - removeLock(lockInfo); - }, - moveToParent: async () => { - const req = { type: 'move-lock-request', id: acebase_core_1.ID.generate(), from: this.id, to: this.masterPeerId, data: { id: lockInfo.lock.id, move_to: 'parent' } }; - let result; - try { - result = await this.request(req); - } - catch (err) { - // We didn't get new lock?! - lockInfo.lock.state = node_lock_1.LOCK_STATE.DONE; - removeLock(lockInfo); - throw err; - } - lockInfo.lock = createIPCLock(result); - return lockInfo.lock; - }, - }; - // this.logger.debug(`Worker ${this.id} received lock ${lock.id} (tid ${lock.tid}, ${lock.comment}, "/${lock.path}", ${lock.forWriting ? 'write' : 'read'})`); - return lockInfo.lock; - }; - const req = { type: 'lock-request', id: acebase_core_1.ID.generate(), from: this.id, to: this.masterPeerId, data: details }; - let result, err; - try { - result = await this.request(req); - } - catch (e) { - err = e; - result = null; - } - if (err) { - removeLock(lockInfo); - throw err; + const reload = async () => { + // Manually reloads current value when cache is out of sync, which should only + // be able to happen if an AceBaseClient is used without cache database, + // and the connection to the server was lost for a while. In all other cases, + // there should be no need to call this method. + assertProxyAvailable(); + mutationQueue.splice(0); // Remove pending mutations. Will be empty in production, but might not be while debugging, leading to weird behaviour. + const snap = await ref.get({ allow_cache: false }); + const oldVal = cache, newVal = snap.val(); + cache = newVal; + // Compare old and new values + const mutations = (0, utils_1.getMutations)(oldVal, newVal); + if (mutations.length === 0) { + return; // Nothing changed } - return createIPCLock(result); - } - } - async request(req) { - // Send request, return result promise - let resolve, reject; - const promise = new Promise((rs, rj) => { - resolve = (result) => { - this._requests.delete(req.id); - rs(result); - }; - reject = (err) => { - this._requests.delete(req.id); - rj(err); - }; - }); - this._requests.set(req.id, { resolve, reject, request: req }); - this.sendMessage(req); - return promise; - } - /** - * Sends a custom request to the IPC master - * @param request - * @returns - */ - sendRequest(request) { - const req = { type: 'request', from: this.id, to: this.masterPeerId, id: acebase_core_1.ID.generate(), data: request }; - return this.request(req) - .catch(err => { - this.logger.error(err); - throw err; - }); - } - replyRequest(requestMessage, result) { - const reply = { type: 'result', id: requestMessage.id, ok: true, from: this.id, to: requestMessage.from, data: result }; - this.sendMessage(reply); - } - /** - * Sends a custom notification to all IPC peers - * @param notification - * @returns - */ - sendNotification(notification) { - const msg = { type: 'notification', from: this.id, data: notification }; - this.sendMessage(msg); + // Run onMutation callback for each changed node + const context = snap.context(); // context might contain acebase_cursor if server support that + context.acebase_proxy = { id: proxyId, source: 'reload' }; + // if (onMutationCallback) { + mutations.forEach(m => { + const targetRef = getTargetRef(ref, m.target); + const newSnap = new data_snapshot_1.DataSnapshot(targetRef, m.val, m.val === null, m.prev, context); + clientEventEmitter.emit('mutation', { snapshot: newSnap, isRemote: true }); + }); + // } + // Notify local subscribers + const mutationsSnap = new data_snapshot_1.MutationsDataSnapshot(ref, mutations, context); + localMutationsEmitter.emit('mutations', { origin: 'local', snap: mutationsSnap }); + }; + return { + async destroy() { + await processPromise; + const promises = [ + subscription.stop(), + ...clientSubscriptions.map(cs => cs.stop()), + ]; + await Promise.all(promises); + ['cursor', 'mutation', 'error'].forEach(event => clientEventEmitter.off(event)); + cache = null; // Remove cache + proxy = null; + }, + stop() { + this.destroy(); + }, + get value() { + assertProxyAvailable(); + return proxy; + }, + get hasValue() { + assertProxyAvailable(); + return cache !== null; + }, + set value(val) { + // Overwrite the value of the proxied path itself! + assertProxyAvailable(); + if (val !== null && typeof val === 'object' && val[isProxy]) { + // Assigning one proxied value to another + val = val.valueOf(); + } + flagOverwritten([]); + cache = val; + }, + get ref() { + return ref; + }, + get cursor() { + return latestCursor; + }, + reload, + onMutation(callback) { + // Fires callback each time anything changes + assertProxyAvailable(); + clientEventEmitter.off('mutation'); // Mimic legacy behaviour that overwrites handler + clientEventEmitter.on('mutation', ({ snapshot, isRemote }) => { + try { + callback(snapshot, isRemote); + } + catch (err) { + clientEventEmitter.emit('error', { source: 'mutation_callback', message: 'Error in dataproxy onMutation callback', details: err }); + } + }); + }, + onError(callback) { + // Fires callback each time anything goes wrong + assertProxyAvailable(); + clientEventEmitter.off('error'); // Mimic legacy behaviour that overwrites handler + clientEventEmitter.on('error', (err) => { + try { + callback(err); + } + catch (err) { + console.error(`Error in dataproxy onError callback: ${err.message}`); + } + }); + }, + on(event, callback) { + clientEventEmitter.on(event, callback); + }, + off(event, callback) { + clientEventEmitter.off(event, callback); + }, + }; } - /** - * If ipc event handling is currently enabled - */ - get eventsEnabled() { return this._eventsEnabled; } - /** - * Enables or disables ipc event handling. When disabled, incoming event messages will be ignored. - */ - set eventsEnabled(enabled) { - this.logger.info(`ipc events ${enabled ? 'enabled' : 'disabled'}`); - this._eventsEnabled = enabled; +} +exports.LiveDataProxy = LiveDataProxy; +function getTargetValue(obj, target) { + let val = obj; + for (const key of target) { + val = typeof val === 'object' && val !== null && key in val ? val[key] : null; } + return val; } -exports.AceBaseIPCPeer = AceBaseIPCPeer; - -},{"../node-lock":13,"acebase-core":46}],10:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.RemovedNodeAddress = exports.NodeAddress = void 0; -class NodeAddress { - constructor(path) { - this.path = path; +function setTargetValue(obj, target, value) { + if (target.length === 0) { + throw new Error('Cannot update root target, caller must do that itself!'); } - toString() { - return `"/${this.path}"`; + const targetObject = target.slice(0, -1).reduce((obj, key) => obj[key], obj); + const prop = target.slice(-1)[0]; + if (value === null || typeof value === 'undefined') { + // Remove it + targetObject instanceof Array ? targetObject.splice(prop, 1) : delete targetObject[prop]; } - /** - * Compares this address to another address - */ - equals(address) { - return this.path === address.path; + else { + // Set or update it + targetObject[prop] = value; } } -exports.NodeAddress = NodeAddress; -class RemovedNodeAddress extends NodeAddress { - constructor(path) { - super(path); - } - toString() { - return `"/${this.path}" (removed)`; - } - /** - * Compares this address to another address - */ - equals(address) { - return address instanceof RemovedNodeAddress && this.path === address.path; - } -} -exports.RemovedNodeAddress = RemovedNodeAddress; - -},{}],11:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.NodeRevisionError = exports.NodeNotFoundError = void 0; -class NodeNotFoundError extends Error { -} -exports.NodeNotFoundError = NodeNotFoundError; -class NodeRevisionError extends Error { +function getTargetRef(ref, target) { + // Create new DataReference to prevent context reuse + const path = path_info_1.PathInfo.get(ref.path).childPath(target); + return new data_reference_1.DataReference(ref.db, path); } -exports.NodeRevisionError = NodeRevisionError; - -},{}],12:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.NodeInfo = void 0; -const node_value_types_1 = require("./node-value-types"); -const acebase_core_1 = 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; - this.childCount = info.childCount; - if (typeof this.path === 'string' && (typeof this.key === 'undefined' && typeof this.index === 'undefined')) { - const pathInfo = acebase_core_1.PathInfo.get(this.path); - if (typeof pathInfo.key === 'number') { - this.index = pathInfo.key; +function createProxy(context) { + const targetRef = getTargetRef(context.root.ref, context.target); + const childProxies = []; + const handler = { + get(target, prop, receiver) { + target = getTargetValue(context.root.cache, context.target); + if (typeof prop === 'symbol') { + if (prop.toString() === Symbol.iterator.toString()) { + // Use .values for @@iterator symbol + prop = 'values'; + } + else if (prop.toString() === isProxy.toString()) { + return true; + } + else { + return Reflect.get(target, prop, receiver); + } } - else { - this.key = pathInfo.key; + if (prop === 'valueOf') { + return function valueOf() { return target; }; } - } - if (typeof this.exists === 'undefined') { - this.exists = true; - } - } - get valueType() { - return this.type; - } - get valueTypeName() { - return (0, node_value_types_1.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.toString()}`; - } - else { - return `"${this.path}" is ${this.valueTypeName} with value ${this.value}`; - } - } -} -exports.NodeInfo = NodeInfo; - -},{"./node-value-types":14,"acebase-core":46}],13:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.NodeLock = exports.NodeLocker = exports.NodeLockError = exports.LOCK_STATE = void 0; -const acebase_core_1 = require("acebase-core"); -const assert_1 = require("./assert"); -const DEBUG_MODE = false; -const DEFAULT_LOCK_TIMEOUT = 120; // in seconds -exports.LOCK_STATE = { - PENDING: 'pending', - LOCKED: 'locked', - EXPIRED: 'expired', - DONE: 'done', -}; -class NodeLockError extends Error { - constructor(message, lock) { - super(message); - this.lock = lock; - } -} -exports.NodeLockError = NodeLockError; -class NodeLocker { - /** - * Provides locking mechanism for nodes, ensures no simultanious read and writes happen to overlapping paths - */ - constructor(logger, lockTimeout = DEFAULT_LOCK_TIMEOUT) { - this.logger = logger; - this._locks = []; - this._lastTid = 0; - this.timeout = lockTimeout * 1000; - } - setTimeout(timeout) { - this.timeout = timeout * 1000; - } - createTid() { - return DEBUG_MODE ? ++this._lastTid : acebase_core_1.ID.generate(); - } - _allowLock(path, tid, forWriting) { - /** - * Disabled path locking because of the following issue: - * - * Process 1 requests WRITE lock on "/users/ewout", is GRANTED - * Process 2 requests READ lock on "", is DENIED (process 1 writing to a descendant) - * Process 3 requests WRITE lock on "/posts/post1", is GRANTED - * Process 1 requests READ lock on "/" because of bound events, is DENIED (3 is writing to a descendant) - * Process 3 requests READ lock on "/" because of bound events, is DENIED (1 is writing to a descendant) - * - * --> DEADLOCK! - * - * Now simply makes sure one transaction has write access at the same time, - * might change again in the future... - */ - const conflict = this._locks - .find(otherLock => { - return (otherLock.tid !== tid - && otherLock.state === exports.LOCK_STATE.LOCKED - && (forWriting || otherLock.forWriting)); - }); - return { allow: !conflict, conflict }; - } - quit() { - return new Promise(resolve => { - if (this._locks.length === 0) { - return resolve(); + if (target === null || typeof target !== 'object') { + throw new Error(`Cannot read property "${prop}" of ${target}. Value of path "/${targetRef.path}" is not an object (anymore)`); } - this._quit = resolve; - }); - } - /** - * Safely reject a pending lock, catching any unhandled promise rejections (that should not happen in the first place, obviously) - * @param lock - */ - _rejectLock(lock, err) { - this._locks.splice(this._locks.indexOf(lock), 1); // Remove from queue - clearTimeout(lock.timeout); - try { - lock.reject(err); - } - catch (err) { - console.error(`Unhandled promise rejection:`, err); - } - } - _processLockQueue() { - if (this._quit) { - // Reject all pending locks - const quitError = new Error('Quitting'); - this._locks - .filter(lock => lock.state === exports.LOCK_STATE.PENDING) - .forEach(lock => this._rejectLock(lock, quitError)); - // Resolve quit promise if queue is empty: - if (this._locks.length === 0) { - this._quit(); + if (target instanceof Array && typeof prop === 'string' && /^[0-9]+$/.test(prop)) { + // Proxy type definitions say prop can be a number, but this is never the case. + prop = parseInt(prop); } - } - const pending = this._locks - .filter(lock => lock.state === exports.LOCK_STATE.PENDING) - .sort((a, b) => { - // // Writes get higher priority so all reads get the most recent data - // if (a.forWriting === b.forWriting) { - // if (a.requested < b.requested) { return -1; } - // else { return 1; } - // } - // else if (a.forWriting) { return -1; } - if (a.priority && !b.priority) { - return -1; + const value = target[prop]; + if (value === null) { + // Removed property. Should never happen, but if it does: + delete target[prop]; + return; // undefined } - else if (!a.priority && b.priority) { - return 1; + // Check if we have a child proxy for this property already. + // If so, and the properties' typeof value did not change, return that + const childProxy = childProxies.find(proxy => proxy.prop === prop); + if (childProxy) { + if (childProxy.typeof === typeof value) { + return childProxy.value; + } + childProxies.splice(childProxies.indexOf(childProxy), 1); } - return a.requested - b.requested; - }); - pending.forEach(lock => { - 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(err => this._rejectLock(lock, err)); + const proxifyChildValue = (prop) => { + const value = target[prop]; // + const childProxy = childProxies.find(child => child.prop === prop); + if (childProxy) { + if (childProxy.typeof === typeof value) { + return childProxy.value; + } + childProxies.splice(childProxies.indexOf(childProxy), 1); + } + if (typeof value !== 'object') { + // Can't proxify non-object values + return value; + } + const newChildProxy = createProxy({ root: context.root, target: context.target.concat(prop), id: context.id, flag: context.flag }); + childProxies.push({ typeof: typeof value, prop, value: newChildProxy }); + return newChildProxy; + }; + const unproxyValue = (value) => { + return value !== null && typeof value === 'object' && value[isProxy] + ? value.getTarget() + : value; + }; + // If the property contains a simple value, return it. + if (['string', 'number', 'boolean'].includes(typeof value) + || value instanceof Date + || value instanceof path_reference_1.PathReference + || value instanceof ArrayBuffer + || (typeof value === 'object' && 'buffer' in value) // Typed Arrays + ) { + return value; } - }); - } - async 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 === exports.LOCK_STATE.EXPIRED)) >= 0) { - const expiredLock = this._locks.find((l => l.tid === tid && l.state === exports.LOCK_STATE.EXPIRED)); - throw new NodeLockError(`lock on tid ${tid} has expired, not allowed to continue`, expiredLock !== null && expiredLock !== void 0 ? expiredLock : null); - } - else if (this._quit && !options.withPriority) { - const refLock = this._locks.find((l => l.tid === tid && l.path === path)); - throw new NodeLockError(`Quitting`, refLock !== null && refLock !== void 0 ? refLock : null); - } - else { - DEBUG_MODE && console.error(`${forWriting ? 'write' : 'read'} lock requested on "${path}" by tid ${tid} (${comment})`); - // // Test the requested lock path - // let duplicateKeys = getPathKeys(path) - // .reduce((r, key) => { - // let i = r.findIndex(c => c.key === key); - // if (i >= 0) { r[i].count++; } - // else { r.push({ key, count: 1 }) } - // return r; - // }, []) - // .filter(c => c.count > 1) - // .map(c => c.key); - // if (duplicateKeys.length > 0) { - // console.log(`ALERT: Duplicate keys found in path "/${path}"`.colorize([ColorStyle.dim, ColorStyle.bgRed]); - // } - 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) { - DEBUG_MODE && console.error(`${lock.forWriting ? 'write' : 'read'} lock ALLOWED on "${lock.path}" by tid ${lock.tid} (${lock.comment})`); - lock.state = exports.LOCK_STATE.LOCKED; - if (typeof lock.granted === 'number') { - //debug.warn(`lock :: ALLOWING ${lock.forWriting ? "write" : "read" } lock on path "/${lock.path}" by tid ${lock.tid}; ${lock.comment}`); + const isArray = target instanceof Array; + if (prop === 'toString') { + return function toString() { + return `[LiveDataProxy for "${targetRef.path}"]`; + }; } - else { - lock.granted = Date.now(); - if (options.noTimeout !== true) { - lock.expires = Date.now() + this.timeout; - //debug.warn(`lock :: GRANTED ${lock.forWriting ? "write" : "read" } lock on path "/${lock.path}" by tid ${lock.tid}; ${lock.comment}`); - let timeoutCount = 0; - const timeoutHandler = () => { - // Autorelease timeouts must only fire when there is something wrong in the - // executing (AceBase) code, eg an unhandled promise rejection causing a lock not - // to be released. To guard against programming errors, we will issue 3 warning - // messages before releasing the lock. - if (lock.state !== exports.LOCK_STATE.LOCKED) { - return; + if (typeof value === 'undefined') { + if (prop === 'push') { + // Push item to an object collection + return function push(item) { + const childRef = targetRef.push(); + context.flag('write', context.target.concat(childRef.key)); //, { previous: null } + target[childRef.key] = item; + return childRef.key; + }; + } + if (prop === 'getTarget') { + // Get unproxied readonly (but still live) version of data. + return function (warn = true) { + warn && console.warn('Use getTarget with caution - any changes will not be synchronized!'); + return target; + }; + } + if (prop === 'getRef') { + // Gets the DataReference to this data target + return function getRef() { + const ref = getTargetRef(context.root.ref, context.target); + return ref; + }; + } + if (prop === 'forEach') { + return function forEach(callback) { + const keys = Object.keys(target); + // Fix: callback with unproxied value + let stop = false; + for (let i = 0; !stop && i < keys.length; i++) { + const key = keys[i]; + const value = proxifyChildValue(key); //, target[key] + stop = callback(value, key, i) === false; } - timeoutCount++; - if (timeoutCount <= 3) { - // Warn first. - this.logger.warn(`${lock.forWriting ? 'write' : 'read'} lock on "/${lock.path}" is taking long [${timeoutCount}]; tid=${lock.tid} comment=${lock.comment}`); - lock.warned = true; - lock.timeout = setTimeout(timeoutHandler, this.timeout / 4); - return; + }; + } + if (['values', 'entries', 'keys'].includes(prop)) { + return function* generator() { + const keys = Object.keys(target); + for (const key of keys) { + if (prop === 'keys') { + yield key; + } + else { + const value = proxifyChildValue(key); //, target[key] + if (prop === 'entries') { + yield [key, value]; + } + else { + yield value; + } + } } - this.logger.error(`${lock.forWriting ? 'write' : 'read'} lock on "/${lock.path}" expired! tid=${lock.tid} comment=${lock.comment}`); - lock.state = exports.LOCK_STATE.EXPIRED; - // let allTransactionLocks = _locks.filter(l => l.tid === lock.tid).sort((a,b) => a.requested < b.requested ? -1 : 1); - // let transactionsDebug = allTransactionLocks.map(l => `${l.state} ${l.forWriting ? "WRITE" : "read"} ${l.comment}`).join("\n"); - // debug.error(transactionsDebug); - this._processLockQueue(); }; - lock.timeout = setTimeout(timeoutHandler, this.timeout / 4); } + if (prop === 'toArray') { + return function toArray(sortFn) { + const arr = Object.keys(target).map(key => proxifyChildValue(key)); //, target[key] + if (sortFn) { + arr.sort(sortFn); + } + return arr; + }; + } + if (prop === 'onChanged') { + // Starts monitoring the value + return function onChanged(callback) { + return context.flag('onChange', context.target, { callback }); + }; + } + if (prop === 'subscribe') { + // Gets subscriber function to use with Observables, or custom handling + return function subscribe() { + return context.flag('subscribe', context.target); + }; + } + if (prop === 'getObservable') { + // Creates an observable for monitoring the value + return function getObservable() { + return context.flag('observe', context.target); + }; + } + if (prop === 'getOrderedCollection') { + return function getOrderedCollection(orderProperty, orderIncrement) { + return new OrderedCollectionProxy(this, orderProperty, orderIncrement); + }; + } + if (prop === 'startTransaction') { + return function startTransaction() { + return context.flag('transaction', context.target); + }; + } + if (prop === 'remove' && !isArray) { + // Removes target from object collection + return function remove() { + if (context.target.length === 0) { + throw new Error('Can\'t remove proxy root value'); + } + const parent = getTargetValue(context.root.cache, context.target.slice(0, -1)); + const key = context.target.slice(-1)[0]; + context.flag('write', context.target); + delete parent[key]; + }; + } + return; // undefined } - return lock; - } - else { - // Keep pending until clashing lock(s) is/are removed - //debug.warn(`lock :: QUEUED ${lock.forWriting ? "write" : "read" } lock on path "/${lock.path}" by tid ${lock.tid}; ${lock.comment}`); - (0, assert_1.assert)(lock.state === exports.LOCK_STATE.PENDING); - return new Promise((resolve, reject) => { - lock.resolve = resolve; - lock.reject = reject; - }); - } - } - unlock(lockOrId, comment, processQueue = true) { - var _a, _b; - let lock, i; - if (lockOrId instanceof NodeLock) { - lock = lockOrId; - i = this._locks.indexOf(lock); - } - else { - const id = lockOrId; - i = this._locks.findIndex(l => l.id === id); - lock = this._locks[i]; - } - if (i < 0) { - const msg = `lock on "/${(_a = lock === null || lock === void 0 ? void 0 : lock.path) !== null && _a !== void 0 ? _a : '?'}" for tid ${(_b = lock === null || lock === void 0 ? void 0 : lock.tid) !== null && _b !== void 0 ? _b : '?'} wasn't found; ${comment}`; - // debug.error(`unlock :: ${msg}`); - throw new NodeLockError(msg, lock !== null && lock !== void 0 ? lock : null); - } - lock.state = exports.LOCK_STATE.DONE; - clearTimeout(lock.timeout); - if (lock.warned) { - this.logger.info(`long running ${lock.forWriting ? 'write' : 'read'} lock on "${lock.path}" by tid ${lock.tid} has been released`); - } - this._locks.splice(i, 1); - DEBUG_MODE && console.error(`${lock.forWriting ? 'write' : 'read'} lock RELEASED on "${lock.path}" by tid ${lock.tid}`); - //debug.warn(`unlock :: RELEASED ${lock.forWriting ? "write" : "read" } lock on "/${lock.path}" for tid ${lock.tid}; ${lock.comment}; ${comment}`); - processQueue && this._processLockQueue(); - return lock; - } - list() { - return this._locks || []; - } - isAllowed(path, tid, forWriting) { - return this._allowLock(path, tid, forWriting).allow; - } -} -exports.NodeLocker = NodeLocker; -let lastid = 0; -class NodeLock { - static get LOCK_STATE() { return exports.LOCK_STATE; } - /** - * Constructor for a record lock - * @param {NodeLocker} locker - * @param {string} path - * @param {string} tid - * @param {boolean} forWriting - * @param {boolean} priority - */ - constructor(locker, path, tid, forWriting, priority = false) { - this.locker = locker; - this.path = path; - this.tid = tid; - this.forWriting = forWriting; - this.priority = priority; - this.state = exports.LOCK_STATE.PENDING; - this.requested = Date.now(); - this.comment = ''; - this.waitingFor = null; - this.id = ++lastid; - this.history = []; - this.warned = false; - } - async release(comment) { - //return this.storage.unlock(this.path, this.tid, comment); - this.history.push({ action: 'release', path: this.path, forWriting: this.forWriting, comment }); - return this.locker.unlock(this, comment || this.comment); - } - async moveToParent() { - const parentPath = acebase_core_1.PathInfo.get(this.path).parentPath; //getPathInfo(this.path).parent; - const allowed = this.locker.isAllowed(parentPath, this.tid, this.forWriting); //_allowLock(parentPath, this.tid, this.forWriting); - if (allowed) { - DEBUG_MODE && console.error(`moveToParent ALLOWED for ${this.forWriting ? 'write' : 'read'} lock on "${this.path}" by tid ${this.tid} (${this.comment})`); - this.history.push({ path: this.path, forWriting: this.forWriting, action: 'moving to parent' }); - this.waitingFor = null; - this.path = parentPath; - // this.comment = `moved to parent: ${this.comment}`; - return this; + else if (typeof value === 'function') { + if (isArray) { + // Handle array methods + const writeArray = (action) => { + context.flag('write', context.target); + return action(); + }; + const cleanArrayValues = (values) => values.map((value) => { + value = unproxyValue(value); + removeVoidProperties(value); + return value; + }); + // Methods that directly change the array: + if (prop === 'push') { + return function push(...items) { + items = cleanArrayValues(items); + return writeArray(() => target.push(...items)); // push the items to the cache array + }; + } + if (prop === 'pop') { + return function pop() { + return writeArray(() => target.pop()); + }; + } + if (prop === 'splice') { + return function splice(start, deleteCount, ...items) { + items = cleanArrayValues(items); + return writeArray(() => target.splice(start, deleteCount, ...items)); + }; + } + if (prop === 'shift') { + return function shift() { + return writeArray(() => target.shift()); + }; + } + if (prop === 'unshift') { + return function unshift(...items) { + items = cleanArrayValues(items); + return writeArray(() => target.unshift(...items)); + }; + } + if (prop === 'sort') { + return function sort(compareFn) { + return writeArray(() => target.sort(compareFn)); + }; + } + if (prop === 'reverse') { + return function reverse() { + return writeArray(() => target.reverse()); + }; + } + // Methods that do not change the array themselves, but + // have callbacks that might, or return child values: + if (['indexOf', 'lastIndexOf'].includes(prop)) { + return function indexOf(item, start) { + if (item !== null && typeof item === 'object' && item[isProxy]) { + // Use unproxied value, or array.indexOf will return -1 (fixes issue #1) + item = item.getTarget(false); + } + return target[prop](item, start); + }; + } + if (['forEach', 'every', 'some', 'filter', 'map'].includes(prop)) { + return function iterate(callback) { + return target[prop]((value, i) => { + return callback(proxifyChildValue(i), i, proxy); //, value + }); + }; + } + if (['reduce', 'reduceRight'].includes(prop)) { + return function reduce(callback, initialValue) { + return target[prop]((prev, value, i) => { + return callback(prev, proxifyChildValue(i), i, proxy); //, value + }, initialValue); + }; + } + if (['find', 'findIndex'].includes(prop)) { + return function find(callback) { + let value = target[prop]((value, i) => { + return callback(proxifyChildValue(i), i, proxy); // , value + }); + if (prop === 'find' && value) { + const index = target.indexOf(value); + value = proxifyChildValue(index); //, value + } + return value; + }; + } + if (['values', 'entries', 'keys'].includes(prop)) { + return function* generator() { + for (let i = 0; i < target.length; i++) { + if (prop === 'keys') { + yield i; + } + else { + const value = proxifyChildValue(i); //, target[i] + if (prop === 'entries') { + yield [i, value]; + } + else { + yield value; + } + } + } + }; + } + } + // Other function (or not an array), should not alter its value + // return function fn(...args) { + // return target[prop](...args); + // } + return value; + } + // Proxify any other value + return proxifyChildValue(prop); //, value + }, + set(target, prop, value, receiver) { + // Eg: chats.chat1.title = 'New chat title'; + // target === chats.chat1, prop === 'title' + target = getTargetValue(context.root.cache, context.target); + if (typeof prop === 'symbol') { + return Reflect.set(target, prop, value, receiver); + } + if (target === null || typeof target !== 'object') { + throw new Error(`Cannot set property "${prop}" of ${target}. Value of path "/${targetRef.path}" is not an object`); + } + if (target instanceof Array && typeof prop === 'string') { + if (!/^[0-9]+$/.test(prop)) { + throw new Error(`Cannot set property "${prop}" on array value of path "/${targetRef.path}"`); + } + prop = parseInt(prop); + } + if (value !== null) { + if (typeof value === 'object') { + if (value[isProxy]) { + // Assigning one proxied value to another + value = value.valueOf(); + } + // else if (Object.isFrozen(value)) { + // // Create a copy to unfreeze it + // value = cloneObject(value); + // } + value = (0, utils_1.cloneObject)(value); // Fix #10, always clone objects so changes made through the proxy won't change the original object (and vice versa) + } + if ((0, utils_1.valuesAreEqual)(value, target[prop])) { //if (compareValues(value, target[prop]) === 'identical') { // (typeof value !== 'object' && target[prop] === value) { + // not changing the actual value, ignore + return true; + } + } + if (context.target.some(key => typeof key === 'number')) { + // Updating an object property inside an array. Flag the first array in target to be written. + // Eg: when chat.members === [{ name: 'Ewout', id: 'someid' }] + // --> chat.members[0].name = 'Ewout' --> Rewrite members array instead of chat/members[0]/name + context.flag('write', context.target.slice(0, context.target.findIndex(key => typeof key === 'number'))); + } + else if (target instanceof Array) { + // Flag the entire array to be overwritten + context.flag('write', context.target); + } + else { + // Flag child property + context.flag('write', context.target.concat(prop)); + } + // Set cached value: + if (value === null) { + delete target[prop]; + } + else { + removeVoidProperties(value); + target[prop] = value; + } + return true; + }, + deleteProperty(target, prop) { + target = getTargetValue(context.root.cache, context.target); + if (target === null) { + throw new Error(`Cannot delete property ${prop.toString()} of null`); + } + if (typeof prop === 'symbol') { + return Reflect.deleteProperty(target, prop); + } + if (!(prop in target)) { + return true; // Nothing to delete + } + context.flag('write', context.target.concat(prop)); + delete target[prop]; + return true; + }, + ownKeys(target) { + target = getTargetValue(context.root.cache, context.target); + return Reflect.ownKeys(target); + }, + has(target, prop) { + target = getTargetValue(context.root.cache, context.target); + return Reflect.has(target, prop); + }, + getOwnPropertyDescriptor(target, prop) { + target = getTargetValue(context.root.cache, context.target); + const descriptor = Reflect.getOwnPropertyDescriptor(target, prop); + if (descriptor) { + descriptor.configurable = true; // prevent "TypeError: 'getOwnPropertyDescriptor' on proxy: trap reported non-configurability for property '...' which is either non-existant or configurable in the proxy target" + } + return descriptor; + }, + getPrototypeOf(target) { + target = getTargetValue(context.root.cache, context.target); + return Reflect.getPrototypeOf(target); + }, + }; + const proxy = new Proxy({}, handler); + return proxy; +} +function removeVoidProperties(obj) { + if (typeof obj !== 'object') { + return; + } + Object.keys(obj).forEach(key => { + const val = obj[key]; + if (val === null || typeof val === 'undefined') { + delete obj[key]; } - else { - // Unlock without processing the queue - DEBUG_MODE && console.error(`moveToParent QUEUED for ${this.forWriting ? 'write' : 'read'} lock on "${this.path}" by tid ${this.tid} (${this.comment})`); - this.locker.unlock(this, `moveLockToParent: ${this.comment}`, false); - // Lock parent node with priority to jump the queue - const newLock = await this.locker.lock(parentPath, this.tid, this.forWriting, this.comment, { withPriority: true }); - DEBUG_MODE && console.error(`QUEUED moveToParent ALLOWED for ${this.forWriting ? 'write' : 'read'} lock on "${this.path}" by tid ${this.tid} (${this.comment})`); - newLock.history = this.history; - newLock.history.push({ path: this.path, forWriting: this.forWriting, action: 'moving to parent through queue (priority)' }); - return newLock; + else if (typeof val === 'object') { + removeVoidProperties(val); } + }); +} +/** + * Convenience function to access ILiveDataProxyValue methods on a proxied value + * @param proxiedValue The proxied value to get access to + * @returns Returns the same object typecasted to an ILiveDataProxyValue + * @example + * // IChatMessages is an ObjectCollection + * let observable: Observable; + * + * // Allows you to do this: + * observable = proxyAccess(chat.messages).getObservable(); + * + * // Instead of: + * observable = (chat.messages.msg1 as any as ILiveDataProxyValue).getObservable(); + * + * // Both do the exact same, but the first is less obscure + */ +function proxyAccess(proxiedValue) { + if (typeof proxiedValue !== 'object' || !proxiedValue[isProxy]) { + throw new Error('Given value is not proxied. Make sure you are referencing the value through the live data proxy.'); } + return proxiedValue; } -exports.NodeLock = NodeLock; - -},{"./assert":4,"acebase-core":46}],14:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getValueType = exports.getNodeValueType = exports.getValueTypeName = exports.VALUE_TYPES = void 0; -const acebase_core_1 = require("acebase-core"); -const nodeValueTypes = { - // Native types: - OBJECT: 1, - ARRAY: 2, - NUMBER: 3, - BOOLEAN: 4, - STRING: 5, - BIGINT: 7, - // Custom types: - DATETIME: 6, - BINARY: 8, - REFERENCE: 9, // Absolute or relative path to other node - // Future: - // DOCUMENT: 10, // JSON/XML documents that are contained entirely within the stored node -}; -exports.VALUE_TYPES = nodeValueTypes; -function getValueTypeName(valueType) { - switch (valueType) { - case exports.VALUE_TYPES.ARRAY: return 'array'; - case exports.VALUE_TYPES.BINARY: return 'binary'; - case exports.VALUE_TYPES.BOOLEAN: return 'boolean'; - case exports.VALUE_TYPES.DATETIME: return 'date'; - case exports.VALUE_TYPES.NUMBER: return 'number'; - case exports.VALUE_TYPES.OBJECT: return 'object'; - case exports.VALUE_TYPES.REFERENCE: return 'reference'; - case exports.VALUE_TYPES.STRING: return 'string'; - case exports.VALUE_TYPES.BIGINT: return 'bigint'; - // case VALUE_TYPES.DOCUMENT: return 'document'; - default: 'unknown'; - } -} -exports.getValueTypeName = getValueTypeName; -function getNodeValueType(value) { - if (value instanceof Array) { - return exports.VALUE_TYPES.ARRAY; - } - else if (value instanceof acebase_core_1.PathReference) { - return exports.VALUE_TYPES.REFERENCE; - } - else if (value instanceof ArrayBuffer) { - return exports.VALUE_TYPES.BINARY; - } - // TODO else if (value instanceof DataDocument) { return VALUE_TYPES.DOCUMENT; } - else if (typeof value === 'string') { - return exports.VALUE_TYPES.STRING; - } - else if (typeof value === 'object') { - return exports.VALUE_TYPES.OBJECT; - } - else if (typeof value === 'bigint') { - return exports.VALUE_TYPES.BIGINT; - } - throw new Error(`Invalid value for standalone node: ${value}`); -} -exports.getNodeValueType = getNodeValueType; -function getValueType(value) { - if (value instanceof Array) { - return exports.VALUE_TYPES.ARRAY; - } - else if (value instanceof acebase_core_1.PathReference) { - return exports.VALUE_TYPES.REFERENCE; +exports.proxyAccess = proxyAccess; +/** + * Provides functionality to work with ordered collections through a live data proxy. Eliminates + * the need for arrays to handle ordered data by adding a 'sort' properties to child objects in a + * collection, and provides functionality to sort and reorder items with a minimal amount of database + * updates. + */ +class OrderedCollectionProxy { + constructor(collection, orderProperty = 'order', orderIncrement = 10) { + this.collection = collection; + this.orderProperty = orderProperty; + this.orderIncrement = orderIncrement; + if (typeof collection !== 'object' || !collection[isProxy]) { + throw new Error('Collection is not proxied'); + } + if (collection.valueOf() instanceof Array) { + throw new Error('Collection is an array, not an object collection'); + } + if (!Object.keys(collection).every(key => typeof collection[key] === 'object')) { + throw new Error('Collection has non-object children'); + } + // Check if the collection has order properties. If not, assign them now + const ok = Object.keys(collection).every(key => typeof collection[key][orderProperty] === 'number'); + if (!ok) { + // Assign order properties now. Database will be updated automatically + const keys = Object.keys(collection); + for (let i = 0; i < keys.length; i++) { + const item = collection[keys[i]]; + item[orderProperty] = i * orderIncrement; // 0, 10, 20, 30 etc + } + } } - else if (value instanceof ArrayBuffer) { - return exports.VALUE_TYPES.BINARY; + /** + * Gets an observable for the target object collection. Same as calling `collection.getObservable()` + * @returns + */ + getObservable() { + return proxyAccess(this.collection).getObservable(); } - else if (value instanceof Date) { - return exports.VALUE_TYPES.DATETIME; + /** + * Gets an observable that emits a new ordered array representation of the object collection each time + * the unlaying data is changed. Same as calling `getArray()` in a `getObservable().subscribe` callback + * @returns + */ + getArrayObservable() { + const Observable = (0, optional_observable_1.getObservable)(); + return new Observable((subscriber => { + const subscription = this.getObservable().subscribe(( /*value*/) => { + const newArray = this.getArray(); + subscriber.next(newArray); + }); + return function unsubscribe() { + subscription.unsubscribe(); + }; + })); } - // TODO else if (value instanceof DataDocument) { return VALUE_TYPES.DOCUMENT; } - else if (typeof value === 'string') { - return exports.VALUE_TYPES.STRING; + /** + * Gets an ordered array representation of the items in your object collection. The items in the array + * are proxied values, changes will be in sync with the database. Note that the array itself + * is not mutable: adding or removing items to it will NOT update the collection in the + * the database and vice versa. Use `add`, `delete`, `sort` and `move` methods to make changes + * that impact the collection's sorting order + * @returns order array + */ + getArray() { + const arr = proxyAccess(this.collection).toArray((a, b) => a[this.orderProperty] - b[this.orderProperty]); + // arr.push = (...items: T[]) => { + // items.forEach(item => this.add(item)); + // return arr.length; + // }; + return arr; } - else if (typeof value === 'object') { - return exports.VALUE_TYPES.OBJECT; + /** + * Adds or moves an item to/within the object collection and takes care of the proper sorting order. + * @param item Item to add or move + * @param index Optional target index in the sorted representation, appends if not specified. + * @param from If the item is being moved + * @returns + */ + add(item, index, from) { + const arr = this.getArray(); + let minOrder = Number.POSITIVE_INFINITY, maxOrder = Number.NEGATIVE_INFINITY; + for (let i = 0; i < arr.length; i++) { + const order = arr[i][this.orderProperty]; + minOrder = Math.min(order, minOrder); + maxOrder = Math.max(order, maxOrder); + } + let fromKey; + if (typeof from === 'number') { + // Moving existing item + fromKey = Object.keys(this.collection).find(key => this.collection[key] === item); + if (!fromKey) { + throw new Error('item not found in collection'); + } + if (from === index) { + return { key: fromKey, index }; + } + if (Math.abs(from - index) === 1) { + // Position being swapped, swap their order property values + const otherItem = arr[index]; + const otherOrder = otherItem[this.orderProperty]; + otherItem[this.orderProperty] = item[this.orderProperty]; + item[this.orderProperty] = otherOrder; + return { key: fromKey, index }; + } + else { + // Remove from array, code below will add again + arr.splice(from, 1); + } + } + if (typeof index !== 'number' || index >= arr.length) { + // append at the end + index = arr.length; + item[this.orderProperty] = (arr.length == 0 ? 0 : maxOrder + this.orderIncrement); + } + else if (index === 0) { + // insert before all others + item[this.orderProperty] = (arr.length == 0 ? 0 : minOrder - this.orderIncrement); + } + else { + // insert between 2 others + const orders = arr.map(item => item[this.orderProperty]); + const gap = orders[index] - orders[index - 1]; + if (gap > 1) { + item[this.orderProperty] = (orders[index] - Math.floor(gap / 2)); + } + else { + // TODO: Can this gap be enlarged by moving one of both orders? + // For now, change all other orders + arr.splice(index, 0, item); + for (let i = 0; i < arr.length; i++) { + arr[i][this.orderProperty] = (i * this.orderIncrement); + } + } + } + const key = typeof fromKey === 'string' + ? fromKey // Moved item, don't add it + : proxyAccess(this.collection).push(item); + return { key, index }; } - else if (typeof value === 'number') { - return exports.VALUE_TYPES.NUMBER; + /** + * Deletes an item from the object collection using the their index in the sorted array representation + * @param index + * @returns the key of the collection's child that was deleted + */ + delete(index) { + const arr = this.getArray(); + const item = arr[index]; + if (!item) { + throw new Error(`Item at index ${index} not found`); + } + const key = Object.keys(this.collection).find(key => this.collection[key] === item); + if (!key) { + throw new Error('Cannot find target object to delete'); + } + this.collection[key] = null; // Deletes it from db + return { key, index }; } - else if (typeof value === 'boolean') { - return exports.VALUE_TYPES.BOOLEAN; + /** + * Moves an item in the object collection by reordering it + * @param fromIndex Current index in the array (the ordered representation of the object collection) + * @param toIndex Target index in the array + * @returns + */ + move(fromIndex, toIndex) { + const arr = this.getArray(); + return this.add(arr[fromIndex], toIndex, fromIndex); } - else if (typeof value === 'bigint') { - return exports.VALUE_TYPES.BIGINT; + /** + * Reorders the object collection using given sort function. Allows quick reordering of the collection which is persisted in the database + * @param sortFn + */ + sort(sortFn) { + const arr = this.getArray(); + arr.sort(sortFn); + for (let i = 0; i < arr.length; i++) { + arr[i][this.orderProperty] = i * this.orderIncrement; + } } - throw new Error(`Unknown value type: ${value}`); } -exports.getValueType = getValueType; +exports.OrderedCollectionProxy = OrderedCollectionProxy; -},{"acebase-core":46}],15:[function(require,module,exports){ +},{"./data-reference":8,"./data-snapshot":9,"./id":11,"./optional-observable":14,"./path-info":16,"./path-reference":17,"./process":18,"./simple-event-emitter":22,"./utils":27}],8:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.NotSupported = void 0; -class NotSupported { - constructor(context = 'browser') { throw new Error(`This feature is not supported in ${context} context`); } +exports.DataReferencesArray = exports.DataSnapshotsArray = exports.DataReferenceQuery = exports.DataReference = exports.QueryDataRetrievalOptions = exports.DataRetrievalOptions = void 0; +const data_snapshot_1 = require("./data-snapshot"); +const subscription_1 = require("./subscription"); +const id_1 = require("./id"); +const path_info_1 = require("./path-info"); +const data_proxy_1 = require("./data-proxy"); +const optional_observable_1 = require("./optional-observable"); +class DataRetrievalOptions { + /** + * Options for data retrieval, allows selective loading of object properties + */ + 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.cache_mode === 'string' && !['allow', 'bypass', 'force'].includes(options.cache_mode)) { + throw new TypeError('invalid value for options.cache_mode'); + } + this.include = options.include || undefined; + this.exclude = options.exclude || undefined; + this.child_objects = typeof options.child_objects === 'boolean' ? options.child_objects : undefined; + this.cache_mode = typeof options.cache_mode === 'string' + ? options.cache_mode + : typeof options.allow_cache === 'boolean' + ? options.allow_cache ? 'allow' : 'bypass' + : 'allow'; + this.cache_cursor = typeof options.cache_cursor === 'string' ? options.cache_cursor : undefined; + } } -exports.NotSupported = NotSupported; - -},{}],16:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.pfs = void 0; -class pfs { - static get hasFileSystem() { return false; } - static get fs() { return null; } +exports.DataRetrievalOptions = DataRetrievalOptions; +class QueryDataRetrievalOptions extends DataRetrievalOptions { + /** + * @param options Options for data retrieval, allows selective loading of object properties + */ + constructor(options) { + super(options); + if (!['undefined', 'boolean'].includes(typeof options.snapshots)) { + throw new TypeError('options.snapshots must be a boolean'); + } + this.snapshots = typeof options.snapshots === 'boolean' ? options.snapshots : true; + } } -exports.pfs = pfs; - -},{}],17:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.executeQuery = void 0; -const acebase_core_1 = require("acebase-core"); -const node_value_types_1 = require("./node-value-types"); -const node_errors_1 = require("./node-errors"); -const data_index_1 = require("./data-index"); -const async_task_batch_1 = require("./async-task-batch"); -// eslint-disable-next-line @typescript-eslint/no-empty-function -const noop = () => { }; -/** - * - * @param storage Target storage instance - * @param path Path of the object collection to perform query on - * @param query Query to execute - * @param options Additional options - * @returns Returns a promise that resolves with matching data or paths in `results` - */ -async function executeQuery(api, path, query, options = { snapshots: false, include: undefined, exclude: undefined, child_objects: undefined, eventHandler: noop }) { - var _a, _b, _c, _d, _e, _f; - // TODO: Refactor to async - if (typeof options !== 'object') { - options = {}; +exports.QueryDataRetrievalOptions = QueryDataRetrievalOptions; +const _private = Symbol('private'); +class DataReference { + /** + * Creates a reference to a node + */ + constructor(db, path, vars) { + this.db = db; + if (!path) { + path = ''; + } + path = path.replace(/^\/|\/$/g, ''); // Trim slashes + const pathInfo = path_info_1.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 || {}, + context: {}, + pushed: false, + cursor: null, + }; } - if (typeof options.snapshots === 'undefined') { - options.snapshots = false; + context(context, merge = false) { + const currentContext = this[_private].context; + if (typeof context === 'object') { + const newContext = context ? merge ? currentContext || {} : context : {}; + if (context) { + // Merge new with current context + Object.keys(context).forEach(key => { + newContext[key] = context[key]; + }); + } + this[_private].context = newContext; + return this; + } + else if (typeof context === 'undefined') { + console.warn('Use snap.context() instead of snap.ref.context() to get updating context in event callbacks'); + return currentContext; + } + else { + throw new Error('Invalid context argument'); + } } - const context = {}; - if ((_a = api.storage.settings.transactions) === null || _a === void 0 ? void 0 : _a.log) { - context.acebase_cursor = acebase_core_1.ID.generate(); + /** + * Contains the last received cursor for this referenced path (if the connected database has transaction logging enabled). + * If you want to be notified if this value changes, add a handler with `ref.onCursor(callback)` + */ + get cursor() { + return this[_private].cursor; } - const queryFilters = query.filters.map(f => (Object.assign({}, f))); - const querySort = query.order.map(s => (Object.assign({}, s))); - const sortMatches = (matches) => { - matches.sort((a, b) => { - const compare = (i) => { - const o = querySort[i]; - const trailKeys = acebase_core_1.PathInfo.getPathKeys(typeof o.key === 'number' ? `[${o.key}]` : o.key); - const left = trailKeys.reduce((val, key) => val !== null && typeof val === 'object' && key in val ? val[key] : null, a.val); - const right = trailKeys.reduce((val, key) => val !== null && typeof val === 'object' && key in val ? val[key] : null, b.val); - if (left === null) { - return right === null ? 0 : o.ascending ? -1 : 1; + set cursor(value) { + var _a; + this[_private].cursor = value; + (_a = this.onCursor) === null || _a === void 0 ? void 0 : _a.call(this, value); + } + /** + * The path this instance was created with + */ + get path() { return this[_private].path; } + /** + * The key or index of this node + */ + get key() { + const key = this[_private].key; + return typeof key === 'number' ? `[${key}]` : key; + } + /** + * If the "key" is a number, it is an index! + */ + get index() { + const key = this[_private].key; + if (typeof key !== 'number') { + throw new Error(`"${key}" is not a number`); + } + return key; + } + /** + * Returns a new reference to this node's parent + */ + get parent() { + const currentPath = path_info_1.PathInfo.fillVariables2(this.path, this.vars); + const info = path_info_1.PathInfo.get(currentPath); + if (info.parentPath === null) { + return null; + } + return new DataReference(this.db, info.parentPath).context(this[_private].context); + } + /** + * Contains values of the variables/wildcards used in a subscription path if this reference was + * created by an event ("value", "child_added" etc), or in a type mapping path when serializing / instantiating typed objects + */ + get vars() { + return this[_private].vars; + } + /** + * Returns a new reference to a child node + * @param childPath Child key, index or path + * @returns reference to the child + */ + child(childPath) { + childPath = typeof childPath === 'number' ? childPath : childPath.replace(/^\/|\/$/g, ''); + const currentPath = path_info_1.PathInfo.fillVariables2(this.path, this.vars); + const targetPath = path_info_1.PathInfo.getChildPath(currentPath, childPath); + return new DataReference(this.db, targetPath).context(this[_private].context); // `${this.path}/${childPath}` + } + /** + * Sets or overwrites the stored value + * @param value value to store in database + * @param onComplete optional completion callback to use instead of returning promise + * @returns promise that resolves with this reference when completed + */ + async set(value, onComplete) { + try { + if (this.isWildcardPath) { + throw new Error(`Cannot set the value of wildcard path "/${this.path}"`); + } + 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 undefined value in "/${this.path}"`); + } + if (!this.db.isReady) { + await this.db.ready(); + } + value = this.db.types.serialize(this.path, value); + const { cursor } = await this.db.api.set(this.path, value, { context: this[_private].context }); + this.cursor = cursor; + if (typeof onComplete === 'function') { + try { + onComplete(null, this); } - if (right === null) { - return o.ascending ? 1 : -1; + catch (err) { + console.error('Error in onComplete callback:', err); } - // TODO: add collation options using Intl.Collator. Note this also has to be implemented in the matching engines (inclusing indexes) - // See discussion https://github.com/appy-one/acebase/discussions/27 - if (left == right) { - if (i < querySort.length - 1) { - return compare(i + 1); - } - else { - return a.path < b.path ? -1 : 1; - } // Sort by path if property values are equal + } + } + catch (err) { + if (typeof onComplete === 'function') { + try { + onComplete(err, this); } - else if (left < right) { - return o.ascending ? -1 : 1; + catch (err) { + console.error('Error in onComplete callback:', err); } - // else if (left > right) { - return o.ascending ? 1 : -1; - // } - }; - return compare(0); - }); - }; - const loadResultsData = async (preResults, options) => { - // Limit the amount of concurrent getValue calls by batching them - if (preResults.length === 0) { - return []; + } + else { + // throw again + throw err; + } } - const maxBatchSize = 50; - const batch = new async_task_batch_1.AsyncTaskBatch(maxBatchSize); - const results = []; - preResults.forEach(({ path }, index) => batch.add(async () => { - const node = await api.storage.getNode(path, options); - const val = node.value; - if (val === null) { - // Record was deleted, but index isn't updated yet? - api.logger.warn(`Indexed result "/${path}" does not have a record!`); - // TODO: let index rebuild - return; + return this; + } + /** + * Updates properties of the referenced node + * @param updates containing the properties to update + * @param onComplete optional completion callback to use instead of returning promise + * @return returns promise that resolves with this reference once completed + */ + async update(updates, onComplete) { + try { + if (this.isWildcardPath) { + throw new Error(`Cannot update the value of wildcard path "/${this.path}"`); } - const result = { path, val }; - if (stepsExecuted.sorted) { - // Put the result in the same index as the preResult was - results[index] = result; + if (!this.db.isReady) { + await this.db.ready(); } - else { - results.push(result); - if (!stepsExecuted.skipped && results.length > query.skip + Math.abs(query.take)) { - // we can toss a value! sort, toss last one - sortMatches(results); - results.pop(); // Always toss last value, results have been sorted already - } + if (typeof updates !== 'object' || updates instanceof Array || updates instanceof ArrayBuffer || updates instanceof Date) { + await this.set(updates); } - })); - await batch.finish(); - return results; - }; - const pathInfo = acebase_core_1.PathInfo.get(path); - const isWildcardPath = pathInfo.keys.some(key => key === '*' || key.toString().startsWith('$')); // path.includes('*'); - const availableIndexes = api.storage.indexes.get(path); - const usingIndexes = []; - // eslint-disable-next-line @typescript-eslint/no-empty-function - let stop = async () => { }; - if (isWildcardPath) { - // Check if path contains $vars with explicit filter values. If so, execute multiple queries and merge results - const vars = pathInfo.keys.filter(key => typeof key === 'string' && key.startsWith('$')); - const hasExplicitFilterValues = vars.length > 0 && vars.every(v => query.filters.some(f => f.key === v && ['==', 'in'].includes(f.op))); - const isRealtime = typeof options.monitor === 'object' && [(_b = options.monitor) === null || _b === void 0 ? void 0 : _b.add, (_c = options.monitor) === null || _c === void 0 ? void 0 : _c.change, (_d = options.monitor) === null || _d === void 0 ? void 0 : _d.remove].some(val => val === true); - if (hasExplicitFilterValues && !isRealtime) { - // create path combinations - const combinations = []; - for (const v of vars) { - const filters = query.filters.filter(f => f.key === v); - const filterValues = filters.reduce((values, f) => { - if (f.op === '==') { - values.push(f.compare); - } - if (f.op === 'in') { - if (!(f.compare instanceof Array)) { - throw new Error(`compare argument for 'in' operator must be an Array`); - } - values.push(...f.compare); - } - return values; - }, []); - // Expand all current combinations with these filter values - const prevCombinations = combinations.splice(0); - filterValues.forEach(fv => { - if (prevCombinations.length === 0) { - combinations.push({ [v]: fv }); - } - else { - combinations.push(...prevCombinations.map(c => (Object.assign(Object.assign({}, c), { [v]: fv })))); - } - }); + else if (Object.keys(updates).length === 0) { + console.warn(`update called on path "/${this.path}", but there is nothing to update`); } - // create queries - const filters = query.filters.filter(f => !vars.includes(f.key)); - const paths = combinations.map(vars => acebase_core_1.PathInfo.get(acebase_core_1.PathInfo.getPathKeys(path).map(key => { var _a; return (_a = vars[key]) !== null && _a !== void 0 ? _a : key; })).path); - const loadData = query.order.length > 0; - const promises = paths.map(path => { - var _a; - return executeQuery(api, path, { filters, take: 0, skip: 0, order: [] }, { - snapshots: loadData, - cache_mode: options.cache_mode, - include: [...((_a = options.include) !== null && _a !== void 0 ? _a : []), ...query.order.map(o => o.key)], - exclude: options.exclude, - }); - }); - const resultSets = await Promise.all(promises); - let results = resultSets.reduce((results, set) => (results.push(...set.results), results), []); - if (loadData) { - sortMatches(results); + else { + updates = this.db.types.serialize(this.path, updates); + const { cursor } = await this.db.api.update(this.path, updates, { context: this[_private].context }); + this.cursor = cursor; } - if (query.skip > 0) { - results.splice(0, query.skip); + if (typeof onComplete === 'function') { + try { + onComplete(null, this); + } + catch (err) { + console.error('Error in onComplete callback:', err); + } } - if (query.take > 0) { - results.splice(query.take); + } + catch (err) { + if (typeof onComplete === 'function') { + try { + onComplete(err, this); + } + catch (err) { + console.error('Error in onComplete callback:', err); + } } - if (options.snapshots && (!loadData || ((_e = options.include) === null || _e === void 0 ? void 0 : _e.length) > 0 || ((_f = options.exclude) === null || _f === void 0 ? void 0 : _f.length) > 0 || !options.child_objects)) { - const { include, exclude, child_objects } = options; - results = await loadResultsData(results, { include, exclude, child_objects }); + else { + // throw again + throw err; } - return { results, context: null, stop }; - // const results = options.snapshots ? results } - else if (availableIndexes.length === 0) { - // Wildcard paths require data to be indexed - const err = new Error(`Query on wildcard path "/${path}" requires an index`); - return Promise.reject(err); + return this; + } + /** + * Sets the value a node using a transaction: it runs your callback function with the current value, uses its return value as the new value to store. + * The transaction is canceled if your callback returns undefined, or throws an error. If your callback returns null, the target node will be removed. + * @param callback - callback function that performs the transaction on the node's current value. It must return the new value to store (or promise with new value), undefined to cancel the transaction, or null to remove the node. + * @returns returns a promise that resolves with the DataReference once the transaction has been processed + */ + async transaction(callback) { + if (this.isWildcardPath) { + throw new Error(`Cannot start a transaction on wildcard path "/${this.path}"`); } - if (queryFilters.length === 0) { - // Filterless query on wildcard path. Use first available index with filter on non-null key value (all results) - const index = availableIndexes.filter((index) => index.type === 'normal')[0]; - queryFilters.push({ key: index.key, op: '!=', compare: null }); + if (!this.db.isReady) { + await this.db.ready(); + } + let throwError; + const cb = (currentValue) => { + currentValue = this.db.types.deserialize(this.path, currentValue); + const snap = new data_snapshot_1.DataSnapshot(this, currentValue); + let newValue; + try { + newValue = callback(snap); + } + catch (err) { + // callback code threw an error + throwError = err; // Remember error + return; // cancel transaction by returning undefined + } + if (newValue instanceof Promise) { + return newValue + .then((val) => { + return this.db.types.serialize(this.path, val); + }) + .catch(err => { + throwError = err; // Remember error + return; // cancel transaction by returning undefined + }); + } + else { + return this.db.types.serialize(this.path, newValue); + } + }; + const { cursor } = await this.db.api.transaction(this.path, cb, { context: this[_private].context }); + this.cursor = cursor; + if (throwError) { + // Rethrow error from callback code + throw throwError; } + return this; } - // Check if there are path specific indexes - // eg: index on "users/$uid/posts", key "$uid", including "title" (or key "title", including "$uid") - // Which are very useful for queries on "users/98sdfkb37/posts" with filter or sort on "title" - // const indexesOnPath = availableIndexes - // .map(index => { - // if (!index.path.includes('$')) { return null; } - // const pattern = '^' + index.path.replace(/(\$[a-z0-9_]+)/gi, (match, name) => `(?<${name}>[a-z0-9_]+|\\*)`) + '$'; - // const re = new RegExp(pattern, 'i'); - // const match = path.match(re); - // const canBeUsed = index.key[0] === '$' - // ? match.groups[index.key] !== '*' // Index key value MUST be present in the path - // : null !== ourFilters.find(filter => filter.key === index.key); // Index key MUST be in a filter - // if (!canBeUsed) { return null; } - // return { - // index, - // wildcards: match.groups, // eg: { "$uid": "98sdfkb37" } - // filters: Object.keys(match.groups).filter(name => match.groups[name] !== '*').length - // } - // }) - // .filter(info => info !== null) - // .sort((a, b) => { - // a.filters > b.filters ? -1 : 1 - // }); - // TODO: - // if (ourFilters.length === 0 && indexesOnPath.length > 0) { - // ourFilters = ourFilters.concat({ key: }) - // usingIndexes.push({ index: filter.index, description: filter.index.description}); - // } - queryFilters.forEach(filter => { - if (filter.index) { - // Index has been assigned already - return; + on(event, callback, cancelCallback) { + if (this.path === '' && ['value', 'child_changed'].includes(event)) { + // Removed 'notify_value' and 'notify_child_changed' events from the list, they do not require additional data loading anymore. + console.warn('WARNING: Listening for value and child_changed events on the root node is a bad practice. These events require loading of all data (value event), or potentially lots of data (child_changed event) each time they are fired'); } - // // Check if there are path indexes we can use - // const pathIndexesWithKey = DataIndex.validOperators.includes(filter.op) - // ? indexesOnPath.filter(info => info.index.key === filter.key || info.index.includeKeys.includes(filter.key)) - // : []; - // Check if there are indexes on this filter key - const indexesOnKey = availableIndexes - .filter(index => index.key === filter.key) - .filter(index => { - return index.validOperators.includes(filter.op); - }); - if (indexesOnKey.length >= 1) { - // If there are multiple indexes on 1 key (happens when index includes other keys), - // we should check other .filters and .order to determine the best one to use - // TODO: Create a good strategy here... - const otherFilterKeys = queryFilters.filter(f => f !== filter).map(f => f.key); - const sortKeys = querySort.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.includes(key)); - const forSorting = availableKeys.filter(key => sortKeys.includes(key)); - const forBoth = forOtherFilters.concat(forSorting.filter(index => !forOtherFilters.includes(index))); - 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, points: points.total, filterKeys: forOtherFilters, sortKeys: forSorting }; - }); - // Use index with the most points - beneficialIndexes.sort((a, b) => a.points > b.points ? -1 : 1); - const bestBenificialIndex = beneficialIndexes[0]; - // Assign to this filter - filter.index = bestBenificialIndex.index; - // Assign to other filters and sorts - bestBenificialIndex.filterKeys.forEach(key => { - queryFilters.filter(f => f !== filter && f.key === key).forEach(f => { - if (!data_index_1.DataIndex.validOperators.includes(f.op)) { - // The used operator for this filter is invalid for use on metadata - // Probably because it is an Array/Fulltext/Geo query operator - return; + let eventPublisher = null; + const eventStream = new subscription_1.EventStream(publisher => { eventPublisher = publisher; }); + // Map OUR callback to original callback, so .off can remove the right callback(s) + const cb = { + event, + stream: eventStream, + userCallback: typeof callback === 'function' && callback, + ourCallback: (err, path, newValue, oldValue, eventContext) => { + if (err) { + // TODO: Investigate if this ever happens? + this.db.logger.error(`Error getting data for event ${event} on path "${path}"`, err); + return; + } + const ref = this.db.ref(path); + ref[_private].vars = path_info_1.PathInfo.extractVariables(this.path, path); + let callbackObject; + if (event.startsWith('notify_')) { + // No data event, callback with reference + callbackObject = ref.context(eventContext || {}); + } + else { + const values = { + previous: this.db.types.deserialize(path, oldValue), + current: this.db.types.deserialize(path, newValue), + }; + if (event === 'child_removed') { + callbackObject = new data_snapshot_1.DataSnapshot(ref, values.previous, true, values.previous, eventContext); + } + else if (event === 'mutations') { + callbackObject = new data_snapshot_1.MutationsDataSnapshot(ref, values.current, eventContext); + } + else { + const isRemoved = event === 'mutated' && values.current === null; + callbackObject = new data_snapshot_1.DataSnapshot(ref, values.current, isRemoved, values.previous, eventContext); + } + } + eventPublisher.publish(callbackObject); + if (eventContext === null || eventContext === void 0 ? void 0 : eventContext.acebase_cursor) { + this.cursor = eventContext.acebase_cursor; + } + }, + }; + this[_private].callbacks.push(cb); + const subscribe = () => { + // (NEW) Add callback to event stream + // ref.on('value', callback) is now exactly the same as ref.on('value').subscribe(callback) + if (typeof callback === 'function') { + eventStream.subscribe(callback, (activated, cancelReason) => { + if (!activated) { + cancelCallback && cancelCallback(cancelReason); } - f.indexUsage = 'filter'; - f.index = bestBenificialIndex.index; - }); - }); - bestBenificialIndex.sortKeys.forEach(key => { - querySort.filter(s => s.key === key).forEach(s => { - s.index = bestBenificialIndex.index; }); - }); - } - if (filter.index) { - usingIndexes.push({ index: filter.index, description: filter.index.description }); - } - }); - if (querySort.length > 0 && query.take !== 0 && queryFilters.length === 0) { - // Check if we can use assign an index to sorts in a filterless take & sort query - querySort.forEach(sort => { - if (sort.index) { - // Index has been assigned already - return; } - sort.index = availableIndexes - .filter(index => index.key === sort.key) - .find(index => index.type === 'normal'); - // if (sort.index) { - // usingIndexes.push({ index: sort.index, description: `${sort.index.description} (for sorting)`}); - // } - }); - } - // const usingIndexes = ourFilters.map(filter => filter.index).filter(index => index); - const indexDescriptions = usingIndexes.map(index => index.description).join(', '); - usingIndexes.length > 0 && api.logger.info(`Using indexes for query: ${indexDescriptions}`); - // Filters that should run on all nodes after indexed results: - const tableScanFilters = queryFilters.filter(filter => !filter.index); - // Check if there are filters that require an index to run (such as "fulltext:contains", and "geo:nearby" etc) - 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); - } - // Check if the filters are using valid operators - const allowedTableScanOperators = ['<', '<=', '==', '!=', '>=', '>', 'like', '!like', 'in', '!in', 'matches', '!matches', 'between', '!between', 'has', '!has', 'contains', '!contains', 'exists', '!exists']; // DISABLED "custom" because it is not fully implemented and only works locally - for (let i = 0; i < tableScanFilters.length; i++) { - const f = tableScanFilters[i]; - if (!allowedTableScanOperators.includes(f.op)) { - return Promise.reject(new Error(`query contains unknown filter operator "${f.op}" on path "${path}", key "${f.key}"`)); - } - } - // Check if the available indexes are sufficient for this wildcard query - if (isWildcardPath && tableScanFilters.length > 0) { - // There are unprocessed filters, which means the fields aren't indexed. - // We're not going to get all data of a wildcard path to query manually. - // Indexes must be created - const keys = tableScanFilters.reduce((keys, f) => { - if (keys.indexOf(f.key) < 0) { - keys.push(f.key); + const advancedOptions = typeof callback === 'object' + ? callback + : { newOnly: !callback }; // newOnly: if callback is not 'truthy', could change this to (typeof callback !== 'function' && callback !== true) but that would break client code that uses a truthy argument. + if (typeof advancedOptions.newOnly !== 'boolean') { + advancedOptions.newOnly = false; } - 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); - } - // Run queries on available indexes - const indexScanPromises = []; - queryFilters.forEach(filter => { - if (filter.index && filter.indexUsage !== 'filter') { - let promise = filter.index.query(filter.op, filter.compare) - .then(results => { - var _a, _b; - (_a = options.eventHandler) === null || _a === void 0 ? void 0 : _a.call(options, { name: 'stats', type: 'index_query', source: filter.index.description, stats: results.stats }); - if (results.hints.length > 0) { - (_b = options.eventHandler) === null || _b === void 0 ? void 0 : _b.call(options, { name: 'hints', type: 'index_query', source: filter.index.description, hints: results.hints }); + if (this.isWildcardPath) { + advancedOptions.newOnly = true; + } + const cancelSubscription = (err) => { + // Access denied? + // Cancel subscription + const callbacks = this[_private].callbacks; + callbacks.splice(callbacks.indexOf(cb), 1); + this.db.api.unsubscribe(this.path, event, cb.ourCallback); + // Call cancelCallbacks + this.db.logger.error(`Subscription "${event}" on path "/${this.path}" canceled because of an error: ${err.message}`); + eventPublisher.cancel(err.message); + }; + const authorized = this.db.api.subscribe(this.path, event, cb.ourCallback, { newOnly: advancedOptions.newOnly, cancelCallback: cancelSubscription, syncFallback: advancedOptions.syncFallback }); + const allSubscriptionsStoppedCallback = () => { + const callbacks = this[_private].callbacks; + callbacks.splice(callbacks.indexOf(cb), 1); + return this.db.api.unsubscribe(this.path, event, cb.ourCallback); + }; + if (authorized instanceof Promise) { + // Web API now returns a promise that resolves if the request is allowed + // and rejects when access is denied by the set security rules + authorized.then(() => { + // Access granted + eventPublisher.start(allSubscriptionsStoppedCallback); + }).catch(cancelSubscription); + } + else { + // Local API, always authorized + eventPublisher.start(allSubscriptionsStoppedCallback); + } + if (!advancedOptions.newOnly) { + // If callback param is supplied (either a callback function or true or something else truthy), + // it will fire events for current values right now. + // Otherwise, it expects the .subscribe methode to be used, which will then + // only be called for future events + if (event === 'value') { + this.get(snap => { + eventPublisher.publish(snap); + }); } - return results; - }); - // Get other filters that can be executed on these indexed results (eg filters on included keys of the index) - const resultFilters = queryFilters.filter(f => f.index === filter.index && f.indexUsage === 'filter'); - if (resultFilters.length > 0) { - // Hook into the promise - promise = promise.then(results => { - resultFilters.forEach(filter => { - const { key, op, index } = filter; - let { compare } = filter; - if (typeof compare === 'string' && !index.caseSensitive) { - compare = compare.toLocaleLowerCase(index.textLocale); + else if (event === 'child_added') { + this.get(snap => { + const val = snap.val(); + if (val === null || typeof val !== 'object') { + return; } - results = results.filterMetadata(key, op, compare); + Object.keys(val).forEach(key => { + const childSnap = new data_snapshot_1.DataSnapshot(this.child(key), val[key]); + eventPublisher.publish(childSnap); + }); }); - return results; - }); + } + else if (event === 'notify_child_added') { + // 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, limit = step; + let skip = 0; + const more = async () => { + const children = await this.db.api.reflect(this.path, 'children', { limit, skip }); + children.list.forEach(child => { + const childRef = this.child(child.key); + eventPublisher.publish(childRef); + // typeof callback === 'function' && callback(childRef); + }); + if (children.more) { + skip += step; + more(); + } + }; + more(); + } } - indexScanPromises.push(promise); + }; + if (this.db.isReady) { + subscribe(); } - }); - const stepsExecuted = { - filtered: queryFilters.length === 0, - skipped: query.skip === 0, - taken: query.take === 0, - sorted: querySort.length === 0, - preDataLoaded: false, - dataLoaded: false, - }; - if (queryFilters.length === 0 && query.take === 0) { - api.logger.warn(`Filterless queries must use .take to limit the results. Defaulting to 100 for query on path "${path}"`); - query.take = 100; + else { + this.db.ready(subscribe); + } + return eventStream; } - if (querySort.length > 0 && querySort[0].index) { - const sortIndex = querySort[0].index; - const ascending = query.take < 0 ? !querySort[0].ascending : querySort[0].ascending; - if (queryFilters.length === 0 && querySort.slice(1).every(s => sortIndex.allMetadataKeys.includes(s.key))) { - api.logger.info(`Using index for sorting: ${sortIndex.description}`); - const metadataSort = querySort.slice(1).map(s => { - s.index = sortIndex; // Assign index to skip later processing of this sort operation - return { key: s.key, ascending: s.ascending }; - }); - const promise = sortIndex.take(query.skip, Math.abs(query.take), { ascending, metadataSort }) - .then(results => { - var _a, _b; - (_a = options.eventHandler) === null || _a === void 0 ? void 0 : _a.call(options, { name: 'stats', type: 'sort_index_take', source: sortIndex.description, stats: results.stats }); - if (results.hints.length > 0) { - (_b = options.eventHandler) === null || _b === void 0 ? void 0 : _b.call(options, { name: 'hints', type: 'sort_index_take', source: sortIndex.description, hints: results.hints }); - } - return results; - }); - indexScanPromises.push(promise); - stepsExecuted.skipped = true; - stepsExecuted.taken = true; - stepsExecuted.sorted = true; + off(event, callback) { + const subscriptions = this[_private].callbacks; + const stopSubs = subscriptions.filter(sub => (!event || sub.event === event) && (!callback || sub.userCallback === callback)); + if (stopSubs.length === 0) { + this.db.logger.warn(`Can't find event subscriptions to stop (path: "${this.path}", event: ${event || '(any)'}, callback: ${callback})`); } - // else if (queryFilters.every(f => [sortIndex.key, ...sortIndex.includeKeys].includes(f.key))) { - // TODO: If an index can be used for sorting, and all filter keys are included in its metadata: query the index! - // Implement: - // sortIndex.query(ourFilters); - // etc - // } + stopSubs.forEach(sub => { + sub.stream.stop(); + }); + return this; } - return Promise.all(indexScanPromises) - .then(async (indexResultSets) => { - // Merge all results in indexResultSets, get distinct nodes - 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 < b.length ? -1 : 1); // Sort results, shortest result set first - const shortestSet = indexResultSets[0]; - const otherSets = indexResultSets.slice(1); - indexedResults = shortestSet.reduce((results, match) => { - // Check if the key is present in the other result sets - const result = { key: match.key, path: match.path, val: { [shortestSet.filterKey]: match.value } }; - const matchedInAllSets = otherSets.every(set => set.findIndex(m => m.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; + get(optionsOrCallback, callback) { + if (!this.db.isReady) { + const promise = this.db.ready().then(() => this.get(optionsOrCallback, callback)); + return typeof optionsOrCallback !== 'function' && typeof callback !== 'function' ? promise : undefined; // only return promise if no callback is used } - if (isWildcardPath || (indexScanPromises.length > 0 && tableScanFilters.length === 0)) { - if (querySort.length === 0 || querySort.every(o => o.index)) { - // No sorting, or all sorts are on indexed keys. We can use current index results - stepsExecuted.preDataLoaded = true; - if (!stepsExecuted.sorted && querySort.length > 0) { - sortMatches(indexedResults); - } - stepsExecuted.sorted = true; - if (!stepsExecuted.skipped && query.skip > 0) { - indexedResults = query.take < 0 - ? indexedResults.slice(0, -query.skip) - : indexedResults.slice(query.skip); - } - if (!stepsExecuted.taken && query.take !== 0) { - indexedResults = query.take < 0 - ? indexedResults.slice(query.take) - : indexedResults.slice(0, query.take); - } - stepsExecuted.skipped = true; - stepsExecuted.taken = true; - if (!options.snapshots) { - return indexedResults; - } - // TODO: exclude already known key values, merge loaded with known - const childOptions = { include: options.include, exclude: options.exclude, child_objects: options.child_objects }; - return loadResultsData(indexedResults, childOptions) - .then(results => { - stepsExecuted.dataLoaded = true; - return results; - }); + callback = + typeof optionsOrCallback === 'function' + ? optionsOrCallback + : typeof callback === 'function' + ? callback + : undefined; + if (this.isWildcardPath) { + const error = new Error(`Cannot get value of wildcard path "/${this.path}". Use .query() instead`); + if (typeof callback === 'function') { + throw error; } - if (options.snapshots || !stepsExecuted.sorted) { - const loadPartialResults = querySort.length > 0; - const childOptions = loadPartialResults - ? { include: querySort.map(order => order.key) } - : { include: options.include, exclude: options.exclude, child_objects: options.child_objects }; - return loadResultsData(indexedResults, childOptions) - .then(results => { - if (querySort.length > 0) { - sortMatches(results); - } - stepsExecuted.sorted = true; - if (query.skip > 0) { - results = query.take < 0 - ? results.slice(0, -query.skip) - : results.slice(query.skip); - } - if (query.take !== 0) { - results = query.take < 0 - ? results.slice(query.take) - : results.slice(0, query.take); - } - stepsExecuted.skipped = true; - stepsExecuted.taken = true; - if (options.snapshots && loadPartialResults) { - // Get the rest - return loadResultsData(results, { include: options.include, exclude: options.exclude, child_objects: options.child_objects }); - } - return results; - }); + return Promise.reject(error); + } + const options = new DataRetrievalOptions(typeof optionsOrCallback === 'object' ? optionsOrCallback : { cache_mode: 'allow' }); + const promise = this.db.api.get(this.path, options).then(result => { + var _a; + const isNewApiResult = ('context' in result && 'value' in result); + if (!isNewApiResult) { + // acebase-core version package was updated but acebase or acebase-client package was not? Warn, but don't throw an error. + console.warn('AceBase api.get method returned an old response value. Update your acebase or acebase-client package'); + result = { value: result, context: {} }; } - else { - // No need to take further actions, return what we have now - return indexedResults; + const value = this.db.types.deserialize(this.path, result.value); + const snapshot = new data_snapshot_1.DataSnapshot(this, value, undefined, undefined, result.context); + if ((_a = result.context) === null || _a === void 0 ? void 0 : _a.acebase_cursor) { + this.cursor = result.context.acebase_cursor; } - } - // If we get here, this is a query on a regular path (no wildcards) with additional non-indexed filters left, - // we can get child records from a single parent. Merge index results by key - let indexKeyFilter; - if (indexedResults.length > 0) { - indexKeyFilter = indexedResults.map(result => result.key); - } - let matches = []; - let preliminaryStop = false; - const loadPartialData = querySort.length > 0; - const childOptions = loadPartialData - ? { include: querySort.map(order => order.key) } - : { include: options.include, exclude: options.exclude, child_objects: options.child_objects }; - const batch = { - promises: [], - async add(promise) { - this.promises.push(promise); - if (this.promises.length >= 1000) { - await Promise.all(this.promises.splice(0)); - } - }, - }; - try { - await api.storage.getChildren(path, { keyFilter: indexKeyFilter, async: true }).next(child => { - if (child.type !== node_value_types_1.VALUE_TYPES.OBJECT) { - return; - } - if (!child.address) { - // Currently only happens if object has no properties - // ({}, stored as a tiny_value in parent record). In that case, - // should it be matched in any query? -- That answer could be YES, when testing a property for !exists. Ignoring for now - return; - } - if (preliminaryStop) { - return false; - } - const matchNode = async () => { - const isMatch = await api.storage.matchNode(child.address.path, tableScanFilters); - if (!isMatch) { - return; - } - const childPath = child.address.path; - let result; - if (options.snapshots || querySort.length > 0) { - const node = await api.storage.getNode(childPath, childOptions); - result = { path: childPath, val: node.value }; - } - else { - result = { path: childPath }; - } - // If a maximumum number of results is requested, we can check if we can preliminary toss this result - // This keeps the memory space used limited to skip + take - // TODO: see if we can limit it to the max number of results returned (.take) - matches.push(result); - if (query.take !== 0 && matches.length > Math.abs(query.take) + query.skip) { - if (querySort.length > 0) { - // A query order has been set. If this value falls in between it can replace some other value - // matched before. - sortMatches(matches); - } - else if (query.take > 0) { - // No query order set, we can stop after 'take' + 'skip' results - preliminaryStop = true; // Flags the loop that no more nodes have to be checked - } - // const ascending = querySort.length === 0 || (query.take >= 0 ? querySort[0].ascending : !querySort[0].ascending); - // if (ascending) { - // matches.pop(); // ascending sort order, toss last value - // } - // else { - // matches.shift(); // descending, toss first value - // } - matches.pop(); // Always toss last value, results have been sorted already - } - }; - const p = batch.add(matchNode()); - if (p instanceof Promise) { - // If this returns a promise, child iteration should pause automatically - return p; - } + return snapshot; + }); + if (callback) { + promise.then(callback).catch(err => { + console.error('Uncaught error:', err); }); + return; } - catch (reason) { - // No record? - if (!(reason instanceof node_errors_1.NodeNotFoundError)) { - api.logger.warn(`Error getting child stream: ${reason}`); - } - return []; - } - // Done iterating all children, wait for all match promises to resolve - await Promise.all(batch.promises); - stepsExecuted.preDataLoaded = loadPartialData; - stepsExecuted.dataLoaded = !loadPartialData; - if (querySort.length > 0) { - sortMatches(matches); + else { + return promise; } - stepsExecuted.sorted = true; - if (query.skip > 0) { - matches = query.take < 0 - ? matches.slice(0, -query.skip) - : matches.slice(query.skip); + } + /** + * Waits for an event to occur + * @param event Name of the event, eg "value", "child_added", "child_changed", "child_removed" + * @param options data retrieval options, to include or exclude specific child keys + * @returns returns promise that resolves with a snapshot of the data + */ + once(event, options) { + if (event === 'value' && !this.isWildcardPath) { + // Shortcut, do not start listening for future events + return this.get(options); } - stepsExecuted.skipped = true; - if (query.take !== 0) { - // (should not be necessary, basically it has already been done in the loop?) - matches = query.take < 0 - ? matches.slice(query.take) - : matches.slice(0, query.take); + return new Promise((resolve) => { + const callback = (snap) => { + this.off(event, callback); // unsubscribe directly + resolve(snap); + }; + this.on(event, callback); + }); + } + /** + * @param value optional value to store into the database right away + * @param onComplete optional callback function to run once value has been stored + * @returns returns promise that resolves with the reference after the passed value has been stored + */ + push(value, onComplete) { + if (this.isWildcardPath) { + const error = new Error(`Cannot push to wildcard path "/${this.path}"`); + if (typeof value === 'undefined' || typeof onComplete === 'function') { + throw error; + } + return Promise.reject(error); } - stepsExecuted.taken = true; - if (!stepsExecuted.dataLoaded) { - matches = await loadResultsData(matches, { include: options.include, exclude: options.exclude, child_objects: options.child_objects }); - stepsExecuted.dataLoaded = true; + const id = id_1.ID.generate(); + const ref = this.child(id); + ref[_private].pushed = true; + if (typeof value !== 'undefined') { + return ref.set(value, onComplete).then(() => ref); } - return matches; - }) - .then(matches => { - // Order the results - if (!stepsExecuted.sorted && querySort.length > 0) { - sortMatches(matches); + else { + return ref; } - if (!options.snapshots) { - // Remove the loaded values from the results, because they were not requested (and aren't complete, we only have data of the sorted keys) - matches = matches.map(match => match.path); + } + /** + * Removes this node and all children + */ + async remove() { + if (this.isWildcardPath) { + throw new Error(`Cannot remove wildcard path "/${this.path}". Use query().remove instead`); } - // Limit result set - if (!stepsExecuted.skipped && query.skip > 0) { - matches = query.take < 0 - ? matches.slice(0, -query.skip) - : matches.slice(query.skip); + if (this.parent === null) { + throw new Error('Cannot remove the root node'); } - if (!stepsExecuted.taken && query.take !== 0) { - matches = query.take < 0 - ? matches.slice(query.take) - : matches.slice(0, query.take); + return this.set(null); + } + /** + * Quickly checks if this reference has a value in the database, without returning its data + * @returns returns a promise that resolves with a boolean value + */ + async exists() { + if (this.isWildcardPath) { + throw new Error(`Cannot check wildcard path "/${this.path}" existence`); } - // NEW: Check if this is a realtime query - future updates must send query result updates - if (options.monitor === true) { - options.monitor = { add: true, change: true, remove: true }; + if (!this.db.isReady) { + await this.db.ready(); } - if (typeof options.monitor === 'object' && (options.monitor.add || options.monitor.change || options.monitor.remove)) { - // TODO: Refactor this to use 'mutations' event instead of 'notify_child_*' - const monitor = options.monitor; - const matchedPaths = options.snapshots ? matches.map(match => match.path) : matches.slice(); - const ref = api.db.ref(path); - const removeMatch = (path) => { - const index = matchedPaths.indexOf(path); - if (index < 0) { + return this.db.api.exists(this.path); + } + get isWildcardPath() { + return this.path.indexOf('*') >= 0 || this.path.indexOf('$') >= 0; + } + /** + * Creates a query object for current node + */ + query() { + return new DataReferenceQuery(this); + } + /** + * Gets the number of children this node has, uses reflection + */ + async count() { + const info = await this.reflect('info', { child_count: true }); + return info.children.count; + } + async reflect(type, args) { + if (this.isWildcardPath) { + throw new Error(`Cannot reflect on wildcard path "/${this.path}"`); + } + if (!this.db.isReady) { + await this.db.ready(); + } + return this.db.api.reflect(this.path, type, args); + } + async export(write, options = { format: 'json', type_safe: true }) { + if (this.isWildcardPath) { + throw new Error(`Cannot export wildcard path "/${this.path}"`); + } + if (!this.db.isReady) { + await this.db.ready(); + } + const writeFn = typeof write === 'function' ? write : write.write.bind(write); + return this.db.api.export(this.path, writeFn, options); + } + /** + * Imports the value of this node and all children + * @param read Function that reads data from your stream + * @param options Only supported format currently is json + * @returns returns a promise that resolves once all data is imported + */ + async import(read, options = { format: 'json', suppress_events: false }) { + if (this.isWildcardPath) { + throw new Error(`Cannot import to wildcard path "/${this.path}"`); + } + if (!this.db.isReady) { + await this.db.ready(); + } + return this.db.api.import(this.path, read, options); + } + proxy(options) { + const isOptionsArg = typeof options === 'object' && (typeof options.cursor !== 'undefined' || typeof options.defaultValue !== 'undefined'); + if (typeof options !== 'undefined' && !isOptionsArg) { + this.db.logger.warn('Warning: live data proxy is being initialized with a deprecated method signature. Use ref.proxy(options) instead of ref.proxy(defaultValue)'); + options = { defaultValue: options }; + } + return data_proxy_1.LiveDataProxy.create(this, options); + } + /** + * @param options optional initial data retrieval options. + * Not recommended to use yet - given includes/excludes are not applied to received mutations, + * or sync actions when using an AceBaseClient with cache db. + */ + observe(options) { + // options should not be used yet - we can't prevent/filter mutation events on excluded paths atm + if (options) { + throw new Error('observe does not support data retrieval options yet'); + } + if (this.isWildcardPath) { + throw new Error(`Cannot observe wildcard path "/${this.path}"`); + } + const Observable = (0, optional_observable_1.getObservable)(); + return new Observable((observer => { + let cache, resolved = false; + let promise = this.get(options).then(snap => { + resolved = true; + cache = snap.val(); + observer.next(cache); + }); + const updateCache = (snap) => { + if (!resolved) { + promise = promise.then(() => updateCache(snap)); return; } - matchedPaths.splice(index, 1); - }; - const addMatch = (path) => { - if (matchedPaths.includes(path)) { - return; + const mutatedPath = snap.ref.path; + if (mutatedPath === this.path) { + cache = snap.val(); + return observer.next(cache); } - matchedPaths.push(path); + const trailKeys = path_info_1.PathInfo.getPathKeys(mutatedPath).slice(path_info_1.PathInfo.getPathKeys(this.path).length); + let target = cache; + while (trailKeys.length > 1) { + const key = trailKeys.shift(); + if (!(key in target)) { + // Happens if initial loaded data did not include / excluded this data, + // or we missed out on an event + target[key] = typeof trailKeys[0] === 'number' ? [] : {}; + } + target = target[key]; + } + const prop = trailKeys.shift(); + const newValue = snap.val(); + if (newValue === null) { + // Remove it + target instanceof Array && typeof prop === 'number' ? target.splice(prop, 1) : delete target[prop]; + } + else { + // Set or update it + target[prop] = newValue; + } + observer.next(cache); }; - const stopMonitoring = () => { - api.unsubscribe(ref.path, 'child_changed', childChangedCallback); - api.unsubscribe(ref.path, 'child_added', childAddedCallback); - api.unsubscribe(ref.path, 'notify_child_removed', childRemovedCallback); + this.on('mutated', updateCache); // TODO: Refactor to 'mutations' event instead + // Return unsubscribe function + return () => { + this.off('mutated', updateCache); }; - stop = async () => { stopMonitoring(); }; - const childChangedCallback = async (err, path, newValue, oldValue) => { - const wasMatch = matchedPaths.includes(path); - let keepMonitoring = true; - // check if the properties we already have match filters, - // and if we have to check additional properties - const checkKeys = []; - queryFilters.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 = queryFilters.filter(filter => filter.key === key); - return filters.every(filter => { - var _a; - if (((_a = filter.index) === null || _a === void 0 ? void 0 : _a.textLocaleKey) && !seenKeys.includes(filter.index.textLocaleKey)) { - // Can't check because localeKey is missing - missingKeys.push(filter.index.textLocaleKey); - return true; // so we'll know if all others did match - } - else if (allowedTableScanOperators.includes(filter.op)) { - return api.storage.test(newValue[key], filter.op, filter.compare); - } - else { - // specific index filter - return filter.index.test(newValue, filter.op, filter.compare); - } - }); - }); - if (isMatch) { - // Matches all checked (updated) keys. BUT. Did we have all data needed? - // If it was a match before, other properties don't matter because they didn't change and won't - // change the current outcome - missingKeys.push(...checkKeys.filter(key => !seenKeys.includes(key))); - // let promise = Promise.resolve(true); - if (!wasMatch && missingKeys.length > 0) { - // We have to check if this node becomes a match - const filterQueue = queryFilters.filter(f => missingKeys.includes(f.key)); - const simpleFilters = filterQueue.filter(f => allowedTableScanOperators.includes(f.op)); - const indexFilters = filterQueue.filter(f => !allowedTableScanOperators.includes(f.op)); - if (simpleFilters.length > 0) { - isMatch = await api.storage.matchNode(path, simpleFilters); - } - if (isMatch && indexFilters.length > 0) { - // TODO: ask index what keys to load (eg: FullTextIndex might need key specified by localeKey) - const keysToLoad = indexFilters.reduce((keys, filter) => { - if (!keys.includes(filter.key)) { - keys.push(filter.key); - } - if (filter.index instanceof data_index_1.FullTextIndex && filter.index.config.localeKey && !keys.includes(filter.index.config.localeKey)) { - keys.push(filter.index.config.localeKey); - } - return keys; - }, []); - const node = await api.storage.getNode(path, { include: keysToLoad }); - if (node.value === null) { - return false; - } - isMatch = indexFilters.every(filter => filter.index.test(node.value, filter.op, filter.compare)); - } - } - } - if (isMatch) { - if (!wasMatch) { - addMatch(path); - } - // load missing data if snapshots are requested - if (options.snapshots) { - const loadOptions = { include: options.include, exclude: options.exclude, child_objects: options.child_objects }; - const node = await api.storage.getNode(path, loadOptions); - newValue = node.value; - } - if (wasMatch && monitor.change) { - keepMonitoring = options.eventHandler({ name: 'change', path, value: newValue }) !== false; - } - else if (!wasMatch && monitor.add) { - keepMonitoring = options.eventHandler({ name: 'add', path, value: newValue }) !== false; - } - } - else if (wasMatch) { - removeMatch(path); - if (monitor.remove) { - keepMonitoring = options.eventHandler({ name: 'remove', path: path, value: oldValue }) !== false; - } - } - if (keepMonitoring === false) { - stopMonitoring(); - } - }; - const childAddedCallback = (err, path, newValue) => { - const isMatch = queryFilters.every(filter => { - if (allowedTableScanOperators.includes(filter.op)) { - return api.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 (monitor.add) { - keepMonitoring = options.eventHandler({ name: 'add', path: path, value: options.snapshots ? newValue : null }) !== false; - } - } - if (keepMonitoring === false) { - stopMonitoring(); - } - }; - const childRemovedCallback = (err, path, newValue, oldValue) => { - let keepMonitoring = true; - removeMatch(path); - if (monitor.remove) { - keepMonitoring = options.eventHandler({ name: 'remove', path: path, value: options.snapshots ? oldValue : null }) !== false; - } - if (keepMonitoring === false) { - stopMonitoring(); - } - }; - if (options.monitor.add || options.monitor.change || options.monitor.remove) { - // Listen for child_changed events - api.subscribe(ref.path, 'child_changed', childChangedCallback); - } - if (options.monitor.remove) { - api.subscribe(ref.path, 'notify_child_removed', childRemovedCallback); - } - if (options.monitor.add) { - api.subscribe(ref.path, 'child_added', childAddedCallback); - } - } - return { results: matches, context, stop }; - }); -} -exports.executeQuery = executeQuery; - -},{"./async-task-batch":5,"./data-index":7,"./node-errors":11,"./node-value-types":14,"acebase-core":46}],18:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.AceBaseStorage = exports.AceBaseStorageSettings = void 0; -const not_supported_1 = require("../../not-supported"); -/** - * Not supported in browser context - */ -class AceBaseStorageSettings extends not_supported_1.NotSupported { -} -exports.AceBaseStorageSettings = AceBaseStorageSettings; -/** - * Not supported in browser context - */ -class AceBaseStorage extends not_supported_1.NotSupported { -} -exports.AceBaseStorage = AceBaseStorage; - -},{"../../not-supported":15}],19:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.createIndex = void 0; -const acebase_core_1 = require("acebase-core"); -const data_index_1 = require("../data-index"); -const promise_fs_1 = require("../promise-fs"); -/** -* Creates an index on specified path and key(s) -* @param path location of objects to be indexed. Eg: "users" to index all children of the "users" node; or "chats/*\/members" to index all members of all chats -* @param key for now - one key to index. Once our B+tree implementation supports nested trees, we can allow multiple fields -*/ -async function createIndex(context, path, key, options) { - if (!context.storage.indexes.supported) { - throw new Error('Indexes are not supported in current environment because it requires Node.js fs'); - } - // path = path.replace(/\/\*$/, ""); // Remove optional trailing "/*" - const { ipc, logger, indexes, storage } = context; - 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 && options.config) { - // Additional index config params are not saved to index files, apply them to the in-memory index now - existingIndex.config = options.config; - } - if (existingIndex && rebuild !== true) { - logger.info(`Index on "/${path}/*/${key}" already exists`.colorize(acebase_core_1.ColorStyle.inverse)); - return existingIndex; + })); } - if (!ipc.isMaster) { - // Pass create request to master - const result = await ipc.sendRequest({ type: 'index.create', path, key, options }); - if (result.ok) { - return storage.indexes.add(result.fileName); + async forEach(callbackOrOptions, callback) { + let options; + if (typeof callbackOrOptions === 'function') { + callback = callbackOrOptions; } - throw new Error(result.reason); - } - await promise_fs_1.pfs.mkdir(`${storage.settings.path}/${storage.name}.acebase`).catch(err => { - if (err.code !== 'EEXIST') { - throw err; + else { + options = callbackOrOptions; } - }); - const index = existingIndex || (() => { - const { include, caseSensitive, textLocale, textLocaleKey } = options; - const indexOptions = { include, caseSensitive, textLocale, textLocaleKey }; - switch (indexType) { - case 'array': return new data_index_1.ArrayIndex(storage, path, key, Object.assign({}, indexOptions)); - case 'fulltext': return new data_index_1.FullTextIndex(storage, path, key, Object.assign(Object.assign({}, indexOptions), { config: options.config })); - case 'geo': return new data_index_1.GeoIndex(storage, path, key, Object.assign({}, indexOptions)); - default: return new data_index_1.DataIndex(storage, path, key, Object.assign({}, indexOptions)); + if (typeof callback !== 'function') { + throw new TypeError('No callback function given'); } - })(); - if (!existingIndex) { - indexes.push(index); + // Get all children through reflection. This could be tweaked further using paging + const info = await this.reflect('children', { limit: 0, skip: 0 }); // Gets ALL child keys + const summary = { + canceled: false, + total: info.list.length, + processed: 0, + }; + // Iterate through all children until callback returns false + for (let i = 0; i < info.list.length; i++) { + const key = info.list[i].key; + // Get child data + const snapshot = await this.child(key).get(options); + summary.processed++; + if (!snapshot.exists()) { + // Was removed in the meantime, skip + continue; + } + // Run callback + const result = await callback(snapshot); + if (result === false) { + summary.canceled = true; + break; // Stop looping + } + } + return summary; } - try { - await index.build(); + async getMutations(cursorOrDate) { + const cursor = typeof cursorOrDate === 'string' ? cursorOrDate : undefined; + const timestamp = cursorOrDate === null || typeof cursorOrDate === 'undefined' ? 0 : cursorOrDate instanceof Date ? cursorOrDate.getTime() : undefined; + return this.db.api.getMutations({ path: this.path, cursor, timestamp }); } - catch (err) { - context.logger.error(`Index build on "/${path}/*/${key}" failed: ${err.message} (code: ${err.code})`.colorize(acebase_core_1.ColorStyle.red)); - if (!existingIndex) { - // Only remove index if we added it. Build may have failed because someone tried creating the index more than once, or rebuilding it while it was building... - indexes.splice(indexes.indexOf(index), 1); - } - throw err; + async getChanges(cursorOrDate) { + const cursor = typeof cursorOrDate === 'string' ? cursorOrDate : undefined; + const timestamp = cursorOrDate === null || typeof cursorOrDate === 'undefined' ? 0 : cursorOrDate instanceof Date ? cursorOrDate.getTime() : undefined; + return this.db.api.getChanges({ path: this.path, cursor, timestamp }); } - ipc.sendNotification({ type: 'index.created', fileName: index.fileName, path, key, options }); - return index; } -exports.createIndex = createIndex; - -},{"../data-index":7,"../promise-fs":16,"acebase-core":46}],20:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.CustomStorageHelpers = void 0; -const acebase_core_1 = require("acebase-core"); -/** - * Helper functions to build custom storage classes with - */ -class CustomStorageHelpers { +exports.DataReference = DataReference; +class DataReferenceQuery { /** - * Helper function that returns a SQL where clause for all children of given path - * @param path Path to get children of - * @param columnName Name of the Path column in your SQL db, default is 'path' - * @returns Returns the SQL where clause + * Creates a query on a reference */ - 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; + constructor(ref) { + this.ref = ref; + this[_private] = { + filters: [], + skip: 0, + take: 0, + order: [], + events: {}, + }; } /** - * Helper function that returns a regular expression to test if paths are children of the given path - * @param path Path to test children of - * @returns Returns regular expression to test paths with - */ - static ChildPathsRegex(path) { - return new RegExp(`^${path}(?:/[^/[]+|\\[[0-9]+\\])$`); - } - /** - * Helper function that returns a SQL where clause for all descendants of given path - * @param path Path to get descendants of - * @param columnName Name of the Path column in your SQL db, default is 'path' - * @returns Returns the SQL where clause + * Applies a filter to the children of the refence being queried. + * If there is an index on the property key being queried, it will be used + * to speed up the query + * @param key property to test value of + * @param op operator to use + * @param compare value to compare with */ - static DescendantPathsSql(path, columnName = 'path') { - const where = path === '' - ? `${columnName} <> ''` - : `${columnName} LIKE '${path}/%' OR ${columnName} LIKE '${path}[%'`; - return where; + 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`); + } + // DISABLED 2019/10/23 because it is not fully implemented only works locally + // if (op === "custom" && typeof compare !== "function") { + // throw `${op} filter for ${key} must supply a Function compare argument`; + // } + // DISABLED 2022/08/15, implemented by query.ts in acebase + // 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, op, compare }); + return this; } /** - * Helper function that returns a regular expression to test if paths are descendants of the given path - * @param path Path to test descendants of - * @returns Returns regular expression to test paths with + * @deprecated use `.filter` instead */ - static DescendantPathsRegex(path) { - return new RegExp(`^${path}(?:/[^/[]+|\\[[0-9]+\\])`); + where(key, op, compare) { + return this.filter(key, op, compare); } /** - * PathInfo helper class. Can be used to extract keys from a given path, get parent paths, check if a path is a child or descendant of other path etc - * @example - * var pathInfo = CustomStorage.PathInfo.get('my/path/to/data'); - * pathInfo.key === 'data'; - * pathInfo.parentPath === 'my/path/to'; - * pathInfo.pathKeys; // ['my','path','to','data']; - * pathInfo.isChildOf('my/path/to') === true; - * pathInfo.isDescendantOf('my/path') === true; - * pathInfo.isParentOf('my/path/to/data/child') === true; - * pathInfo.isAncestorOf('my/path/to/data/child/grandchild') === true; - * pathInfo.childPath('child') === 'my/path/to/data/child'; - * pathInfo.childPath(0) === 'my/path/to/data[0]'; + * Limits the number of query results */ - static get PathInfo() { - return acebase_core_1.PathInfo; + take(n) { + this[_private].take = n; + return this; } -} -exports.CustomStorageHelpers = CustomStorageHelpers; - -},{"acebase-core":46}],21:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.CustomStorage = exports.CustomStorageNodeInfo = exports.CustomStorageNodeAddress = exports.CustomStorageSettings = exports.CustomStorageTransaction = exports.ICustomStorageNode = exports.ICustomStorageNodeMetaData = exports.CustomStorageHelpers = void 0; -const acebase_core_1 = require("acebase-core"); -const { compareValues } = acebase_core_1.Utils; -const node_info_1 = require("../../node-info"); -const node_lock_1 = require("../../node-lock"); -const node_value_types_1 = require("../../node-value-types"); -const node_errors_1 = require("../../node-errors"); -const index_1 = require("../index"); -const helpers_1 = require("./helpers"); -const node_address_1 = require("../../node-address"); -const assert_1 = require("../../assert"); -var helpers_2 = require("./helpers"); -Object.defineProperty(exports, "CustomStorageHelpers", { enumerable: true, get: function () { return helpers_2.CustomStorageHelpers; } }); -/** Interface for metadata being stored for nodes */ -class ICustomStorageNodeMetaData { - constructor() { - /** cuid (time sortable revision id). Nodes stored in the same operation share this id */ - this.revision = ''; - /** Number of revisions, starting with 1. Resets to 1 after deletion and recreation */ - this.revision_nr = 0; - /** Creation date/time in ms since epoch UTC */ - this.created = 0; - /** Last modification date/time in ms since epoch UTC */ - this.modified = 0; - /** Type of the node's value. 1=object, 2=array, 3=number, 4=boolean, 5=string, 6=date, 7=reserved, 8=binary, 9=reference */ - this.type = 0; + /** + * Skips the first n query results + */ + skip(n) { + this[_private].skip = n; + return this; } -} -exports.ICustomStorageNodeMetaData = ICustomStorageNodeMetaData; -/** Interface for metadata combined with a stored value */ -class ICustomStorageNode extends ICustomStorageNodeMetaData { - constructor() { - super(); - /** only Object, Array, large string and binary values. */ - this.value = null; + sort(key, ascending = true) { + if (!['string', 'number'].includes(typeof key)) { + throw 'key must be a string or number'; + } + this[_private].order.push({ key, ascending }); + return this; } -} -exports.ICustomStorageNode = ICustomStorageNode; -/** Enables get/set/remove operations to be wrapped in transactions to improve performance and reliability. */ -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 + * @deprecated use `.sort` instead */ - constructor(target) { - this.production = false; // dev mode by default - this.target = { - get originalPath() { return target.path; }, - path: target.path, - get write() { return target.write; }, + order(key, ascending = true) { + return this.sort(key, ascending); + } + get(optionsOrCallback, callback) { + if (!this.ref.db.isReady) { + const promise = this.ref.db.ready().then(() => this.get(optionsOrCallback, callback)); + return typeof optionsOrCallback !== 'function' && typeof callback !== 'function' ? promise : undefined; // only return promise if no callback is used + } + callback = + typeof optionsOrCallback === 'function' + ? optionsOrCallback + : typeof callback === 'function' + ? callback + : undefined; + const options = new QueryDataRetrievalOptions(typeof optionsOrCallback === 'object' ? optionsOrCallback : { snapshots: true, cache_mode: 'allow' }); + options.allow_cache = options.cache_mode !== 'bypass'; // Backward compatibility when using older acebase-client + options.eventHandler = ev => { + // TODO: implement context for query events + if (!this[_private].events[ev.name]) { + return false; + } + const listeners = this[_private].events[ev.name]; + if (typeof listeners !== 'object' || listeners.length === 0) { + return false; + } + if (['add', 'change', 'remove'].includes(ev.name)) { + const eventData = { + name: ev.name, + ref: new DataReference(this.ref.db, ev.path), + }; + if (options.snapshots && ev.name !== 'remove') { + const val = db.types.deserialize(ev.path, ev.value); + eventData.snapshot = new data_snapshot_1.DataSnapshot(eventData.ref, val, false); + } + ev = eventData; + } + listeners.forEach(callback => { + var _a, _b; + try { + callback(ev); + } + catch (err) { + this.ref.db.logger.error(`Error executing "${ev.name}" event handler of realtime query on path "${this.ref.path}": ${(_b = (_a = err === null || err === void 0 ? void 0 : err.stack) !== null && _a !== void 0 ? _a : err === null || err === void 0 ? void 0 : err.message) !== null && _b !== void 0 ? _b : err}`); + } + }); }; - this.id = acebase_core_1.ID.generate(); + // Check if there are event listeners set for realtime changes + options.monitor = { add: false, change: false, remove: false }; + if (this[_private].events) { + if (this[_private].events['add'] && this[_private].events['add'].length > 0) { + options.monitor.add = true; + } + if (this[_private].events['change'] && this[_private].events['change'].length > 0) { + options.monitor.change = true; + } + if (this[_private].events['remove'] && this[_private].events['remove'].length > 0) { + options.monitor.remove = true; + } + } + // Stop realtime results if they are still enabled on a previous .get on this instance + this.stop(); + // NOTE: returning promise here, regardless of callback argument. Good argument to refactor method to async/await soon + const db = this.ref.db; + return db.api.query(this.ref.path, this[_private], options) + .catch(err => { + throw new Error(err); + }) + .then(res => { + const { stop } = res; + let { results, context } = res; + this.stop = async () => { + await stop(); + }; + if (!('results' in res && 'context' in res)) { + console.warn('Query results missing context. Update your acebase and/or acebase-client packages'); + results = res, context = {}; + } + if (options.snapshots) { + const snaps = results.map(result => { + const val = db.types.deserialize(result.path, result.val); + return new data_snapshot_1.DataSnapshot(db.ref(result.path), val, false, undefined, context); + }); + return DataSnapshotsArray.from(snaps); + } + else { + const refs = results.map(path => db.ref(path)); + return DataReferencesArray.from(refs); + } + }) + .then(results => { + callback && callback(results); + return results; + }); } /** - * Returns the number of children stored in their own records. This implementation uses `childrenOf` to count, override if storage supports a quicker way. - * Eg: For SQL databases, you can implement this with a single query like `SELECT count(*) FROM nodes WHERE ${CustomStorageHelpers.ChildPathsSql(path)}` - * @param path - * @returns Returns a promise that resolves with the number of children + * Stops a realtime query, no more notifications will be received. */ - async getChildCount(path) { - let childCount = 0; - await this.childrenOf(path, { metadata: false, value: false }, () => { childCount++; return false; }); - return childCount; + async stop() { + // Overridden by .get } /** - * NOT USED YET - * Default implementation of getMultiple that executes .get for each given path. Override for custom logic - * @param paths - * @returns Returns promise with a Map of paths to nodes + * Executes the query and returns references. Short for `.get({ snapshots: false })` + * @param callback callback to use instead of returning a promise + * @returns returns an Promise that resolves with an array of DataReferences, or void when using a callback + * @deprecated Use `find` instead */ - async getMultiple(paths) { - const map = new Map(); - await Promise.all(paths.map(path => this.get(path).then(val => map.set(path, val)))); - return map; + getRefs(callback) { + return this.get({ snapshots: false }, callback); } /** - * NOT USED YET - * Default implementation of setMultiple that executes .set for each given path. Override for custom logic - * @param nodes + * Executes the query and returns an array of references. Short for `.get({ snapshots: false })` */ - async setMultiple(nodes) { - await Promise.all(nodes.map(({ path, node }) => this.set(path, node))); + find() { + return this.get({ snapshots: false }); } /** - * Default implementation of removeMultiple that executes .remove for each given path. Override for custom logic - * @param paths + * Executes the query and returns the number of results */ - async removeMultiple(paths) { - await Promise.all(paths.map(path => this.remove(path))); + async count() { + const refs = await this.find(); + return refs.length; } /** - * @returns {Promise} + * Executes the query and returns if there are any results */ - async commit() { throw new Error(`CustomStorageTransaction.rollback must be overridden by subclass`); } - /** - * 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 targetPath + async exists() { + const originalTake = this[_private].take; + const p = this.take(1).find(); + this.take(originalTake); + const refs = await p; + return refs.length !== 0; + } + /** + * Executes the query, removes all matches from the database + * @returns returns a Promise that resolves once all matches have been removed */ - 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 = helpers_1.CustomStorageHelpers.PathInfo.get(targetPath); - if (pathInfo.isParentOf(currentPath)) { - if (this._lock) { - this._lock = await this._lock.moveToParent(); + async remove(callback) { + const refs = await this.find(); + // Perform updates on each distinct parent collection (only 1 parent if this is not a wildcard path) + const parentUpdates = refs.reduce((parents, ref) => { + const parent = parents[ref.parent.path]; + if (!parent) { + parents[ref.parent.path] = [ref]; } + else { + parent.push(ref); + } + return parents; + }, {}); + const db = this.ref.db; + const promises = Object.keys(parentUpdates).map(async (parentPath) => { + const updates = refs.reduce((updates, ref) => { + updates[ref.key] = null; + return updates; + }, {}); + const ref = db.ref(parentPath); + try { + await ref.update(updates); + return { ref, success: true }; + } + catch (error) { + return { ref, success: false, error }; + } + }); + const results = await Promise.all(promises); + callback && callback(results); + return results; + } + on(event, callback) { + if (!this[_private].events[event]) { + this[_private].events[event] = []; } - else { - throw new Error(`Locking issue. Locked path "${this._lock.path}" is not a child/descendant of "${targetPath}"`); - } - this.target.path = targetPath; - return targetPath; + this[_private].events[event].push(callback); + return this; } -} -exports.CustomStorageTransaction = CustomStorageTransaction; -/** - * 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. - */ -class CustomStorageSettings extends index_1.StorageSettings { - constructor(settings) { - super(settings); - /** - * Whether default node locking should be used. - * Set to false if your storage backend disallows multiple simultanious write transactions. - * Set to true if your storage backend does not support transactions (eg LocalStorage) or allows - * multiple simultanious write transactions (eg AceBase binary). - * @default true - */ - this.locking = true; - if (typeof settings !== 'object') { - throw new Error('settings missing'); + /** + * Unsubscribes from (a) previously added event(s) + * @param event Name of the event + * @param callback callback function to remove + * @returns returns reference to this query + */ + off(event, callback) { + if (typeof event === 'undefined') { + this[_private].events = {}; + return this; } - if (typeof settings.ready !== 'function') { - throw new Error(`ready must be a function`); + if (!this[_private].events[event]) { + return this; } - if (typeof settings.getTransaction !== 'function') { - throw new Error(`getTransaction must be a function`); + if (typeof callback === 'undefined') { + delete this[_private].events[event]; + return this; } - this.name = settings.name; - // this.info = `${this.name || 'CustomStorage'} realtime database`; - this.locking = settings.locking !== false; - if (this.locking) { - this.lockTimeout = typeof settings.lockTimeout === 'number' ? settings.lockTimeout : 120; + const index = this[_private].events[event].indexOf(callback); + if (!~index) { + return this; } - this.ready = settings.ready; - // Hijack getTransaction to add locking - const useLocking = this.locking; - const nodeLocker = useLocking ? new node_lock_1.NodeLocker(console, this.lockTimeout) : null; - this.getTransaction = async ({ path, write }) => { - // console.log(`${write ? 'WRITE' : 'READ'} transaction requested for path "${path}"`) - const transaction = await settings.getTransaction({ path, write }); - (0, assert_1.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; + this[_private].events[event].splice(index, 1); + return this; + } + async forEach(callbackOrOptions, callback) { + let options; + if (typeof callbackOrOptions === 'function') { + callback = callbackOrOptions; + } + else { + options = callbackOrOptions; + } + if (typeof callback !== 'function') { + throw new TypeError('No callback function given'); + } + // Get all query results. This could be tweaked further using paging + const refs = await this.find(); + const summary = { + canceled: false, + total: refs.length, + processed: 0, }; + // Iterate through all children until callback returns false + for (let i = 0; i < refs.length; i++) { + const ref = refs[i]; + // Get child data + const snapshot = await ref.get(options); + summary.processed++; + if (!snapshot.exists()) { + // Was removed in the meantime, skip + continue; + } + // Run callback + const result = await callback(snapshot); + if (result === false) { + summary.canceled = true; + break; // Stop looping + } + } + return summary; } } -exports.CustomStorageSettings = CustomStorageSettings; -class CustomStorageNodeAddress { - constructor(containerPath) { - this.path = containerPath; +exports.DataReferenceQuery = DataReferenceQuery; +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()); } } -exports.CustomStorageNodeAddress = CustomStorageNodeAddress; -class CustomStorageNodeInfo extends node_info_1.NodeInfo { - constructor(info) { - super(info); - this.revision = info.revision; - this.revision_nr = info.revision_nr; - this.created = info.created; - this.modified = info.modified; +exports.DataSnapshotsArray = DataSnapshotsArray; +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); } } -exports.CustomStorageNodeInfo = CustomStorageNodeInfo; -class CustomStorage extends index_1.Storage { - constructor(dbname, settings, env) { - super(dbname, settings, env); - this._customImplementation = settings; - this._init(); +exports.DataReferencesArray = DataReferencesArray; + +},{"./data-proxy":7,"./data-snapshot":9,"./id":11,"./optional-observable":14,"./path-info":16,"./subscription":24}],9:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.MutationsDataSnapshot = exports.DataSnapshot = void 0; +const path_info_1 = require("./path-info"); +function getChild(snapshot, path, previous = false) { + if (!snapshot.exists()) { + return null; } - async _init() { - this.logger.info(`Database "${this.name}" details:`.colorize(acebase_core_1.ColorStyle.dim)); - this.logger.info(`- Type: CustomStorage`.colorize(acebase_core_1.ColorStyle.dim)); - this.logger.info(`- Path: ${this.settings.path}`.colorize(acebase_core_1.ColorStyle.dim)); - this.logger.info(`- Max inline value size: ${this.settings.maxInlineValueSize}`.colorize(acebase_core_1.ColorStyle.dim)); - this.logger.info(`- Autoremove undefined props: ${this.settings.removeVoidProperties}`.colorize(acebase_core_1.ColorStyle.dim)); - // Create root node if it's not there yet - 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'); + let child = previous ? snapshot.previous() : snapshot.val(); + if (typeof path === 'number') { + return child[path]; } - throwImplementationError(message) { - throw new Error(`CustomStorage "${this._customImplementation.name}" ${message}`); + path_info_1.PathInfo.getPathKeys(path).every(key => { + child = child[key]; + return typeof child !== 'undefined'; + }); + return child || null; +} +function getChildren(snapshot) { + if (!snapshot.exists()) { + return []; } - _storeNode(path, node, options) { - // serialize the value to store - 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: node_value_types_1.VALUE_TYPES.DATETIME, value: val.getTime() }; - } - else if (val instanceof acebase_core_1.PathReference) { - return { type: node_value_types_1.VALUE_TYPES.REFERENCE, value: val.path }; - } - else if (val instanceof ArrayBuffer) { - return { type: node_value_types_1.VALUE_TYPES.BINARY, value: acebase_core_1.ascii85.encode(val) }; - } - else if (typeof val === 'object') { - (0, assert_1.assert)(Object.keys(val).length === 0, 'child object stored in parent can only be empty'); - return val; + const 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 { + /** + * Indicates whether the node exists in the database + */ + exists() { return false; } + /** + * Creates a new DataSnapshot instance + */ + constructor(ref, value, isRemoved = false, prevValue, context) { + this.ref = ref; + this.val = () => { return value; }; + this.previous = () => { return prevValue; }; + this.exists = () => { + if (isRemoved) { + return false; } + return value !== null && typeof value !== 'undefined'; }; - const unprocessed = `Caller should have pre-processed the value by converting it to a string`; - if (node.type === node_value_types_1.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 < node.value.length; i++) { - obj[i] = node.value[i]; - } - node.value = obj; - } - if (node.type === node_value_types_1.VALUE_TYPES.BINARY && typeof node.value !== 'string') { - console.warn(`Unprocessed binary value. ${unprocessed}`); - node.value = acebase_core_1.ascii85.encode(node.value); - } - if (node.type === node_value_types_1.VALUE_TYPES.REFERENCE && node.value instanceof acebase_core_1.PathReference) { - console.warn(`Unprocessed path reference. ${unprocessed}`); - node.value = node.value.path; - } - if ([node_value_types_1.VALUE_TYPES.OBJECT, node_value_types_1.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 => { - node.value[key] = getTypedChildValue(original[key]); - }); - } - return options.transaction.set(path, node); + this.context = () => { return context || {}; }; } - _processReadNodeValue(node) { - const getTypedChildValue = (val) => { - // Typed value stored in parent record - if (val.type === node_value_types_1.VALUE_TYPES.BINARY) { - // binary stored in a parent record as a string - return acebase_core_1.ascii85.decode(val.value); - } - else if (val.type === node_value_types_1.VALUE_TYPES.DATETIME) { - // Date value stored as number - return new Date(val.value); - } - else if (val.type === node_value_types_1.VALUE_TYPES.REFERENCE) { - // Path reference stored as string - return new acebase_core_1.PathReference(val.value); - } - else { - throw new Error(`Unhandled child value type ${val.type}`); + /** + * Creates a `DataSnapshot` instance + * @internal (for internal use) + */ + static for(ref, value) { + return new DataSnapshot(ref, value); + } + child(path) { + // Create new snapshot for child data + const val = getChild(this, path, false); + const prev = getChild(this, path, true); + return new DataSnapshot(this.ref.child(path), val, false, prev); + } + /** + * Checks if the snapshot's value has a child with the given key or path + * @param path child key or path + */ + hasChild(path) { + return getChild(this, path) !== null; + } + /** + * Indicates whether the the snapshot's value has any child nodes + */ + hasChildren() { + return getChildren(this).length > 0; + } + /** + * The number of child nodes in this snapshot + */ + numChildren() { + return getChildren(this).length; + } + /** + * Runs a callback function for each child node in this snapshot until the callback returns false + * @param callback function that is called with a snapshot of each child node in this snapshot. + * Must return a boolean value that indicates whether to continue iterating or not. + */ + forEach(callback) { + const value = this.val(); + const prev = this.previous(); + return getChildren(this).every((key) => { + const snap = new DataSnapshot(this.ref.child(key), value[key], false, prev[key]); + return callback(snap); + }); + } + /** + * The key of the node's path + */ + get key() { return this.ref.key; } +} +exports.DataSnapshot = DataSnapshot; +class MutationsDataSnapshot extends DataSnapshot { + constructor(ref, mutations, context) { + super(ref, mutations, false, undefined, context); + /** + * Don't use this to get previous values of mutated nodes. + * Use `.previous` properties on the individual child snapshots instead. + * @throws Throws an error if you do use it. + */ + this.previous = () => { throw new Error('Iterate values to get previous values for each mutation'); }; + this.val = (warn = true) => { + if (warn) { + console.warn('Unless you know what you are doing, it is best not to use the value of a mutations snapshot directly. Use child methods and forEach to iterate the mutations instead'); } + return mutations; }; - switch (node.type) { - case node_value_types_1.VALUE_TYPES.ARRAY: - case node_value_types_1.VALUE_TYPES.OBJECT: { - // check if any value needs to be converted - // NOTE: Arrays are stored with numeric properties - const obj = node.value; - Object.keys(obj).forEach(key => { - const item = obj[key]; - if (typeof item === 'object' && 'type' in item) { - obj[key] = getTypedChildValue(item); - } - }); - node.value = obj; - break; - } - case node_value_types_1.VALUE_TYPES.BINARY: { - node.value = acebase_core_1.ascii85.decode(node.value); - break; - } - case node_value_types_1.VALUE_TYPES.REFERENCE: { - node.value = new acebase_core_1.PathReference(node.value); - break; - } - case node_value_types_1.VALUE_TYPES.STRING: { - // No action needed - // node.value = node.value; - break; - } - default: - throw new Error(`Invalid standalone record value type`); // should never happen - } } - async _readNode(path, options) { - // deserialize a stored value (always an object with "type", "value", "revision", "revision_nr", "created", "modified") - const node = await options.transaction.get(path); - if (node === null) { - return null; - } - if (typeof node !== 'object') { - this.throwImplementationError(`transaction.get must return an ICustomStorageNode object. Use JSON.parse if your set function stored it as a string`); - } - this._processReadNodeValue(node); - return node; + /** + * Runs a callback function for each mutation in this snapshot until the callback returns false + * @param callback function that is called with a snapshot of each mutation in this snapshot. Must return a boolean value that indicates whether to continue iterating or not. + * @returns Returns whether every child was interated + */ + forEach(callback) { + const mutations = this.val(false); + return mutations.every(mutation => { + const ref = mutation.target.reduce((ref, key) => ref.child(key), this.ref); + const snap = new DataSnapshot(ref, mutation.val, false, mutation.prev); + return callback(snap); + }); } - _getTypeFromStoredValue(val) { - let type; - if (typeof val === 'string') { - type = node_value_types_1.VALUE_TYPES.STRING; - } - else if (typeof val === 'number') { - type = node_value_types_1.VALUE_TYPES.NUMBER; - } - else if (typeof val === 'boolean') { - type = node_value_types_1.VALUE_TYPES.BOOLEAN; - } - else if (val instanceof Array) { - type = node_value_types_1.VALUE_TYPES.ARRAY; + child(index) { + if (typeof index !== 'number') { + throw new Error('child index must be a number'); } - else if (typeof val === 'object') { - if ('type' in val) { - const serialized = val; - type = serialized.type; - val = serialized.value; - if (type === node_value_types_1.VALUE_TYPES.DATETIME) { - val = new Date(val); - } - else if (type === node_value_types_1.VALUE_TYPES.REFERENCE) { - val = new acebase_core_1.PathReference(val); - } + const mutation = this.val(false)[index]; + const ref = mutation.target.reduce((ref, key) => ref.child(key), this.ref); + return new DataSnapshot(ref, mutation.val, false, mutation.prev); + } +} +exports.MutationsDataSnapshot = MutationsDataSnapshot; + +},{"./path-info":16}],10:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.DebugLogger = void 0; +const process_1 = require("./process"); +// eslint-disable-next-line @typescript-eslint/no-empty-function +const noop = () => { }; +class DebugLogger { + constructor(level = 'log', prefix = '') { + this.level = level; + this.prefix = prefix; + this.setLevel(level); + } + setLevel(level) { + const prefix = this.prefix ? this.prefix + ' %s' : ''; + this.verbose = ['verbose'].includes(level) ? prefix ? console.log.bind(console, prefix) : console.log.bind(console) : noop; + this.trace = ['verbose'].includes(level) ? prefix ? console.log.bind(console, prefix) : console.log.bind(console) : noop; + this.debug = ['verbose'].includes(level) ? prefix ? console.log.bind(console, prefix) : console.log.bind(console) : noop; + this.log = ['verbose', 'log'].includes(level) ? prefix ? console.log.bind(console, prefix) : console.log.bind(console) : noop; + this.info = ['verbose', 'log'].includes(level) ? prefix ? console.log.bind(console, prefix) : console.log.bind(console) : noop; + this.warn = ['verbose', 'log', 'warn'].includes(level) ? prefix ? console.warn.bind(console, prefix) : console.warn.bind(console) : noop; + this.error = ['verbose', 'log', 'warn', 'error'].includes(level) ? prefix ? console.error.bind(console, prefix) : console.error.bind(console) : noop; + this.fatal = ['verbose', 'log', 'warn', 'error'].includes(level) ? prefix ? console.error.bind(console, prefix) : console.error.bind(console) : noop; + this.write = (text) => { + const isRunKit = typeof process_1.default !== 'undefined' && process_1.default.env && typeof process_1.default.env.RUNKIT_ENDPOINT_PATH === 'string'; + if (text && isRunKit) { + text.split('\n').forEach(line => console.log(line)); // Logs each line separately } else { - type = node_value_types_1.VALUE_TYPES.OBJECT; + console.log(text); } - } - else { - throw new Error(`Unknown value type`); - } - return { type, value: val }; + }; } - /** - * 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 - */ - async _writeNode(path, value, options) { - if (!options.merge && 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`); +} +exports.DebugLogger = DebugLogger; + +},{"./process":18}],11:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ID = void 0; +const cuid_1 = require("./cuid"); +// const uuid62 = require('uuid62'); +let timeBias = 0; +class ID { + /** + * (for internal use) + * bias in milliseconds to adjust generated cuid timestamps with + */ + static set timeBias(bias) { + if (typeof bias !== 'number') { + return; } - // Check if the value for this node changed, to prevent recursive calls to - // perform unnecessary writes that do not change any data - if (typeof options.diff === 'undefined' && typeof options.currentValue !== 'undefined') { - const diff = compareValues(options.currentValue, value); - if (options.merge && typeof diff === 'object') { - diff.removed = diff.removed.filter(key => value[key] === null); // Only keep "removed" items that are really being removed by setting to null + timeBias = bias; + } + static generate() { + // Could also use https://www.npmjs.com/package/pushid for Firebase style 20 char id's + return (0, cuid_1.default)(timeBias).slice(1); // Cuts off the always leading 'c' + // return uuid62.v1(); + } +} +exports.ID = ID; + +},{"./cuid":5}],12:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ObjectCollection = exports.PartialArray = exports.SimpleObservable = exports.SchemaDefinition = exports.Colorize = exports.ColorStyle = exports.SimpleEventEmitter = exports.SimpleCache = exports.ascii85 = exports.PathInfo = exports.Utils = exports.TypeMappings = exports.Transport = exports.EventSubscription = exports.EventPublisher = exports.EventStream = exports.PathReference = exports.ID = exports.DebugLogger = exports.OrderedCollectionProxy = exports.proxyAccess = exports.MutationsDataSnapshot = exports.DataSnapshot = exports.DataReferencesArray = exports.DataSnapshotsArray = exports.QueryDataRetrievalOptions = exports.DataRetrievalOptions = exports.DataReferenceQuery = exports.DataReference = exports.Api = exports.AceBaseBaseSettings = exports.AceBaseBase = void 0; +var acebase_base_1 = require("./acebase-base"); +Object.defineProperty(exports, "AceBaseBase", { enumerable: true, get: function () { return acebase_base_1.AceBaseBase; } }); +Object.defineProperty(exports, "AceBaseBaseSettings", { enumerable: true, get: function () { return acebase_base_1.AceBaseBaseSettings; } }); +var api_1 = require("./api"); +Object.defineProperty(exports, "Api", { enumerable: true, get: function () { return api_1.Api; } }); +var data_reference_1 = require("./data-reference"); +Object.defineProperty(exports, "DataReference", { enumerable: true, get: function () { return data_reference_1.DataReference; } }); +Object.defineProperty(exports, "DataReferenceQuery", { enumerable: true, get: function () { return data_reference_1.DataReferenceQuery; } }); +Object.defineProperty(exports, "DataRetrievalOptions", { enumerable: true, get: function () { return data_reference_1.DataRetrievalOptions; } }); +Object.defineProperty(exports, "QueryDataRetrievalOptions", { enumerable: true, get: function () { return data_reference_1.QueryDataRetrievalOptions; } }); +Object.defineProperty(exports, "DataSnapshotsArray", { enumerable: true, get: function () { return data_reference_1.DataSnapshotsArray; } }); +Object.defineProperty(exports, "DataReferencesArray", { enumerable: true, get: function () { return data_reference_1.DataReferencesArray; } }); +var data_snapshot_1 = require("./data-snapshot"); +Object.defineProperty(exports, "DataSnapshot", { enumerable: true, get: function () { return data_snapshot_1.DataSnapshot; } }); +Object.defineProperty(exports, "MutationsDataSnapshot", { enumerable: true, get: function () { return data_snapshot_1.MutationsDataSnapshot; } }); +var data_proxy_1 = require("./data-proxy"); +Object.defineProperty(exports, "proxyAccess", { enumerable: true, get: function () { return data_proxy_1.proxyAccess; } }); +Object.defineProperty(exports, "OrderedCollectionProxy", { enumerable: true, get: function () { return data_proxy_1.OrderedCollectionProxy; } }); +var debug_1 = require("./debug"); +Object.defineProperty(exports, "DebugLogger", { enumerable: true, get: function () { return debug_1.DebugLogger; } }); +var id_1 = require("./id"); +Object.defineProperty(exports, "ID", { enumerable: true, get: function () { return id_1.ID; } }); +var path_reference_1 = require("./path-reference"); +Object.defineProperty(exports, "PathReference", { enumerable: true, get: function () { return path_reference_1.PathReference; } }); +var subscription_1 = require("./subscription"); +Object.defineProperty(exports, "EventStream", { enumerable: true, get: function () { return subscription_1.EventStream; } }); +Object.defineProperty(exports, "EventPublisher", { enumerable: true, get: function () { return subscription_1.EventPublisher; } }); +Object.defineProperty(exports, "EventSubscription", { enumerable: true, get: function () { return subscription_1.EventSubscription; } }); +exports.Transport = require("./transport"); +var type_mappings_1 = require("./type-mappings"); +Object.defineProperty(exports, "TypeMappings", { enumerable: true, get: function () { return type_mappings_1.TypeMappings; } }); +exports.Utils = require("./utils"); +var path_info_1 = require("./path-info"); +Object.defineProperty(exports, "PathInfo", { enumerable: true, get: function () { return path_info_1.PathInfo; } }); +var ascii85_1 = require("./ascii85"); +Object.defineProperty(exports, "ascii85", { enumerable: true, get: function () { return ascii85_1.ascii85; } }); +var simple_cache_1 = require("./simple-cache"); +Object.defineProperty(exports, "SimpleCache", { enumerable: true, get: function () { return simple_cache_1.SimpleCache; } }); +var simple_event_emitter_1 = require("./simple-event-emitter"); +Object.defineProperty(exports, "SimpleEventEmitter", { enumerable: true, get: function () { return simple_event_emitter_1.SimpleEventEmitter; } }); +var simple_colors_1 = require("./simple-colors"); +Object.defineProperty(exports, "ColorStyle", { enumerable: true, get: function () { return simple_colors_1.ColorStyle; } }); +Object.defineProperty(exports, "Colorize", { enumerable: true, get: function () { return simple_colors_1.Colorize; } }); +var schema_1 = require("./schema"); +Object.defineProperty(exports, "SchemaDefinition", { enumerable: true, get: function () { return schema_1.SchemaDefinition; } }); +var simple_observable_1 = require("./simple-observable"); +Object.defineProperty(exports, "SimpleObservable", { enumerable: true, get: function () { return simple_observable_1.SimpleObservable; } }); +var partial_array_1 = require("./partial-array"); +Object.defineProperty(exports, "PartialArray", { enumerable: true, get: function () { return partial_array_1.PartialArray; } }); +const object_collection_1 = require("./object-collection"); +Object.defineProperty(exports, "ObjectCollection", { enumerable: true, get: function () { return object_collection_1.ObjectCollection; } }); + +},{"./acebase-base":1,"./api":2,"./ascii85":3,"./data-proxy":7,"./data-reference":8,"./data-snapshot":9,"./debug":10,"./id":11,"./object-collection":13,"./partial-array":15,"./path-info":16,"./path-reference":17,"./schema":19,"./simple-cache":20,"./simple-colors":21,"./simple-event-emitter":22,"./simple-observable":23,"./subscription":24,"./transport":25,"./type-mappings":26,"./utils":27}],13:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ObjectCollection = void 0; +const id_1 = require("./id"); +/** + * Convenience interface for defining an object collection + * @example + * type ChatMessage = { + * text: string, uid: string, sent: Date + * } + * type Chat = { + * title: text + * messages: ObjectCollection + * } + */ +class ObjectCollection { + /** + * Converts and array of values into an object collection, generating a unique key for each item in the array + * @param array + * @example + * const array = [ + * { title: "Don't make me think!", author: "Steve Krug" }, + * { title: "The tipping point", author: "Malcolm Gladwell" } + * ]; + * + * // Convert: + * const collection = ObjectCollection.from(array); + * // --> { + * // kh1x3ygb000120r7ipw6biln: { + * // title: "Don't make me think!", + * // author: "Steve Krug" + * // }, + * // kh1x3ygb000220r757ybpyec: { + * // title: "The tipping point", + * // author: "Malcolm Gladwell" + * // } + * // } + * + * // Now it's easy to add them to the db: + * db.ref('books').update(collection); + */ + static from(array) { + const collection = {}; + array.forEach(child => { + collection[id_1.ID.generate()] = child; + }); + return collection; + } +} +exports.ObjectCollection = ObjectCollection; + +},{"./id":11}],14:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.setObservable = exports.getObservable = void 0; +const simple_observable_1 = require("./simple-observable"); +const utils_1 = require("./utils"); +let _shimRequested = false; +let _observable; +(async () => { + // Try pre-loading rxjs Observable + // Test availability in global scope first + const global = (0, utils_1.getGlobalObject)(); + if (typeof global.Observable !== 'undefined') { + _observable = global.Observable; + return; + } + // Try importing it from dependencies + try { + const { Observable } = await Promise.resolve().then(() => require('rxjs')); + _observable = Observable; + } + catch (_a) { + // rxjs Observable not available, setObservable must be used if usage of SimpleObservable is not desired + _observable = simple_observable_1.SimpleObservable; + } +})(); +function getObservable() { + if (_observable === simple_observable_1.SimpleObservable && !_shimRequested) { + console.warn('Using AceBase\'s simple Observable implementation because rxjs is not available. ' + + 'Add it to your project with "npm install rxjs", add it to AceBase using db.setObservable(Observable), ' + + 'or call db.setObservable("shim") to suppress this warning'); + } + if (_observable) { + return _observable; + } + throw new Error('RxJS Observable could not be loaded. '); +} +exports.getObservable = getObservable; +function setObservable(Observable) { + if (Observable === 'shim') { + _observable = simple_observable_1.SimpleObservable; + _shimRequested = true; + } + else { + _observable = Observable; + } +} +exports.setObservable = setObservable; + +},{"./simple-observable":23,"./utils":27,"rxjs":62}],15:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.PartialArray = void 0; +/** + * Sparse/partial array converted to a serializable object. Use `Object.keys(sparseArray)` and `Object.values(sparseArray)` to iterate its indice and/or values + */ +class PartialArray { + constructor(sparseArray) { + if (sparseArray instanceof Array) { + for (let i = 0; i < sparseArray.length; i++) { + if (typeof sparseArray[i] !== 'undefined') { + this[i] = sparseArray[i]; + } } - options.diff = diff; } - if (options.diff === 'identical') { - return; // Done! + else if (sparseArray) { + Object.assign(this, sparseArray); } - const transaction = options.transaction; - // Get info about current node at path - const currentRow = options.currentValue === null - ? null // No need to load info if currentValue is null (we already know it doesn't exist) - : await this._readNode(path, { transaction }); - if (options.merge && currentRow) { - if (currentRow.type === node_value_types_1.VALUE_TYPES.ARRAY && !(value instanceof Array) && typeof value === 'object' && Object.keys(value).some(key => isNaN(parseInt(key)))) { - throw new Error(`Cannot merge existing array of path "${path}" with an object`); - } - if (value instanceof Array && currentRow.type !== node_value_types_1.VALUE_TYPES.ARRAY) { - throw new Error(`Cannot merge existing object of path "${path}" with an array`); - } + } +} +exports.PartialArray = PartialArray; + +},{}],16:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.PathInfo = void 0; +function getPathKeys(path) { + path = path.replace(/\[/g, '/[').replace(/^\/+/, '').replace(/\/+$/, ''); // Replace [ with /[, remove leading slashes, remove trailing slashes + if (path.length === 0) { + return []; + } + const keys = path.split('/'); + return keys.map(key => { + return key.startsWith('[') ? parseInt(key.slice(1, -1)) : key; + }); +} +class PathInfo { + static get(path) { + return new PathInfo(path); + } + static getChildPath(path, childKey) { + // return getChildPath(path, childKey); + return PathInfo.get(path).child(childKey).path; + } + static getPathKeys(path) { + return getPathKeys(path); + } + constructor(path) { + if (typeof path === 'string') { + this.keys = getPathKeys(path); } - const revision = options.revision || acebase_core_1.ID.generate(); - const mainNode = { - type: currentRow && currentRow.type === node_value_types_1.VALUE_TYPES.ARRAY ? node_value_types_1.VALUE_TYPES.ARRAY : node_value_types_1.VALUE_TYPES.OBJECT, - value: {}, - }; - const childNodeValues = {}; - if (value instanceof Array) { - mainNode.type = node_value_types_1.VALUE_TYPES.ARRAY; - // Convert array to object with numeric properties - const obj = {}; - for (let i = 0; i < value.length; i++) { - obj[i] = value[i]; - } - value = obj; - } - else if (value instanceof acebase_core_1.PathReference) { - mainNode.type = node_value_types_1.VALUE_TYPES.REFERENCE; - mainNode.value = value.path; - } - else if (value instanceof ArrayBuffer) { - mainNode.type = node_value_types_1.VALUE_TYPES.BINARY; - mainNode.value = acebase_core_1.ascii85.encode(value); - } - else if (typeof value === 'string') { - mainNode.type = node_value_types_1.VALUE_TYPES.STRING; - mainNode.value = value; + else if (path instanceof Array) { + this.keys = path; } - const currentIsObjectOrArray = currentRow ? [node_value_types_1.VALUE_TYPES.OBJECT, node_value_types_1.VALUE_TYPES.ARRAY].includes(currentRow.type) : false; - const newIsObjectOrArray = [node_value_types_1.VALUE_TYPES.OBJECT, node_value_types_1.VALUE_TYPES.ARRAY].includes(mainNode.type); - const children = { - current: [], - new: [], - }; - let currentObject = null; - if (currentIsObjectOrArray) { - currentObject = currentRow.value; - children.current = Object.keys(currentObject); - // if (currentObject instanceof Array) { // ALWAYS FALSE BECAUSE THEY ARE STORED AS OBJECTS WITH NUMERIC PROPERTIES - // // Convert array to object with numeric properties - // const obj = {}; - // for (let i = 0; i < value.length; i++) { - // obj[i] = value[i]; - // } - // currentObject = obj; - // } - if (newIsObjectOrArray) { - mainNode.value = currentObject; - } + this.path = this.keys.reduce((path, key, i) => i === 0 ? `${key}` : typeof key === 'string' ? `${path}/${key}` : `${path}[${key}]`, ''); + } + get key() { + return this.keys.length === 0 ? null : this.keys.slice(-1)[0]; + } + get parent() { + if (this.keys.length == 0) { + return null; } - if (newIsObjectOrArray) { - // Object or array. Determine which properties can be stored in the main node, - // and which should be stored in their own nodes - if (!options.merge) { - // Check which keys are present in the old object, but not in newly given object - Object.keys(mainNode.value).forEach(key => { - if (!(key in value)) { - // Property that was in old object, is not in new value -> set to null to mark deletion! - value[key] = null; - } - }); + const parentKeys = this.keys.slice(0, -1); + return new PathInfo(parentKeys); + } + get parentPath() { + return this.keys.length === 0 ? null : this.parent.path; + } + child(childKey) { + if (typeof childKey === 'string') { + if (childKey.length === 0) { + throw new Error(`child key for path "${this.path}" cannot be empty`); } - Object.keys(value).forEach(key => { - const val = value[key]; - delete mainNode.value[key]; // key is being overwritten, moved from inline to dedicated, or deleted. TODO: check if this needs to be done SQLite & MSSQL implementations too - if (val === null) { // || typeof val === 'undefined' - // This key is being removed + // Allow expansion of a child path (eg "user/name") into equivalent `child('user').child('name')` + const keys = getPathKeys(childKey); + keys.forEach(key => { + // Check AceBase key rules here so they will be enforced regardless of storage target. + // This prevents specific keys to be allowed in one environment (eg browser), but then + // refused upon syncing to a binary AceBase db. Fixes https://github.com/appy-one/acebase/issues/172 + if (typeof key !== 'string') { return; } - else if (typeof val === 'undefined') { - if (this.settings.removeVoidProperties === true) { - delete value[key]; // Kill the property in the passed object as well, to prevent differences in stored and working values - return; - } - else { - throw new Error(`Property "${key}" has invalid value. Cannot store undefined values. Set removeVoidProperties option to true to automatically remove undefined properties`); - } - } - // Where to store this value? - if (this.valueFitsInline(val)) { - // Store in main node - mainNode.value[key] = val; - } - else { - // Store in child node - childNodeValues[key] = val; - } - }); - } - // Insert or update node - const isArray = mainNode.type === node_value_types_1.VALUE_TYPES.ARRAY; - if (currentRow) { - // update - this.logger.info(`Node "/${path}" is being ${options.merge ? 'updated' : 'overwritten'}`.colorize(acebase_core_1.ColorStyle.cyan)); - // If existing is an array or object, we have to find out which children are affected - if (currentIsObjectOrArray || newIsObjectOrArray) { - // Get current child nodes in dedicated child records - const pathInfo = acebase_core_1.PathInfo.get(path); - const keys = []; - let checkExecuted = false; - const includeChildCheck = (childPath) => { - checkExecuted = true; - if (!transaction.production && !pathInfo.isParentOf(childPath)) { - // Double check failed - this.throwImplementationError(`"${childPath}" is not a child of "${path}" - childrenOf must only check and return paths that are children`); - } - return true; - }; - const addChildPath = (childPath) => { - if (!checkExecuted) { - this.throwImplementationError(`childrenOf did not call checkCallback before addCallback`); - } - const key = acebase_core_1.PathInfo.get(childPath).key; - keys.push(key.toString()); // .toString to make sure all keys are compared as strings - return true; // Keep streaming - }; - 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); - } - }); + if (/[\x00-\x08\x0b\x0c\x0e-\x1f/[\]\\]/.test(key)) { + throw new Error(`Invalid child key "${key}" for path "${this.path}". Keys cannot contain control characters or any of the following characters: \\ / [ ]`); } - const changes = { - insert: children.new.filter(key => !children.current.includes(key)), - update: [], - delete: options && options.merge ? Object.keys(value).filter(key => value[key] === null) : children.current.filter(key => !children.new.includes(key)), - }; - changes.update = children.new.filter(key => children.current.includes(key) && !changes.delete.includes(key)); - if (isArray && options.merge && (changes.insert.length > 0 || changes.delete.length > 0)) { - // deletes or inserts of individual array entries are not allowed, unless it is the last entry: - // - deletes would cause the paths of following items to change, which is unwanted because the actual data does not change, - // eg: removing index 3 on array of size 10 causes entries with index 4 to 9 to 'move' to indexes 3 to 8 - // - inserts might introduce gaps in indexes, - // eg: adding to index 7 on an array of size 3 causes entries with indexes 3 to 6 to go 'missing' - const newArrayKeys = changes.update.concat(changes.insert); - const isExhaustive = newArrayKeys.every((k, index, arr) => arr.includes(index.toString())); - if (!isExhaustive) { - throw new Error(`Elements cannot be inserted beyond, or removed before the end of an array. Rewrite the whole array at path "${path}" or change your schema to use an object collection instead`); - } + if (key.length > 128) { + throw new Error(`child key "${key}" for path "${this.path}" is too long. Max key length is 128`); } - // (over)write all child nodes that must be stored in their own record - const writePromises = Object.keys(childNodeValues).map(key => { - const keyOrIndex = isArray ? parseInt(key) : key; - const childDiff = typeof options.diff === 'object' ? options.diff.forChild(keyOrIndex) : undefined; - if (childDiff === 'identical') { - // console.warn(`Skipping _writeNode recursion for child "${keyOrIndex}"`); - return; // Skip - } - const childPath = pathInfo.childPath(keyOrIndex); // PathInfo.getChildPath(path, key); - const childValue = childNodeValues[keyOrIndex]; - // Pass current child value to _writeNode - const currentChildValue = typeof options.currentValue === 'undefined' // Fixing issue #20 - ? undefined - : options.currentValue !== null && typeof options.currentValue === 'object' && keyOrIndex in options.currentValue - ? options.currentValue[keyOrIndex] - : null; - return this._writeNode(childPath, childValue, { transaction, revision, merge: false, currentValue: currentChildValue, diff: childDiff }); - }); - // Delete all child nodes that were stored in their own record, but are being removed - // Also delete nodes that are being moved from a dedicated record to inline - const movingNodes = newIsObjectOrArray ? keys.filter(key => key in mainNode.value) : []; // moving from dedicated to inline value - const deleteDedicatedKeys = changes.delete.concat(movingNodes); - const deletePromises = deleteDedicatedKeys.map(key => { - const keyOrIndex = isArray ? parseInt(key) : key; - const childPath = pathInfo.childPath(keyOrIndex); - return this._deleteNode(childPath, { transaction }); - }); - const promises = writePromises.concat(deletePromises); - await Promise.all(promises); - } - // Update main node - // TODO: Check if revision should change? - const p = 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, - }); - if (p instanceof Promise) { - return await p; - } - } - else { - // Current node does not exist, create it and any child nodes - // write all child nodes that must be stored in their own record - this.logger.info(`Node "/${path}" is being created`.colorize(acebase_core_1.ColorStyle.cyan)); - if (isArray) { - // Check if the array is "intact" (all entries have an index from 0 to the end with no gaps) - const arrayKeys = Object.keys(mainNode.value).concat(Object.keys(childNodeValues)); - const isExhaustive = arrayKeys.every((k, index, arr) => arr.includes(index.toString())); - if (!isExhaustive) { - throw new Error(`Cannot store arrays with missing entries`); + if (key.length === 0) { + throw new Error(`child key for path "${this.path}" cannot be empty`); } - } - const promises = Object.keys(childNodeValues).map(key => { - const keyOrIndex = isArray ? parseInt(key) : key; - const childPath = acebase_core_1.PathInfo.getChildPath(path, keyOrIndex); - const childValue = childNodeValues[keyOrIndex]; - return this._writeNode(childPath, childValue, { transaction, revision, merge: false, currentValue: null }); - }); - // Create current node - const p = this._storeNode(path, { - type: mainNode.type, - value: mainNode.value, - revision, - revision_nr: 1, - created: Date.now(), - modified: Date.now(), - }, { - transaction, }); - if (p instanceof Promise) { - promises.push(p); - } - await Promise.all(promises); + childKey = keys; } + return new PathInfo(this.keys.concat(childKey)); + } + childPath(childKey) { + return this.child(childKey).path; + } + get pathKeys() { + return this.keys; } /** - * 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! - */ - async _deleteNode(path, options) { - const pathInfo = acebase_core_1.PathInfo.get(path); - this.logger.info(`Node "/${path}" is being deleted`.colorize(acebase_core_1.ColorStyle.cyan)); - const deletePaths = [path]; - let checkExecuted = false; - const includeDescendantCheck = (descPath) => { - checkExecuted = true; - if (!transaction.production && !pathInfo.isAncestorOf(descPath)) { - // Double check failed - this.throwImplementationError(`"${descPath}" is not a descendant of "${path}" - descendantsOf must only check and return paths that are descendants`); - } - return true; + * If varPath contains variables or wildcards, it will return them with the values found in fullPath + * @param {string} varPath path containing variables such as * and $name + * @param {string} fullPath real path to a node + * @returns {{ [index: number]: string|number, [variable: string]: string|number }} returns an array-like object with all variable values. All named variables are also set on the array by their name (eg vars.uid and vars.$uid) + * @example + * PathInfo.extractVariables('users/$uid/posts/$postid', 'users/ewout/posts/post1/title') === { + * 0: 'ewout', + * 1: 'post1', + * uid: 'ewout', // or $uid + * postid: 'post1' // or $postid + * }; + * + * PathInfo.extractVariables('users/*\/posts/*\/$property', 'users/ewout/posts/post1/title') === { + * 0: 'ewout', + * 1: 'post1', + * 2: 'title', + * property: 'title' // or $property + * }; + * + * PathInfo.extractVariables('users/$user/friends[*]/$friend', 'users/dora/friends[4]/diego') === { + * 0: 'dora', + * 1: 4, + * 2: 'diego', + * user: 'dora', // or $user + * friend: 'diego' // or $friend + * }; + */ + static extractVariables(varPath, fullPath) { + if (!varPath.includes('*') && !varPath.includes('$')) { + return []; + } + // if (!this.equals(fullPath)) { + // throw new Error(`path does not match with the path of this PathInfo instance: info.equals(path) === false!`) + // } + const keys = getPathKeys(varPath); + const pathKeys = getPathKeys(fullPath); + let count = 0; + const variables = { + get length() { return count; }, }; - const addDescendant = (descPath) => { - if (!checkExecuted) { - this.throwImplementationError(`descendantsOf did not call checkCallback before addCallback`); + keys.forEach((key, index) => { + const pathKey = pathKeys[index]; + if (key === '*') { + variables[count++] = pathKey; } - deletePaths.push(descPath); - return true; - }; - const transaction = options.transaction; - await transaction.descendantsOf(path, { metadata: false, value: false }, includeDescendantCheck, addDescendant); - this.logger.info(`Nodes ${deletePaths.map(p => `"/${p}"`).join(',')} are being deleted`.colorize(acebase_core_1.ColorStyle.cyan)); - return transaction.removeMultiple(deletePaths); + else if (typeof key === 'string' && key[0] === '$') { + variables[count++] = pathKey; + // Set the $variable property + variables[key] = pathKey; + // Set friendly property name (without $) + const varName = key.slice(1); + if (typeof variables[varName] === 'undefined') { + variables[varName] = pathKey; + } + } + }); + return variables; } /** - * Enumerates all children of a given Node for reflection purposes + * If varPath contains variables or wildcards, it will return a path with the variables replaced by the keys found in fullPath. + * @example + * PathInfo.fillVariables('users/$uid/posts/$postid', 'users/ewout/posts/post1/title') === 'users/ewout/posts/post1' */ - getChildren(path, options = {}) { - let callback; - const generator = { - /** - * - * @param valueCallback callback function to run for each child. Return false to stop iterating - * @returns returns a promise that resolves with a boolean indicating if all children have been enumerated, or was canceled by the valueCallback function - */ - next(valueCallback) { - callback = valueCallback; - return start(); - }, - }; - const start = async () => { - const transaction = options.transaction || await this._customImplementation.getTransaction({ path, write: false }); - try { - let canceled = false; - await (async () => { - const node = await this._readNode(path, { transaction }); - if (!node) { - throw new node_errors_1.NodeNotFoundError(`Node "/${path}" does not exist`); - } - if (![node_value_types_1.VALUE_TYPES.OBJECT, node_value_types_1.VALUE_TYPES.ARRAY].includes(node.type)) { - // No children - return; - } - const isArray = node.type === node_value_types_1.VALUE_TYPES.ARRAY; - const value = node.value; - let keys = Object.keys(value).map(key => isArray ? parseInt(key) : key); - if (options.keyFilter) { - keys = keys.filter(key => options.keyFilter.includes(key)); - } - const pathInfo = acebase_core_1.PathInfo.get(path); - keys.length > 0 && keys.every(key => { - const 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: new Date(node.created), - modified: new Date(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 (!transaction.production && !pathInfo.isParentOf(childPath)) { - // Double check failed - this.throwImplementationError(`"${childPath}" is not a child of "${path}" - childrenOf must only check and return paths that are children`); - } - if (options.keyFilter) { - const key = acebase_core_1.PathInfo.get(childPath).key; - return options.keyFilter.includes(key); - } - return true; - }; - const addChildNode = (childPath, node) => { - if (!checkExecuted) { - this.throwImplementationError(`childrenOf did not call checkCallback before addCallback`); - } - const key = acebase_core_1.PathInfo.get(childPath).key; - const info = new CustomStorageNodeInfo({ - path: childPath, - type: node.type, - key: isArray ? null : key, - index: isArray ? key : null, - address: new node_address_1.NodeAddress(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) { - // transaction was created by us, commit - await transaction.commit(); - } - return canceled; + static fillVariables(varPath, fullPath) { + if (varPath.indexOf('*') < 0 && varPath.indexOf('$') < 0) { + return varPath; + } + const keys = getPathKeys(varPath); + const pathKeys = getPathKeys(fullPath); + const merged = keys.map((key, index) => { + if (key === pathKeys[index] || index >= pathKeys.length) { + return key; } - catch (err) { - if (!options.transaction) { - // transaction was created by us, rollback - await transaction.rollback(err); + 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 "${varPath}" because they do not match`); + } + }); + let mergedPath = ''; + merged.forEach(key => { + if (typeof key === 'number') { + mergedPath += `[${key}]`; + } + else { + if (mergedPath.length > 0) { + mergedPath += '/'; } - throw err; + mergedPath += key; } - }; // start() - return generator; + }); + return mergedPath; } - async getNode(path, options) { - // path = path.replace(/'/g, ''); // prevent sql injection, remove single quotes - options = options || {}; - const transaction = options.transaction || await this._customImplementation.getTransaction({ path, write: false }); - try { - const node = await (async () => { - // Get path, path/* and path[* - const filtered = (options.include && options.include.length > 0) || (options.exclude && options.exclude.length > 0) || options.child_objects === false; - const pathInfo = acebase_core_1.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); - (0, assert_1.assert)(lockPath === pathInfo.parentPath, `transaction.moveToParentPath() did not move to the right parent path of "${path}"`); - const parentNode = await this._readNode(pathInfo.parentPath, { transaction }); - if (parentNode && [node_value_types_1.VALUE_TYPES.OBJECT, node_value_types_1.VALUE_TYPES.ARRAY].includes(parentNode.type) && pathInfo.key in parentNode.value) { - 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 isArray = targetNode.type === node_value_types_1.VALUE_TYPES.ARRAY; - /** - * Convert include & exclude filters to PathInfo instances for easier handling - */ - const convertFilterArray = (arr) => { - const isNumber = (key) => /^[0-9]+$/.test(key); - return arr.map(path => acebase_core_1.PathInfo.get(isArray && isNumber(path) ? `[${path}]` : path)); - }; - const includeFilter = options.include ? convertFilterArray(options.include) : []; - const excludeFilter = options.exclude ? convertFilterArray(options.exclude) : []; - /** - * Apply include filters to prevent unwanted properties stored inline to be added. - * - * Removes properties that are not on the trail of any include filter, but were loaded because they are - * stored inline in the parent node. - * - * Example: - * data of `"users/someuser/posts/post1"`: `{ title: 'My first post', posted: (date), history: {} }` - * code: `db.ref('users/someuser').get({ include: ['posts/*\/title'] })` - * descPath: `"users/someuser/posts/post1"`, - * trailKeys: `["posts", "post1"]`, - * includeFilter[0]: `["posts", "*", "title"]` - * properties `posted` and `history` must be removed from the object - */ - const applyFiltersOnInlineData = (descPath, node) => { - if ([node_value_types_1.VALUE_TYPES.OBJECT, node_value_types_1.VALUE_TYPES.ARRAY].includes(node.type) && includeFilter.length > 0) { - const trailKeys = acebase_core_1.PathInfo.getPathKeys(descPath).slice(pathInfo.keys.length); - const checkPathInfo = new acebase_core_1.PathInfo(trailKeys); - const remove = []; - const includes = includeFilter.filter(info => info.isDescendantOf(checkPathInfo)); - if (includes.length > 0) { - const isArray = node.type === node_value_types_1.VALUE_TYPES.ARRAY; - remove.push(...Object.keys(node.value).map(key => isArray ? +key : key)); // Mark all at first - for (const info of includes) { - const targetProp = info.keys[trailKeys.length]; - if (typeof targetProp === 'string' && (targetProp === '*' || targetProp.startsWith('$'))) { - remove.splice(0); - break; - } - const index = remove.indexOf(targetProp); - index >= 0 && remove.splice(index, 1); - } - } - const hasIncludeOnChild = includeFilter.some(info => info.isChildOf(checkPathInfo)); - const hasExcludeOnChild = excludeFilter.some(info => info.isChildOf(checkPathInfo)); - if (hasExcludeOnChild && !hasIncludeOnChild) { - // do not remove children that are NOT in direct exclude filters (which includes them again) - const excludes = excludeFilter.filter(info => info.isChildOf(checkPathInfo)); - for (let i = 0; i < remove.length; i++) { - if (!excludes.find(info => info.equals(remove[i]))) { - remove.splice(i, 1); - i--; - } - } - } - // remove.length > 0 && this.debug.log(`Remove properties:`, remove); - for (const key of remove) { - delete node.value[key]; - } - } - }; - applyFiltersOnInlineData(path, targetNode); - let checkExecuted = false; - const includeDescendantCheck = (descPath, metadata) => { - checkExecuted = true; - if (!transaction.production && !pathInfo.isAncestorOf(descPath)) { - // Double check failed - this.throwImplementationError(`"${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 - const descPathKeys = acebase_core_1.PathInfo.getPathKeys(descPath); - const trailKeys = descPathKeys.slice(pathInfo.keys.length); - const checkPathInfo = new acebase_core_1.PathInfo(trailKeys); - let include = (includeFilter.length > 0 - ? includeFilter.some(info => checkPathInfo.isOnTrailOf(info)) - : true) - && (excludeFilter.length > 0 - ? !excludeFilter.some(info => info.equals(checkPathInfo) || info.isAncestorOf(checkPathInfo)) - : true); - // Apply child_objects filter. If metadata is not loaded, we can only skip deeper descendants here - any child object that does get through will be ignored by addDescendant - if (include - && options.child_objects === false - && (pathInfo.isParentOf(descPath) && [node_value_types_1.VALUE_TYPES.OBJECT, node_value_types_1.VALUE_TYPES.ARRAY].includes(metadata ? metadata.type : -1) - || acebase_core_1.PathInfo.getPathKeys(descPath).length > pathInfo.pathKeys.length + 1)) { - include = false; - } - return include; - }; - const descRows = []; - const addDescendant = (descPath, node) => { - // console.warn(`Adding descendant "${descPath}"`); - if (!checkExecuted) { - this.throwImplementationError('descendantsOf did not call checkCallback before addCallback'); - } - if (options.child_objects === false && [node_value_types_1.VALUE_TYPES.OBJECT, node_value_types_1.VALUE_TYPES.ARRAY].includes(node.type)) { - // child objects are filtered out, but this one got through because includeDescendantCheck did not have access to its metadata, - // which is ok because doing that might drastically improve performance in client code. Skip it now. - return true; - } - // Apply include filters to prevent unwanted properties stored inline to be added - applyFiltersOnInlineData(descPath, node); - // Process the value - this._processReadNodeValue(node); - // Add node - const row = node; - row.path = descPath; - descRows.push(row); - return true; // Keep streaming - }; - await transaction.descendantsOf(path, { metadata: true, value: true }, includeDescendantCheck, addDescendant); - this.logger.info(`Read node "/${path}" and ${filtered ? '(filtered) ' : ''}descendants from ${descRows.length + 1} records`.colorize(acebase_core_1.ColorStyle.magenta)); - const result = targetNode; - const objectToArray = (obj) => { - // Convert object value to array - const arr = []; - Object.keys(obj).forEach(key => { - const index = parseInt(key); - arr[index] = obj[index]; - }); - return arr; - }; - if (targetNode.type === node_value_types_1.VALUE_TYPES.ARRAY) { - result.value = objectToArray(result.value); - } - if (targetNode.type === node_value_types_1.VALUE_TYPES.OBJECT || targetNode.type === node_value_types_1.VALUE_TYPES.ARRAY) { - // target node is an object or array - // merge with other found (child) nodes - const targetPathKeys = acebase_core_1.PathInfo.getPathKeys(path); - const value = targetNode.value; - for (let i = 0; i < descRows.length; i++) { - const otherNode = descRows[i]; - const pathKeys = acebase_core_1.PathInfo.getPathKeys(otherNode.path); - const trailKeys = pathKeys.slice(targetPathKeys.length); - let parent = value; - for (let j = 0; j < trailKeys.length; j++) { - (0, assert_1.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' - ? node_value_types_1.VALUE_TYPES.ARRAY - : node_value_types_1.VALUE_TYPES.OBJECT; - let nodeValue; - if (!isLast) { - nodeValue = nodeType === node_value_types_1.VALUE_TYPES.OBJECT ? {} : []; - } - else { - nodeValue = otherNode.value; - if (nodeType === node_value_types_1.VALUE_TYPES.ARRAY) { - nodeValue = objectToArray(nodeValue); - } - } - if (key in parent) { - // Merge with parent - const mergePossible = typeof parent[key] === typeof nodeValue && [node_value_types_1.VALUE_TYPES.OBJECT, node_value_types_1.VALUE_TYPES.ARRAY].includes(nodeType); - if (!mergePossible) { - // Ignore the value in the child record, see issue #20: "Assertion failed: Merging child values can only be done if existing and current values are both an array or object" - this.logger.error(`The value stored in node "${otherNode.path}" cannot be merged with the parent node, value will be ignored. This error should disappear once the target node value is updated. See issue #20 for more information`, { path, parent, key, nodeType, nodeValue }); - } - else { - Object.keys(nodeValue).forEach(childKey => { - if (childKey in parent[key]) { - this.throwImplementationError(`Custom storage merge error: child key "${childKey}" is in parent value already! Make sure the get/childrenOf/descendantsOf methods of the custom storage class return values that can be modified by AceBase without affecting the stored source`); - } - parent[key][childKey] = nodeValue[childKey]; - }); - } - } - else { - parent[key] = nodeValue; - } - parent = parent[key]; - } - } - } - else if (descRows.length > 0) { - this.throwImplementationError(`multiple records found for non-object value!`); - } - // Post process filters to remove any data that got through 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 - (0, assert_1.assert)(Object.keys(result.value[key]).length === 0); - delete result.value[key]; - } - }); - } - if (options.include) { - // TODO: remove any unselected children that did get through - } - 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 = acebase_core_1.PathInfo.getPathKeys(path); - process(result.value, checkKeys); - }); - } - return result; - })(); - if (!options.transaction) { - // transaction was created by us, commit - await transaction.commit(); - } - return node; + /** + * Replaces all variables in a path with the values in the vars argument + * @param varPath path containing variables + * @param vars variables object such as one gotten from PathInfo.extractVariables + */ + static fillVariables2(varPath, vars) { + if (typeof vars !== 'object' || Object.keys(vars).length === 0) { + return varPath; // Nothing to fill } - catch (err) { - if (!options.transaction) { - // transaction was created by us, rollback - await transaction.rollback(err); + const pathKeys = getPathKeys(varPath); + let n = 0; + const targetPath = pathKeys.reduce((path, key) => { + if (typeof key === 'string' && (key === '*' || key.startsWith('$'))) { + return PathInfo.getChildPath(path, vars[n++]); } - throw err; + else { + return PathInfo.getChildPath(path, key); + } + }, ''); + return targetPath; + } + /** + * Checks if a given path matches this path, eg "posts/*\/title" matches "posts/12344/title" and "users/123/name" matches "users/$uid/name" + */ + equals(otherPath) { + const other = otherPath instanceof PathInfo ? otherPath : new PathInfo(otherPath); + if (this.path === other.path) { + return true; + } // they are identical + if (this.keys.length !== other.keys.length) { + return false; } + return this.keys.every((key, index) => { + const otherKey = other.keys[index]; + return otherKey === key + || (typeof otherKey === 'string' && (otherKey === '*' || otherKey[0] === '$')) + || (typeof key === 'string' && (key === '*' || key[0] === '$')); + }); } - async getNodeInfo(path, options = {}) { - options = options || {}; - const pathInfo = acebase_core_1.PathInfo.get(path); - const transaction = options.transaction || await this._customImplementation.getTransaction({ path, write: false }); - try { - const node = await this._readNode(path, { transaction }); - const info = new CustomStorageNodeInfo({ - 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 node_address_1.NodeAddress(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 !== '') { - // Try parent node - const lockPath = await transaction.moveToParentPath(pathInfo.parentPath); - (0, assert_1.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 && [node_value_types_1.VALUE_TYPES.OBJECT, node_value_types_1.VALUE_TYPES.ARRAY].includes(parent.type) && pathInfo.key in parent.value) { - // Stored in parent node - 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 { - // Parent doesn't exist, so the node we're looking for cannot exist either - info.address = null; - } - } - if (options.include_child_count) { - info.childCount = 0; - if ([node_value_types_1.VALUE_TYPES.OBJECT, node_value_types_1.VALUE_TYPES.ARRAY].includes(info.valueType) && info.address) { - // Get number of children - info.childCount = node.value ? Object.keys(node.value).length : 0; - info.childCount += await transaction.getChildCount(path); - } - } - if (!options.transaction) { - // transaction was created by us, commit - await transaction.commit(); - } - return info; + /** + * Checks if a given path is an ancestor, eg "posts" is an ancestor of "posts/12344/title" + */ + isAncestorOf(descendantPath) { + const descendant = descendantPath instanceof PathInfo ? descendantPath : new PathInfo(descendantPath); + if (descendant.path === '' || this.path === descendant.path) { + return false; } - catch (err) { - if (!options.transaction) { - // transaction was created by us, rollback - await transaction.rollback(err); - } - throw err; + if (this.path === '') { + return true; + } + if (this.keys.length >= descendant.keys.length) { + return false; } + return this.keys.every((key, index) => { + const otherKey = descendant.keys[index]; + return otherKey === key + || (typeof otherKey === 'string' && (otherKey === '*' || otherKey[0] === '$')) + || (typeof key === 'string' && (key === '*' || key[0] === '$')); + }); } - // TODO: Move to Storage base class? - async setNode(path, value, options = { suppress_events: false, context: null }) { - if (this.settings.readOnly) { - throw new Error(`Database is opened in read-only mode`); + /** + * Checks if a given path is a descendant, eg "posts/1234/title" is a descendant of "posts" + */ + isDescendantOf(ancestorPath) { + const ancestor = ancestorPath instanceof PathInfo ? ancestorPath : new PathInfo(ancestorPath); + if (this.path === '' || this.path === ancestor.path) { + return false; } - const pathInfo = acebase_core_1.PathInfo.get(path); - const transaction = options.transaction || await this._customImplementation.getTransaction({ 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, suppress_events: options.suppress_events, context: options.context }); - } - else if (typeof options.assert_revision !== 'undefined') { - const info = await this.getNodeInfo(path, { transaction }); - if (info.revision !== options.assert_revision) { - throw new node_errors_1.NodeRevisionError(`revision '${info.revision}' does not match requested revision '${options.assert_revision}'`); - } - if (info.address && info.address.path === path && value !== null && !this.valueFitsInline(value)) { - // Overwrite node - await this._writeNodeWithTracking(path, value, { merge: false, transaction, suppress_events: options.suppress_events, context: options.context }); - } - else { - // Update parent node - const lockPath = await transaction.moveToParentPath(pathInfo.parentPath); - (0, assert_1.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, suppress_events: options.suppress_events, context: options.context }); - } - } - else { - // Delegate operation to update on parent node - const lockPath = await transaction.moveToParentPath(pathInfo.parentPath); - (0, assert_1.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, suppress_events: options.suppress_events, context: options.context }); - } - if (!options.transaction) { - // transaction was created by us, commit - await transaction.commit(); - } + if (ancestorPath === '') { + return true; } - catch (err) { - if (!options.transaction) { - // transaction was created by us, rollback - await transaction.rollback(err); - } - throw err; + if (ancestor.keys.length >= this.keys.length) { + return false; } + return ancestor.keys.every((key, index) => { + const otherKey = this.keys[index]; + return otherKey === key + || (typeof otherKey === 'string' && (otherKey === '*' || otherKey[0] === '$')) + || (typeof key === 'string' && (key === '*' || key[0] === '$')); + }); } - // TODO: Move to Storage base class? - async updateNode(path, updates, options = { suppress_events: false, context: null }) { - if (this.settings.readOnly) { - throw new Error(`Database is opened in read-only mode`); - } - if (typeof updates !== 'object') { - throw new Error(`invalid updates argument`); //. Must be a non-empty object or array - } - else if (Object.keys(updates).length === 0) { - return; // Nothing to update. Done! + /** + * Checks if the other path is on the same trail as this path. Paths on the same trail if they share a + * common ancestor. Eg: "posts" is on the trail of "posts/1234/title" and vice versa. + */ + isOnTrailOf(otherPath) { + const other = otherPath instanceof PathInfo ? otherPath : new PathInfo(otherPath); + if (this.path.length === 0 || other.path.length === 0) { + return true; } - const transaction = options.transaction || await this._customImplementation.getTransaction({ path, write: true }); - try { - // Get info about current node - const nodeInfo = await this.getNodeInfo(path, { transaction }); - const pathInfo = acebase_core_1.PathInfo.get(path); - if (nodeInfo.exists && nodeInfo.address && nodeInfo.address.path === path) { - // Node exists and is stored in its own record. - // Update it - await this._writeNodeWithTracking(path, updates, { transaction, merge: true, suppress_events: options.suppress_events, context: options.context }); - } - else if (nodeInfo.exists) { - // Node exists, but is stored in its parent node. - const pathInfo = acebase_core_1.PathInfo.get(path); - const lockPath = await transaction.moveToParentPath(pathInfo.parentPath); - (0, assert_1.assert)(lockPath === pathInfo.parentPath, `transaction.moveToParentPath() did not move to the right parent path of "${path}"`); - await this._writeNodeWithTracking(pathInfo.parentPath, { [pathInfo.key]: updates }, { transaction, merge: true, suppress_events: options.suppress_events, context: options.context }); - } - else { - // The node does not exist, it's parent doesn't have it either. Update the parent instead - const lockPath = await transaction.moveToParentPath(pathInfo.parentPath); - (0, assert_1.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, suppress_events: options.suppress_events, context: options.context }); - } - if (!options.transaction) { - // transaction was created by us, commit - await transaction.commit(); - } + if (this.path === other.path) { + return true; } - catch (err) { - if (!options.transaction) { - // transaction was created by us, rollback - await transaction.rollback(err); + return this.pathKeys.every((key, index) => { + if (index >= other.keys.length) { + return true; } - throw err; - } + const otherKey = other.keys[index]; + return otherKey === key + || (typeof otherKey === 'string' && (otherKey === '*' || otherKey[0] === '$')) + || (typeof key === 'string' && (key === '*' || key[0] === '$')); + }); + } + /** + * Checks if a given path is a direct child, eg "posts/1234/title" is a child of "posts/1234" + */ + isChildOf(otherPath) { + const other = otherPath instanceof PathInfo ? otherPath : new PathInfo(otherPath); + if (this.path === '') { + return false; + } // If our path is the root, it's nobody's child... + return this.parent.equals(other); + } + /** + * Checks if a given path is its parent, eg "posts/1234" is the parent of "posts/1234/title" + */ + isParentOf(otherPath) { + const other = otherPath instanceof PathInfo ? otherPath : new PathInfo(otherPath); + if (other.path === '') { + return false; + } // If the other path is the root, this path cannot be its parent + return this.equals(other.parent); } } -exports.CustomStorage = CustomStorage; +exports.PathInfo = PathInfo; -},{"../../assert":4,"../../node-address":10,"../../node-errors":11,"../../node-info":12,"../../node-lock":13,"../../node-value-types":14,"../index":29,"./helpers":20,"acebase-core":46}],22:[function(require,module,exports){ +},{}],17:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.createIndexedDBInstance = void 0; -const acebase_core_1 = require("acebase-core"); -const __1 = require(".."); -const __2 = require("../../.."); -const settings_1 = require("./settings"); -const transaction_1 = require("./transaction"); -function createIndexedDBInstance(dbname, init = {}) { - const settings = new settings_1.IndexedDBStorageSettings(init); - // We'll create an IndexedDB with name "dbname.acebase" - const request = indexedDB.open(`${dbname}.acebase`, 1); - request.onupgradeneeded = (e) => { - // create datastore - const db = request.result; - // Create "nodes" object store for metadata - db.createObjectStore('nodes', { keyPath: 'path' }); - // Create "content" object store with all data - db.createObjectStore('content'); - }; - let idb; - const readyPromise = new Promise((resolve, reject) => { - request.onsuccess = e => { - idb = request.result; - resolve(); - }; - request.onerror = e => { - reject(e); - }; - }); - const cache = new acebase_core_1.SimpleCache(typeof settings.cacheSeconds === 'number' ? settings.cacheSeconds : 60); // 60 second node cache by default - // cache.enabled = false; - const storageSettings = new __1.CustomStorageSettings({ - name: 'IndexedDB', - locking: true, - removeVoidProperties: settings.removeVoidProperties, - maxInlineValueSize: settings.maxInlineValueSize, - lockTimeout: settings.lockTimeout, - ready() { - return readyPromise; - }, - async getTransaction(target) { - await readyPromise; - const context = { - debug: false, - db: idb, - cache, - ipc, - }; - return new transaction_1.IndexedDBStorageTransaction(context, target); - }, - }); - const db = new __2.AceBase(dbname, { - logLevel: settings.logLevel, - storage: storageSettings, - sponsor: settings.sponsor, - // isolated: settings.isolated, - }); - const ipc = db.api.storage.ipc; - db.settings.ipcEvents = settings.multipleTabs === true; - ipc.on('notification', async (notification) => { - const message = notification.data; - if (typeof message !== 'object') { - return; - } - if (message.action === 'cache.invalidate') { - // console.warn(`Invalidating cache for paths`, message.paths); - for (const path of message.paths) { - cache.remove(path); - } - } - }); - return db; +exports.PathReference = void 0; +class PathReference { + /** + * Creates a reference to a path that can be stored in the database. Use this to create cross-references to other data in your database + * @param path + */ + constructor(path) { + this.path = path; + } } -exports.createIndexedDBInstance = createIndexedDBInstance; +exports.PathReference = PathReference; -},{"..":21,"../../..":6,"./settings":23,"./transaction":24,"acebase-core":46}],23:[function(require,module,exports){ +},{}],18:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.IndexedDBStorageSettings = void 0; -const __1 = require("../.."); -class IndexedDBStorageSettings extends __1.StorageSettings { - constructor(settings) { - super(settings); - /** - * Whether to enable cross-tab synchronization - * @default false - */ - this.multipleTabs = false; - /** - * How many seconds to keep node info in memory, to speed up IndexedDB performance. - * @default 60 - */ - this.cacheSeconds = 60; - /** - * You can turn this on if you are a sponsor - * @default false - */ - this.sponsor = false; - if (typeof settings.logLevel === 'string') { - this.logLevel = settings.logLevel; - } - if (typeof settings.multipleTabs === 'boolean') { - this.multipleTabs = settings.multipleTabs; - } - if (typeof settings.cacheSeconds === 'number') { - this.cacheSeconds = settings.cacheSeconds; - } - if (typeof settings.sponsor === 'boolean') { - this.sponsor = settings.sponsor; - } - ['type', 'ipc', 'path'].forEach((prop) => { - if (prop in settings) { - console.warn(`${prop} setting is not supported for AceBase IndexedDBStorage`); - } - }); - } -} -exports.IndexedDBStorageSettings = IndexedDBStorageSettings; +exports.default = { + // eslint-disable-next-line @typescript-eslint/ban-types + nextTick(fn) { + setTimeout(fn, 0); + }, +}; -},{"../..":29}],24:[function(require,module,exports){ +},{}],19:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.IndexedDBStorageTransaction = void 0; -const __1 = require(".."); -function _requestToPromise(request) { - return new Promise((resolve, reject) => { - request.onsuccess = event => { - return resolve(request.result || null); - }; - request.onerror = reject; - }); -} -class IndexedDBStorageTransaction extends __1.CustomStorageTransaction { - /** - * Creates a transaction object for IndexedDB usage. Because IndexedDB automatically commits - * transactions when they have not been touched for a number of microtasks (eg promises - * resolving whithout querying data), we will enqueue set and remove operations until commit - * or rollback. We'll create separate IndexedDB transactions for get operations, caching their - * values to speed up successive requests for the same data. - */ - constructor(context, target) { - super(target); - this.context = context; - this.production = true; // Improves performance, only set when all works well - this._pending = []; - } - _createTransaction(write = false) { - const tx = this.context.db.transaction(['nodes', 'content'], write ? 'readwrite' : 'readonly'); - return tx; +exports.SchemaDefinition = void 0; +// parses a typestring, creates checker functions +function parse(definition) { + // tokenize + let pos = 0; + function consumeSpaces() { + let c; + while (c = definition[pos], [' ', '\r', '\n', '\t'].includes(c)) { + pos++; + } } - _splitMetadata(node) { - const value = node.value; - const copy = Object.assign({}, node); - delete copy.value; - const metadata = copy; - return { metadata, value }; + function consumeCharacter(c) { + if (definition[pos] !== c) { + throw new Error(`Unexpected character at position ${pos}. Expected: '${c}', found '${definition[pos]}'`); + } + pos++; } - async commit() { - // console.log(`*** commit ${this._pending.length} operations ****`); - if (this._pending.length === 0) { - return; + function readProperty() { + consumeSpaces(); + const prop = { name: '', optional: false, wildcard: false }; + let c; + while (c = definition[pos], c === '_' || c === '$' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (prop.name.length > 0 && c >= '0' && c <= '9') || (prop.name.length === 0 && c === '*')) { + prop.name += c; + pos++; } - const batch = this._pending.splice(0); - this.context.ipc.sendNotification({ action: 'cache.invalidate', paths: batch.map(op => op.path) }); - const tx = this._createTransaction(true); - try { - await new Promise((resolve, reject) => { - let stop = false, processed = 0; - const handleError = (err) => { - stop = true; - reject(err); - }; - const handleSuccess = () => { - if (++processed === batch.length) { - resolve(); - } - }; - batch.forEach((op, i) => { - if (stop) { - return; - } - let r1, r2; - const path = op.path; - if (op.action === 'set') { - const { metadata, value } = this._splitMetadata(op.node); - const nodeInfo = { path, metadata }; - r1 = tx.objectStore('nodes').put(nodeInfo); // Insert into "nodes" object store - r2 = tx.objectStore('content').put(value, path); // Add value to "content" object store - this.context.cache.set(path, op.node); - } - else if (op.action === 'remove') { - r1 = tx.objectStore('content').delete(path); // Remove from "content" object store - r2 = tx.objectStore('nodes').delete(path); // Remove from "nodes" data store - this.context.cache.set(path, null); - } - else { - handleError(new Error(`Unknown pending operation "${op.action}" on path "${path}" `)); - } - let succeeded = 0; - r1.onsuccess = r2.onsuccess = () => { - if (++succeeded === 2) { - handleSuccess(); - } - }; - r1.onerror = r2.onerror = handleError; - }); - }); - tx.commit && tx.commit(); + if (prop.name.length === 0) { + throw new Error(`Property name expected at position ${pos}, found: ${definition.slice(pos, pos + 10)}..`); } - catch (err) { - console.error(err); - tx.abort && tx.abort(); - throw err; + if (definition[pos] === '?') { + prop.optional = true; + pos++; } + if (prop.name === '*' || prop.name[0] === '$') { + prop.optional = true; + prop.wildcard = true; + } + consumeSpaces(); + consumeCharacter(':'); + return prop; } - async rollback(err) { - // Nothing has committed yet, so we'll leave it like that - this._pending = []; - } - async get(path) { - // console.log(`*** get "${path}" ****`); - if (this.context.cache.has(path)) { - const cache = this.context.cache.get(path); - // console.log(`Using cached node for path "${path}": `, cache); - return cache; + function readType() { + consumeSpaces(); + let type = { typeOf: 'any' }, c; + // try reading simple type first: (string,number,boolean,Date etc) + let name = ''; + while (c = definition[pos], (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) { + name += c; + pos++; } - const tx = this._createTransaction(false); - const r1 = _requestToPromise(tx.objectStore('nodes').get(path)); // Get metadata from "nodes" object store - const r2 = _requestToPromise(tx.objectStore('content').get(path)); // Get content from "content" object store - try { - const results = await Promise.all([r1, r2]); - tx.commit && tx.commit(); - const info = results[0]; - if (!info) { - // Node doesn't exist - this.context.cache.set(path, null); - return null; + if (name.length === 0) { + if (definition[pos] === '*') { + // any value + consumeCharacter('*'); + type.typeOf = 'any'; } - const node = info.metadata; - node.value = results[1]; - this.context.cache.set(path, node); - return node; - } - catch (err) { - console.error(`IndexedDB get error`, err); - tx.abort && tx.abort(); - throw err; - } - } - set(path, node) { - // Queue the operation until commit - this._pending.push({ action: 'set', path, node }); - } - remove(path) { - // Queue the operation until commit - this._pending.push({ action: 'remove', path }); - } - async removeMultiple(paths) { - // Queues multiple items at once, dramatically improves performance for large datasets - paths.forEach(path => { - this._pending.push({ action: 'remove', path }); - }); - } - childrenOf(path, include, checkCallback, addCallback) { - // console.log(`*** childrenOf "${path}" ****`); - return this._getChildrenOf(path, Object.assign(Object.assign({}, include), { descendants: false }), checkCallback, addCallback); - } - descendantsOf(path, include, checkCallback, addCallback) { - // console.log(`*** descendantsOf "${path}" ****`); - return this._getChildrenOf(path, Object.assign(Object.assign({}, include), { descendants: true }), checkCallback, addCallback); - } - _getChildrenOf(path, include, checkCallback, addCallback) { - // Use cursor to loop from path on - return new Promise((resolve, reject) => { - const pathInfo = __1.CustomStorageHelpers.PathInfo.get(path); - const tx = this._createTransaction(false); - const store = tx.objectStore('nodes'); - const query = IDBKeyRange.lowerBound(path, true); - const cursor = include.metadata ? store.openCursor(query) : store.openKeyCursor(query); - cursor.onerror = e => { - var _a; - (_a = tx.abort) === null || _a === void 0 ? void 0 : _a.call(tx); - reject(e); - }; - cursor.onsuccess = async (e) => { - var _a, _b, _c; - const otherPath = (_b = (_a = cursor.result) === null || _a === void 0 ? void 0 : _a.key) !== null && _b !== void 0 ? _b : null; - let keepGoing = true; - if (otherPath === null) { - // No more results - keepGoing = false; - } - else if (!pathInfo.isAncestorOf(otherPath)) { - // Paths are sorted, no more children or ancestors to be expected! - keepGoing = false; - } - else if (include.descendants || pathInfo.isParentOf(otherPath)) { - let node; - if (include.metadata) { - const valueCursor = cursor; - const data = valueCursor.result.value; - node = data.metadata; - } - const shouldAdd = checkCallback(otherPath, node); - if (shouldAdd) { - if (include.value) { - // Load value! - if (this.context.cache.has(otherPath)) { - const cache = this.context.cache.get(otherPath); - node.value = cache.value; - } - else { - const req = tx.objectStore('content').get(otherPath); - node.value = await new Promise((resolve, reject) => { - req.onerror = e => { - resolve(null); // Value missing? - }; - req.onsuccess = e => { - resolve(req.result); - }; - }); - this.context.cache.set(otherPath, node.value === null ? null : node); - } - } - keepGoing = addCallback(otherPath, node); - } + else if (['\'', '"', '`'].includes(definition[pos])) { + // Read string value + type.typeOf = 'string'; + type.value = ''; + const quote = definition[pos]; + consumeCharacter(quote); + while (c = definition[pos], c && c !== quote) { + type.value += c; + pos++; } - if (keepGoing) { - try { - cursor.result.continue(); + consumeCharacter(quote); + } + else if (definition[pos] >= '0' && definition[pos] <= '9') { + // read numeric value + type.typeOf = 'number'; + let nr = ''; + while (c = definition[pos], c === '.' || c === 'n' || (c >= '0' && c <= '9')) { + nr += c; + pos++; + } + if (nr.endsWith('n')) { + type.value = BigInt(nr); + } + else if (nr.includes('.')) { + type.value = parseFloat(nr); + } + else { + type.value = parseInt(nr); + } + } + else if (definition[pos] === '{') { + // Read object (interface) definition + consumeCharacter('{'); + type.typeOf = 'object'; + type.instanceOf = Object; + // Read children: + type.children = []; + while (true) { + const prop = readProperty(); + const types = readTypes(); + type.children.push({ name: prop.name, optional: prop.optional, wildcard: prop.wildcard, types }); + consumeSpaces(); + if (definition[pos] === ';' || definition[pos] === ',') { + consumeCharacter(definition[pos]); + consumeSpaces(); } - catch (err) { - // We reached the end of the cursor? - keepGoing = false; + if (definition[pos] === '}') { + break; } } - if (!keepGoing) { - (_c = tx.commit) === null || _c === void 0 ? void 0 : _c.call(tx); - resolve(); + consumeCharacter('}'); + } + else if (definition[pos] === '/') { + // Read regular expression definition + consumeCharacter('/'); + let pattern = '', flags = ''; + while (c = definition[pos], c !== '/' || pattern.endsWith('\\')) { + pattern += c; + pos++; } - }; - }); - } -} -exports.IndexedDBStorageTransaction = IndexedDBStorageTransaction; - -},{"..":21}],25:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.createLocalStorageInstance = exports.LocalStorageTransaction = exports.LocalStorageSettings = void 0; -const __1 = require(".."); -const __2 = require("../../.."); -const settings_1 = require("./settings"); -Object.defineProperty(exports, "LocalStorageSettings", { enumerable: true, get: function () { return settings_1.LocalStorageSettings; } }); -const transaction_1 = require("./transaction"); -Object.defineProperty(exports, "LocalStorageTransaction", { enumerable: true, get: function () { return transaction_1.LocalStorageTransaction; } }); -function createLocalStorageInstance(dbname, init = {}) { - const settings = new settings_1.LocalStorageSettings(init); - // Determine whether to use localStorage or sessionStorage - const ls = settings.provider ? settings.provider : settings.temp ? localStorage : sessionStorage; - // Setup our CustomStorageSettings - const storageSettings = new __1.CustomStorageSettings({ - name: 'LocalStorage', - locking: true, - removeVoidProperties: settings.removeVoidProperties, - maxInlineValueSize: settings.maxInlineValueSize, - async ready() { - // LocalStorage is always ready - }, - async getTransaction(target) { - // Create an instance of our transaction class - const context = { - debug: true, - dbname, - localStorage: ls, - }; - const transaction = new transaction_1.LocalStorageTransaction(context, target); - return transaction; - }, - }); - const db = new __2.AceBase(dbname, { logLevel: settings.logLevel, storage: storageSettings, sponsor: settings.sponsor }); - db.settings.ipcEvents = settings.multipleTabs === true; - return db; -} -exports.createLocalStorageInstance = createLocalStorageInstance; - -},{"..":21,"../../..":6,"./settings":26,"./transaction":27}],26:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.LocalStorageSettings = void 0; -const __1 = require("../.."); -class LocalStorageSettings extends __1.StorageSettings { - constructor(settings) { - super(settings); - /** - * whether to use sessionStorage instead of localStorage - * @default false - */ - this.temp = false; - /** - * Whether to enable cross-tab synchronization - * @default false - */ - this.multipleTabs = false; - if (typeof settings.temp === 'boolean') { - this.temp = settings.temp; + consumeCharacter('/'); + while (c = definition[pos], ['g', 'i', 'm', 's', 'u', 'y', 'd'].includes(c)) { + flags += c; + pos++; + } + type.typeOf = 'string'; + type.matches = new RegExp(pattern, flags); + } + else { + throw new Error(`Expected a type definition at position ${pos}, found character '${definition[pos]}'`); + } } - if (typeof settings.provider === 'object') { - this.provider = settings.provider; + else if (['string', 'number', 'boolean', 'bigint', 'undefined', 'String', 'Number', 'Boolean', 'BigInt'].includes(name)) { + type.typeOf = name.toLowerCase(); } - if (typeof settings.multipleTabs === 'boolean') { - this.multipleTabs = settings.multipleTabs; + else if (name === 'Object' || name === 'object') { + type.typeOf = 'object'; + type.instanceOf = Object; } - if (typeof settings.logLevel === 'string') { - this.logLevel = settings.logLevel; + else if (name === 'Date') { + type.typeOf = 'object'; + type.instanceOf = Date; } - if (typeof settings.sponsor === 'boolean') { - this.sponsor = settings.sponsor; + else if (name === 'Binary' || name === 'binary') { + type.typeOf = 'object'; + type.instanceOf = ArrayBuffer; } - ['type', 'ipc', 'path'].forEach((prop) => { - if (prop in settings) { - console.warn(`${prop} setting is not supported for AceBase LocalStorage`); - } - }); + else if (name === 'any') { + type.typeOf = 'any'; + } + else if (name === 'null') { + // This is ignored, null values are not stored in the db (null indicates deletion) + type.typeOf = 'object'; + type.value = null; + } + else if (name === 'Array') { + // Read generic Array defintion + consumeCharacter('<'); + type.typeOf = 'object'; + type.instanceOf = Array; //name; + type.genericTypes = readTypes(); + consumeCharacter('>'); + } + else if (['true', 'false'].includes(name)) { + type.typeOf = 'boolean'; + type.value = name === 'true'; + } + else { + throw new Error(`Unknown type at position ${pos}: "${type}"`); + } + // Check if it's an Array of given type (eg: string[] or string[][]) + // Also converts to generics, string[] becomes Array, string[][] becomes Array> + consumeSpaces(); + while (definition[pos] === '[') { + consumeCharacter('['); + consumeCharacter(']'); + type = { typeOf: 'object', instanceOf: Array, genericTypes: [type] }; + } + return type; + } + function readTypes() { + consumeSpaces(); + const types = [readType()]; + while (definition[pos] === '|') { + consumeCharacter('|'); + types.push(readType()); + consumeSpaces(); + } + return types; } + return readType(); } -exports.LocalStorageSettings = LocalStorageSettings; - -},{"../..":29}],27:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.LocalStorageTransaction = void 0; -const __1 = require(".."); -// Setup CustomStorageTransaction for browser's LocalStorage -class LocalStorageTransaction extends __1.CustomStorageTransaction { - constructor(context, target) { - super(target); - this.context = context; - this._storageKeysPrefix = `${this.context.dbname}.acebase::`; +function checkObject(path, properties, obj, partial) { + // Are there any properties that should not be in there? + const invalidProperties = properties.find(prop => prop.name === '*' || prop.name[0] === '$') // Only if no wildcard properties are allowed + ? [] + : Object.keys(obj).filter(key => ![null, undefined].includes(obj[key]) // Ignore null or undefined values + && !properties.find(prop => prop.name === key)); + if (invalidProperties.length > 0) { + return { ok: false, reason: `Object at path "${path}" cannot have propert${invalidProperties.length === 1 ? 'y' : 'ies'} ${invalidProperties.map(p => `"${p}"`).join(', ')}` }; } - async commit() { - // All changes have already been committed. TODO: use same approach as IndexedDB + // Loop through properties that should be present + function checkProperty(property) { + const hasValue = ![null, undefined].includes(obj[property.name]); + if (!property.optional && (partial ? obj[property.name] === null : !hasValue)) { + return { ok: false, reason: `Property at path "${path}/${property.name}" is not optional` }; + } + if (hasValue && property.types.length === 1) { + return checkType(`${path}/${property.name}`, property.types[0], obj[property.name], false); + } + if (hasValue && !property.types.some(type => checkType(`${path}/${property.name}`, type, obj[property.name], false).ok)) { + return { ok: false, reason: `Property at path "${path}/${property.name}" does not match any of ${property.types.length} allowed types` }; + } + return { ok: true }; } - async rollback(err) { - // Not able to rollback changes, because we did not keep track + const namedProperties = properties.filter(prop => !prop.wildcard); + const failedProperty = namedProperties.find(prop => !checkProperty(prop).ok); + if (failedProperty) { + const reason = checkProperty(failedProperty).reason; + return { ok: false, reason }; } - async get(path) { - // Gets value from localStorage, wrapped in Promise - const json = this.context.localStorage.getItem(this.getStorageKeyForPath(path)); - const val = JSON.parse(json); - return val; + const wildcardProperty = properties.find(prop => prop.wildcard); + if (!wildcardProperty) { + return { ok: true }; } - async set(path, val) { - // Sets value in localStorage, wrapped in Promise - const json = JSON.stringify(val); - this.context.localStorage.setItem(this.getStorageKeyForPath(path), json); + const wildcardChildKeys = Object.keys(obj).filter(key => !namedProperties.find(prop => prop.name === key)); + let result = { ok: true }; + for (let i = 0; i < wildcardChildKeys.length && result.ok; i++) { + const childKey = wildcardChildKeys[i]; + result = checkProperty({ name: childKey, types: wildcardProperty.types, optional: true, wildcard: true }); } - async remove(path) { - // Removes a value from localStorage, wrapped in Promise - this.context.localStorage.removeItem(this.getStorageKeyForPath(path)); + return result; +} +function checkType(path, type, value, partial, trailKeys) { + const ok = { ok: true }; + if (type.typeOf === 'any') { + return ok; } - async childrenOf(path, include, checkCallback, addCallback) { - // Streams all child paths - // Cannot query localStorage, so loop through all stored keys to find children - const pathInfo = __1.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; - } - const 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; - } - } + if (trailKeys instanceof Array && trailKeys.length > 0) { + // The value to check resides in a descendant path of given type definition. + // Recursivly check child type definitions to find a match + if (type.typeOf !== 'object') { + return { ok: false, reason: `path "${path}" must be typeof ${type.typeOf}` }; // given value resides in a child path, but parent is not allowed be an object. } - } - async descendantsOf(path, include, checkCallback, addCallback) { - // Streams all descendant paths - // Cannot query localStorage, so loop through all stored keys to find descendants - const pathInfo = __1.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; - } - const 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; - } - } + if (!type.children) { + return ok; + } + const childKey = trailKeys[0]; + let property = type.children.find(prop => prop.name === childKey); + if (!property) { + property = type.children.find(prop => prop.name === '*' || prop.name[0] === '$'); + } + if (!property) { + return { ok: false, reason: `Object at path "${path}" cannot have property "${childKey}"` }; + } + if (property.optional && value === null && trailKeys.length === 1) { + return ok; } + let result; + property.types.some(type => { + const childPath = typeof childKey === 'number' ? `${path}[${childKey}]` : `${path}/${childKey}`; + result = checkType(childPath, type, value, partial, trailKeys.slice(1)); + return result.ok; + }); + return result; } - /** - * Helper function to get the path from a localStorage key - */ - getPathFromStorageKey(key) { - return key.slice(this._storageKeysPrefix.length); + if (value === null) { + return ok; } - /** - * Helper function to get the localStorage key for a path - */ - getStorageKeyForPath(path) { - return `${this._storageKeysPrefix}${path}`; + if (type.instanceOf === Object && (typeof value !== 'object' || value instanceof Array || value instanceof Date)) { + return { ok: false, reason: `path "${path}" must be an object collection` }; } -} -exports.LocalStorageTransaction = LocalStorageTransaction; - -},{"..":21}],28:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.SchemaValidationError = void 0; -class SchemaValidationError extends Error { - constructor(reason) { - super(`Schema validation failed: ${reason}`); - this.reason = reason; + if (type.instanceOf && (typeof value !== 'object' || value.constructor !== type.instanceOf)) { // !(value instanceof type.instanceOf) // value.constructor.name !== type.instanceOf + return { ok: false, reason: `path "${path}" must be an instance of ${type.instanceOf.name}` }; + } + if ('value' in type && value !== type.value) { + return { ok: false, reason: `path "${path}" must be value: ${type.value}` }; } + if (typeof value !== type.typeOf) { + return { ok: false, reason: `path "${path}" must be typeof ${type.typeOf}` }; + } + if (type.instanceOf === Array && type.genericTypes && !value.every(v => type.genericTypes.some(t => checkType(path, t, v, false).ok))) { + return { ok: false, reason: `every array value of path "${path}" must match one of the specified types` }; + } + if (type.typeOf === 'object' && type.children) { + return checkObject(path, type.children, value, partial); + } + if (type.matches && !type.matches.test(value)) { + return { ok: false, reason: `path "${path}" must match regular expression /${type.matches.source}/${type.matches.flags}` }; + } + return ok; } -exports.SchemaValidationError = SchemaValidationError; - -},{}],29:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.Storage = exports.SchemaValidationError = exports.StorageSettings = void 0; -var storage_settings_js_1 = require("./storage-settings.js"); -Object.defineProperty(exports, "StorageSettings", { enumerable: true, get: function () { return storage_settings_js_1.StorageSettings; } }); -var errors_js_1 = require("./errors.js"); -Object.defineProperty(exports, "SchemaValidationError", { enumerable: true, get: function () { return errors_js_1.SchemaValidationError; } }); -var storage_js_1 = require("./storage.js"); -Object.defineProperty(exports, "Storage", { enumerable: true, get: function () { return storage_js_1.Storage; } }); - -},{"./errors.js":28,"./storage-settings.js":33,"./storage.js":34}],30:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.createIndex = void 0; -var create_index_1 = require("./create-index"); -Object.defineProperty(exports, "createIndex", { enumerable: true, get: function () { return create_index_1.createIndex; } }); - -},{"./create-index":19}],31:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.MSSQLStorage = exports.MSSQLStorageSettings = void 0; -const not_supported_1 = require("../../not-supported"); -/** - * Not supported in browser context - */ -class MSSQLStorageSettings extends not_supported_1.NotSupported { +// eslint-disable-next-line @typescript-eslint/ban-types +function getConstructorType(val) { + switch (val) { + case String: return 'string'; + case Number: return 'number'; + case Boolean: return 'boolean'; + case Date: return 'Date'; + case BigInt: return 'bigint'; + case Array: throw new Error('Schema error: Array cannot be used without a type. Use string[] or Array instead'); + default: throw new Error(`Schema error: unknown type used: ${val.name}`); + } } -exports.MSSQLStorageSettings = MSSQLStorageSettings; -/** - * Not supported in browser context - */ -class MSSQLStorage extends not_supported_1.NotSupported { +class SchemaDefinition { + constructor(definition, handling = { warnOnly: false }) { + this.handling = handling; + this.source = definition; + if (typeof definition === 'object') { + // Turn object into typescript definitions + // eg: + // const example = { + // name: String, + // born: Date, + // instrument: "'guitar'|'piano'", + // "address?": { + // street: String + // } + // }; + // Resulting ts: "{name:string,born:Date,instrument:'guitar'|'piano',address?:{street:string}}" + const toTS = (obj) => { + return '{' + Object.keys(obj) + .map(key => { + let val = obj[key]; + if (val === undefined) { + val = 'undefined'; + } + else if (val instanceof RegExp) { + val = `/${val.source}/${val.flags}`; + } + else if (typeof val === 'object') { + val = toTS(val); + } + else if (typeof val === 'function') { + val = getConstructorType(val); + } + else if (!['string', 'number', 'boolean', 'bigint'].includes(typeof val)) { + throw new Error(`Type definition for key "${key}" must be a string, number, boolean, bigint, object, regular expression, or one of these classes: String, Number, Boolean, Date, BigInt`); + } + return `${key}:${val}`; + }) + .join(',') + '}'; + }; + this.text = toTS(definition); + } + else if (typeof definition === 'string') { + this.text = definition; + } + else { + throw new Error('Type definiton must be a string or an object'); + } + this.type = parse(this.text); + } + check(path, value, partial, trailKeys) { + const result = checkType(path, this.type, value, partial, trailKeys); + if (!result.ok && this.handling.warnOnly) { + // Only issue a warning, allows schema definitions to be added to a production db to monitor if they are accurate before enforcing them. + result.warning = `${partial ? 'Partial schema' : 'Schema'} check on path "${path}"${trailKeys ? ` for child "${trailKeys.join('/')}"` : ''} failed: ${result.reason}`; + result.ok = true; + this.handling.warnCallback(result.warning); + } + return result; + } } -exports.MSSQLStorage = MSSQLStorage; +exports.SchemaDefinition = SchemaDefinition; -},{"../../not-supported":15}],32:[function(require,module,exports){ +},{}],20:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.SQLiteStorage = exports.SQLiteStorageSettings = void 0; -const not_supported_1 = require("../../not-supported"); -/** - * Not supported in browser context - */ -class SQLiteStorageSettings extends not_supported_1.NotSupported { -} -exports.SQLiteStorageSettings = SQLiteStorageSettings; +exports.SimpleCache = void 0; +const utils_1 = require("./utils"); +const calculateExpiryTime = (expirySeconds) => expirySeconds > 0 ? Date.now() + (expirySeconds * 1000) : Infinity; /** - * Not supported in browser context + * Simple cache implementation that retains immutable values in memory for a limited time. + * Immutability is enforced by cloning the stored and retrieved values. To change a cached value, it will have to be `set` again with the new value. */ -class SQLiteStorage extends not_supported_1.NotSupported { -} -exports.SQLiteStorage = SQLiteStorage; - -},{"../../not-supported":15}],33:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.StorageSettings = void 0; -/** - * Storage Settings - */ -class StorageSettings { - constructor(settings = {}) { - /** - * in bytes, max amount of child data to store within a parent record before moving to a dedicated record. Default is 50 - * @default 50 - */ - this.maxInlineValueSize = 50; - /** - * Instead of throwing errors on undefined values, remove the properties automatically. Default is false - * @default false - */ - this.removeVoidProperties = false; - /** - * Target path to store database files in, default is `'.'` - * @default '.' - */ - this.path = '.'; - /** - * timeout setting for read and write locks in seconds. Operations taking longer than this will be aborted. Default is 120 seconds. - * @default 120 - */ - this.lockTimeout = 120; - /** - * optional type of storage class - used by `AceBaseStorage` to create different specific db files (data, transaction, auth etc) - * @see AceBaseStorageSettings see `AceBaseStorageSettings.type` for more info - */ - this.type = 'data'; - /** - * Whether the database should be opened in readonly mode - * @default false - */ - this.readOnly = false; - if (typeof settings.maxInlineValueSize === 'number') { - this.maxInlineValueSize = settings.maxInlineValueSize; - } - if (typeof settings.removeVoidProperties === 'boolean') { - this.removeVoidProperties = settings.removeVoidProperties; - } - if (typeof settings.path === 'string') { - this.path = settings.path; - } - if (this.path.endsWith('/')) { - this.path = this.path.slice(0, -1); +class SimpleCache { + get size() { return this.cache.size; } + constructor(options) { + var _a; + this.enabled = true; + if (typeof options === 'number') { + // Old signature: only expirySeconds given + options = { expirySeconds: options }; } - if (typeof settings.lockTimeout === 'number') { - this.lockTimeout = settings.lockTimeout; + options.cloneValues = options.cloneValues !== false; + if (typeof options.expirySeconds !== 'number' && typeof options.maxEntries !== 'number') { + throw new Error('Either expirySeconds or maxEntries must be specified'); } - if (typeof settings.type === 'string') { - this.type = settings.type; + this.options = options; + this.cache = new Map(); + // Cleanup every minute + const interval = setInterval(() => { this.cleanUp(); }, 60 * 1000); + (_a = interval.unref) === null || _a === void 0 ? void 0 : _a.call(interval); + } + has(key) { + if (!this.enabled) { + return false; } - if (typeof settings.readOnly === 'boolean') { - this.readOnly = settings.readOnly; + return this.cache.has(key); + } + get(key) { + if (!this.enabled) { + return null; } - if (['object', 'string'].includes(typeof settings.ipc)) { - this.ipc = settings.ipc; + const entry = this.cache.get(key); + if (!entry) { + return null; + } // if (!entry || entry.expires <= Date.now()) { return null; } + entry.expires = calculateExpiryTime(this.options.expirySeconds); + entry.accessed = Date.now(); + return this.options.cloneValues ? (0, utils_1.cloneObject)(entry.value) : entry.value; + } + set(key, value) { + if (this.options.maxEntries > 0 && this.cache.size >= this.options.maxEntries && !this.cache.has(key)) { + // console.warn(`* cache limit ${this.options.maxEntries} reached: ${this.cache.size}`); + // Remove an expired item or the one that was accessed longest ago + let oldest = null; + const now = Date.now(); + for (const [key, entry] of this.cache.entries()) { + if (entry.expires <= now) { + // Found an expired item. Remove it now and stop + this.cache.delete(key); + oldest = null; + break; + } + if (!oldest || entry.accessed < oldest.accessed) { + oldest = { key, accessed: entry.accessed }; + } + } + if (oldest !== null) { + this.cache.delete(oldest.key); + } } + this.cache.set(key, { value: this.options.cloneValues ? (0, utils_1.cloneObject)(value) : value, added: Date.now(), accessed: Date.now(), expires: calculateExpiryTime(this.options.expirySeconds) }); + } + remove(key) { + this.cache.delete(key); + } + cleanUp() { + const now = Date.now(); + this.cache.forEach((entry, key) => { + if (entry.expires <= now) { + this.cache.delete(key); + } + }); } } -exports.StorageSettings = StorageSettings; +exports.SimpleCache = SimpleCache; -},{}],34:[function(require,module,exports){ +},{"./utils":27}],21:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.Storage = void 0; -const acebase_core_1 = require("acebase-core"); -const index_js_1 = require("../ipc/index.js"); -const assert_js_1 = require("../assert.js"); -const index_js_2 = require("../data-index/index.js"); -const indexes_js_1 = require("./indexes.js"); -const index_js_3 = require("../promise-fs/index.js"); -const errors_js_1 = require("./errors.js"); -const node_errors_js_1 = require("../node-errors.js"); -const node_info_js_1 = require("../node-info.js"); -const node_value_types_js_1 = require("../node-value-types.js"); -const { compareValues, getChildValues, encodeString, defer } = acebase_core_1.Utils; -const DEBUG_MODE = false; -const SUPPORTED_EVENTS = ['value', 'child_added', 'child_changed', 'child_removed', 'mutated', 'mutations']; -// Add 'notify_*' event types for each event to enable data-less notifications, so data retrieval becomes optional -SUPPORTED_EVENTS.push(...SUPPORTED_EVENTS.map(event => `notify_${event}`)); -// eslint-disable-next-line @typescript-eslint/no-empty-function -const NOOP = () => { }; -class Storage extends acebase_core_1.SimpleEventEmitter { - createTid() { - return DEBUG_MODE ? ++this._lastTid : acebase_core_1.ID.generate(); +exports.Colorize = exports.SetColorsEnabled = exports.ColorsSupported = exports.ColorStyle = void 0; +const process_1 = require("./process"); +// See from https://en.wikipedia.org/wiki/ANSI_escape_code +const FontCode = { + bold: 1, + dim: 2, + italic: 3, + underline: 4, + inverse: 7, + hidden: 8, + strikethrough: 94, +}; +const ColorCode = { + black: 30, + red: 31, + green: 32, + yellow: 33, + blue: 34, + magenta: 35, + cyan: 36, + white: 37, + grey: 90, + // Bright colors: + brightRed: 91, + // TODO, other bright colors +}; +const BgColorCode = { + bgBlack: 40, + bgRed: 41, + bgGreen: 42, + bgYellow: 43, + bgBlue: 44, + bgMagenta: 45, + bgCyan: 46, + bgWhite: 47, + bgGrey: 100, + bgBrightRed: 101, + // TODO, other bright colors +}; +const ResetCode = { + all: 0, + color: 39, + background: 49, + bold: 22, + dim: 22, + italic: 23, + underline: 24, + inverse: 27, + hidden: 28, + strikethrough: 29, +}; +var ColorStyle; +(function (ColorStyle) { + ColorStyle["reset"] = "reset"; + ColorStyle["bold"] = "bold"; + ColorStyle["dim"] = "dim"; + ColorStyle["italic"] = "italic"; + ColorStyle["underline"] = "underline"; + ColorStyle["inverse"] = "inverse"; + ColorStyle["hidden"] = "hidden"; + ColorStyle["strikethrough"] = "strikethrough"; + ColorStyle["black"] = "black"; + ColorStyle["red"] = "red"; + ColorStyle["green"] = "green"; + ColorStyle["yellow"] = "yellow"; + ColorStyle["blue"] = "blue"; + ColorStyle["magenta"] = "magenta"; + ColorStyle["cyan"] = "cyan"; + ColorStyle["grey"] = "grey"; + ColorStyle["bgBlack"] = "bgBlack"; + ColorStyle["bgRed"] = "bgRed"; + ColorStyle["bgGreen"] = "bgGreen"; + ColorStyle["bgYellow"] = "bgYellow"; + ColorStyle["bgBlue"] = "bgBlue"; + ColorStyle["bgMagenta"] = "bgMagenta"; + ColorStyle["bgCyan"] = "bgCyan"; + ColorStyle["bgWhite"] = "bgWhite"; + ColorStyle["bgGrey"] = "bgGrey"; +})(ColorStyle = exports.ColorStyle || (exports.ColorStyle = {})); +function ColorsSupported() { + // Checks for basic color support + if (typeof process_1.default === 'undefined' || !process_1.default.stdout || !process_1.default.env || !process_1.default.platform || process_1.default.platform === 'browser') { + return false; } - /** - * Base class for database storage, must be extended by back-end specific methods. - * Currently implemented back-ends are AceBaseStorage, SQLiteStorage, MSSQLStorage, CustomStorage - * @param name name of the database - * @param settings instance of AceBaseStorageSettings or SQLiteStorageSettings - */ - constructor(name, settings, env) { - var _a; - super(); - this.name = name; - this.settings = settings; - // private _validation = new Map boolean, schema?: SchemaDefinition }>; - this._schemas = []; - this._indexes = []; - this._annoucedIndexes = new Map(); - this.indexes = { - /** - * Tests if (the default storage implementation of) indexes are supported in the environment. - * They are currently only supported when running in Node.js because they use the fs filesystem. - * TODO: Implement storage specific indexes (eg in SQLite, MySQL, MSSQL, in-memory) - */ - get supported() { - return index_js_3.pfs === null || index_js_3.pfs === void 0 ? void 0 : index_js_3.pfs.hasFileSystem; - }, - create: (path, key, options = { - rebuild: false, - }) => { - const context = { storage: this, logger: this.logger, indexes: this._indexes, ipc: this.ipc }; - return (0, indexes_js_1.createIndex)(context, path, key, options); - }, - /** - * Returns indexes at a path, or a specific index on a key in that path - */ - get: (path, key = null) => { - if (path.includes('$')) { - // Replace $variables in path with * wildcards - const pathKeys = acebase_core_1.PathInfo.getPathKeys(path).map(key => typeof key === 'string' && key.startsWith('$') ? '*' : key); - path = (new acebase_core_1.PathInfo(pathKeys)).path; - } - return this._indexes.filter(index => index.path === path && - (key === null || key === index.key)); - }, - /** - * Returns all indexes on a target path, optionally includes indexes on child and parent paths - */ - getAll: (targetPath, options = { parentPaths: true, childPaths: true }) => { - const pathKeys = acebase_core_1.PathInfo.getPathKeys(targetPath); - return this._indexes.filter(index => { - const indexKeys = acebase_core_1.PathInfo.getPathKeys(index.path + '/*'); - // check if index is on a parent node of given path: - if (options.parentPaths && indexKeys.every((key, i) => { return key === '*' || pathKeys[i] === key; }) && [index.key].concat(...index.includeKeys).includes(pathKeys[indexKeys.length])) { - // eg: path = 'restaurants/1/location/lat', index is on 'restaurants(/*)', key 'location' - return true; - } - else if (indexKeys.length < pathKeys.length) { - // the index is on a higher path, and did not match above parent paths check - return false; - } - else if (!options.childPaths && indexKeys.length !== pathKeys.length) { - // no checking for indexes on child paths and index path has more or less keys than path - // eg: path = 'restaurants/1', index is on child path 'restaurants/*/reviews(/*)', key 'rating' - return false; - } - // check if all path's keys match the index path - // eg: path = 'restaurants/1', index is on 'restaurants(/*)', key 'name' - // or: path = 'restaurants/1', index is on 'restaurants/*/reviews(/*)', key 'rating' (and options.childPaths === true) - return pathKeys.every((key, i) => { - return [key, '*'].includes(indexKeys[i]); //key === indexKeys[i] || indexKeys[i] === '*'; - }); - }); - }, - /** - * Returns all indexes - */ - list: () => { - return this._indexes.slice(); - }, - /** - * Discovers and populates all created indexes - */ - load: async () => { - this._indexes.splice(0); - if (!index_js_3.pfs.hasFileSystem) { - // If pfs (fs) is not available, don't try using it - return; - } - let files = []; - try { - files = (await index_js_3.pfs.readdir(`${this.settings.path}/${this.name}.acebase`)); - } - catch (err) { - if (err.code !== 'ENOENT') { - // If the directory is not found, there are no file indexes. (probably not supported by used storage class) - // Only complain if error is something else - this.logger.error(err); - } - } - const promises = []; - files.forEach(fileName => { - if (!fileName.endsWith('.idx')) { - return; + if (process_1.default.platform === 'win32') { + return true; + } + const env = process_1.default.env; + if (env.COLORTERM) { + return true; + } + if (env.TERM === 'dumb') { + return false; + } + if (env.CI || env.TEAMCITY_VERSION) { + return !!env.TRAVIS; + } + if (['iTerm.app', 'HyperTerm', 'Hyper', 'MacTerm', 'Apple_Terminal', 'vscode'].includes(env.TERM_PROGRAM)) { + return true; + } + if (/^xterm-256|^screen|^xterm|^vt100|color|ansi|cygwin|linux/i.test(env.TERM)) { + return true; + } + return false; +} +exports.ColorsSupported = ColorsSupported; +let _enabled = ColorsSupported(); +function SetColorsEnabled(enabled) { + _enabled = ColorsSupported() && enabled; +} +exports.SetColorsEnabled = SetColorsEnabled; +function Colorize(str, style) { + if (!_enabled) { + return str; + } + const openCodes = [], closeCodes = []; + const addStyle = (style) => { + if (style === ColorStyle.reset) { + openCodes.push(ResetCode.all); + } + else if (style in FontCode) { + openCodes.push(FontCode[style]); + closeCodes.push(ResetCode[style]); + } + else if (style in ColorCode) { + openCodes.push(ColorCode[style]); + closeCodes.push(ResetCode.color); + } + else if (style in BgColorCode) { + openCodes.push(BgColorCode[style]); + closeCodes.push(ResetCode.background); + } + }; + if (style instanceof Array) { + style.forEach(addStyle); + } + else { + addStyle(style); + } + // const open = '\u001b[' + openCodes.join(';') + 'm'; + // const close = '\u001b[' + closeCodes.join(';') + 'm'; + const open = openCodes.map(code => '\u001b[' + code + 'm').join(''); + const close = closeCodes.map(code => '\u001b[' + code + 'm').join(''); + // return open + str + close; + return str.split('\n').map(line => open + line + close).join('\n'); +} +exports.Colorize = Colorize; +String.prototype.colorize = function (style) { + return Colorize(this, style); +}; + +},{"./process":18}],22:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.SimpleEventEmitter = void 0; +function runCallback(callback, data) { + try { + callback(data); + } + catch (err) { + console.error('Error in subscription callback', err); + } +} +const _subscriptions = Symbol('subscriptions'); +const _oneTimeEvents = Symbol('oneTimeEvents'); +class SimpleEventEmitter { + constructor() { + this[_subscriptions] = []; + this[_oneTimeEvents] = new Map(); + } + on(event, callback) { + if (this[_oneTimeEvents].has(event)) { + return runCallback(callback, this[_oneTimeEvents].get(event)); + } + this[_subscriptions].push({ event, callback, once: false }); + return this; + } + off(event, callback) { + this[_subscriptions] = this[_subscriptions].filter(s => s.event !== event || (callback && s.callback !== callback)); + return this; + } + once(event, callback) { + return new Promise(resolve => { + const ourCallback = (data) => { + resolve(data); + callback === null || callback === void 0 ? void 0 : callback(data); + }; + if (this[_oneTimeEvents].has(event)) { + runCallback(ourCallback, this[_oneTimeEvents].get(event)); + } + else { + this[_subscriptions].push({ event, callback: ourCallback, once: true }); + } + }); + } + emit(event, data) { + if (this[_oneTimeEvents].has(event)) { + throw new Error(`Event "${event}" was supposed to be emitted only once`); + } + for (let i = 0; i < this[_subscriptions].length; i++) { + const s = this[_subscriptions][i]; + if (s.event !== event) { + continue; + } + runCallback(s.callback, data); + if (s.once) { + this[_subscriptions].splice(i, 1); + i--; + } + } + return this; + } + emitOnce(event, data) { + if (this[_oneTimeEvents].has(event)) { + throw new Error(`Event "${event}" was supposed to be emitted only once`); + } + this.emit(event, data); + this[_oneTimeEvents].set(event, data); // Mark event as being emitted once for future subscribers + this.off(event); // Remove all listeners for this event, they won't fire again + return this; + } + pipe(event, eventEmitter) { + this.on(event, (data) => { + eventEmitter.emit(event, data); + }); + } + pipeOnce(event, eventEmitter) { + this.once(event, (data) => { + eventEmitter.emitOnce(event, data); + }); + } +} +exports.SimpleEventEmitter = SimpleEventEmitter; + +},{}],23:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.SimpleObservable = void 0; +/** + * rxjs is an optional dependency that only needs installing when any of AceBase's observe methods are used. + * If for some reason rxjs is not available (eg in test suite), we can provide a shim. This class is used when + * `db.setObservable("shim")` is called + */ +class SimpleObservable { + constructor(create) { + this._active = false; + this._subscribers = []; + this._create = create; + } + subscribe(subscriber) { + if (!this._active) { + const next = (value) => { + // emit value to all subscribers + this._subscribers.forEach(s => { + try { + s(value); } - const needsStoragePrefix = this.settings.type !== 'data'; // auth indexes need to start with "[auth]-" and have to be ignored by other storage types - const hasStoragePrefix = /^\[[a-z]+\]-/.test(fileName); - if ((!needsStoragePrefix && !hasStoragePrefix) || needsStoragePrefix && fileName.startsWith(`[${this.settings.type}]-`)) { - const p = this.indexes.add(fileName); - promises.push(p); + catch (err) { + console.error('Error in subscriber callback:', err); } }); - await Promise.all(promises); - }, - add: async (fileName) => { - const existingIndex = this._indexes.find(index => index.fileName === fileName); - if (existingIndex) { - return existingIndex; - } - else if (this._annoucedIndexes.has(fileName)) { - // Index is already in the process of being added, wait until it becomes availabe - const index = await this._annoucedIndexes.get(fileName); - return index; + }; + const observer = { next }; + this._cleanup = this._create(observer); + this._active = true; + } + this._subscribers.push(subscriber); + const unsubscribe = () => { + this._subscribers.splice(this._subscribers.indexOf(subscriber), 1); + if (this._subscribers.length === 0) { + this._active = false; + this._cleanup(); + } + }; + const subscription = { + unsubscribe, + }; + return subscription; + } +} +exports.SimpleObservable = SimpleObservable; + +},{}],24:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.EventStream = exports.EventPublisher = exports.EventSubscription = void 0; +class EventSubscription { + /** + * @param stop function that stops the subscription from receiving future events + */ + constructor(stop) { + this.stop = stop; + this._internal = { + state: 'init', + activatePromises: [], + }; + } + /** + * Notifies when subscription is activated or canceled + * @param callback optional callback to run each time activation state changes + * @returns returns a promise that resolves once activated, or rejects when it is denied (and no callback was supplied) + */ + activated(callback) { + if (callback) { + this._internal.activatePromises.push({ callback }); + if (this._internal.state === 'active') { + callback(true); + } + else if (this._internal.state === 'canceled') { + callback(false, this._internal.cancelReason); + } + } + // Changed behaviour: now also returns a Promise when the callback is used. + // This allows for 1 activated call to both handle: first activation result, + // and any future events using the callback + 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)); + } + // eslint-disable-next-line @typescript-eslint/no-empty-function + const noop = () => { }; + this._internal.activatePromises.push({ + resolve, + reject: callback ? noop : reject, // Don't reject when callback is used: let callback handle this (prevents UnhandledPromiseRejection if only callback is used) + }); + }); + } + /** (for internal use) */ + _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); + } + } + } +} +exports.EventSubscription = EventSubscription; +class EventPublisher { + /** + * + * @param publish function that publishes a new value to subscribers, return if there are any active subscribers + * @param start function that notifies subscribers their subscription is activated + * @param cancel function that notifies subscribers their subscription has been canceled, removes all subscriptions + */ + constructor(publish, start, cancel) { + this.publish = publish; + this.start = start; + this.cancel = cancel; + } +} +exports.EventPublisher = EventPublisher; +class EventStream { + constructor(eventPublisherCallback) { + const subscribers = []; + let noMoreSubscribersCallback; + let activationState; // TODO: refactor to string only: STATE_INIT, STATE_STOPPED, STATE_ACTIVATED, STATE_CANCELED + const STATE_STOPPED = 'stopped (no more subscribers)'; + this.subscribe = (callback, activationCallback) => { + if (typeof callback !== 'function') { + throw new TypeError('callback must be a function'); + } + else if (activationState === STATE_STOPPED) { + throw new Error('stream can\'t be used anymore because all subscribers were stopped'); + } + const sub = { + callback, + activationCallback: function (activated, cancelReason) { + activationCallback === null || activationCallback === void 0 ? void 0 : activationCallback(activated, cancelReason); + this.subscription._setActivationState(activated, cancelReason); + }, + subscription: new EventSubscription(function stop() { + subscribers.splice(subscribers.indexOf(this), 1); + return checkActiveSubscribers(); + }), + }; + subscribers.push(sub); + if (typeof activationState !== 'undefined') { + if (activationState === true) { + activationCallback === null || activationCallback === void 0 ? void 0 : activationCallback(true); + sub.subscription._setActivationState(true); + } + else if (typeof activationState === 'string') { + activationCallback === null || activationCallback === void 0 ? void 0 : activationCallback(false, activationState); + sub.subscription._setActivationState(false, activationState); } + } + return sub.subscription; + }; + const checkActiveSubscribers = () => { + let ret; + if (subscribers.length === 0) { + ret = noMoreSubscribersCallback === null || noMoreSubscribersCallback === void 0 ? void 0 : noMoreSubscribersCallback(); + activationState = STATE_STOPPED; + } + return Promise.resolve(ret); + }; + this.unsubscribe = (callback) => { + 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 = () => { + // Stop (remove) all subscriptions + subscribers.splice(0); + checkActiveSubscribers(); + }; + /** + * For publishing side: adds a value that will trigger callbacks to all subscribers + * @param val + * @returns returns whether there are subscribers left + */ + const publish = (val) => { + subscribers.forEach(sub => { try { - // Announce the index to prevent race condition in between reading and receiving the IPC index.created notification - const indexPromise = index_js_2.DataIndex.readFromFile(this, fileName); - this._annoucedIndexes.set(fileName, indexPromise); - const index = await indexPromise; - this._indexes.push(index); - this._annoucedIndexes.delete(fileName); - return index; + sub.callback(val); } catch (err) { - this.logger.error(err); - return null; - } - }, - /** - * Deletes an index from the database - */ - delete: async (fileName) => { - const index = await this.indexes.remove(fileName); - await index.delete(); - this.ipc.sendNotification({ type: 'index.deleted', fileName: index.fileName, path: index.path, keys: index.key }); - }, - /** - * Removes an index from the list. Does not delete the actual file, `delete` does that! - * @returns returns the removed index - */ - remove: async (fileName) => { - const index = this._indexes.find(index => index.fileName === fileName); - if (!index) { - throw new Error(`Index ${fileName} not found`); + console.error(`Error running subscriber callback: ${err.message}`); } - this._indexes.splice(this._indexes.indexOf(index), 1); - return index; - }, - close: async () => { - // Close all indexes - const promises = this.indexes.list().map(index => index.close().catch(err => this.logger.error(err))); - await Promise.all(promises); - }, + }); + if (subscribers.length === 0) { + checkActiveSubscribers(); + } + return subscribers.length > 0; }; - this._eventSubscriptions = {}; - this.subscriptions = { - /** - * Adds a subscription to a node - * @param path Path to the node to add subscription to - * @param type Type of the subscription - * @param callback Subscription callback function - */ - add: (path, type, callback) => { - if (SUPPORTED_EVENTS.indexOf(type) < 0) { - throw new TypeError(`Invalid event type "${type}"`); - } - let pathSubs = this._eventSubscriptions[path]; - if (!pathSubs) { - pathSubs = this._eventSubscriptions[path] = []; - } - // if (pathSubs.findIndex(ps => ps.type === type && ps.callback === callback)) { - // this.logger.warn(`Identical subscription of type ${type} on path "${path}" being added`); - // } - pathSubs.push({ created: Date.now(), type, callback }); - this.emit('subscribe', { path, event: type, callback }); // Enables IPC peers to be notified - }, - /** - * Removes 1 or more subscriptions from a node - * @param path Path to the node to remove the subscription from - * @param type Type of subscription(s) to remove (optional: if omitted all types will be removed) - * @param callback Callback to remove (optional: if omitted all of the same type will be removed) - */ - remove: (path, type, callback) => { - const pathSubs = this._eventSubscriptions[path]; - if (!pathSubs) { - return; - } - const next = () => pathSubs.findIndex(ps => (type ? ps.type === type : true) && (callback ? ps.callback === callback : true)); - let i; - while ((i = next()) >= 0) { - pathSubs.splice(i, 1); - } - this.emit('unsubscribe', { path, event: type, callback }); // Enables IPC peers to be notified - }, - /** - * Checks if there are any subscribers at given path that need the node's previous value when a change is triggered - * @param path - */ - hasValueSubscribersForPath(path) { - const valueNeeded = this.getValueSubscribersForPath(path); - return !!valueNeeded; - }, - /** - * Gets all subscribers at given path that need the node's previous value when a change is triggered - * @param path - */ - getValueSubscribersForPath: (path) => { - // Subscribers that MUST have the entire previous value of a node before updating: - // - "value" events on the path itself, and any ancestor path - // - "child_added", "child_removed" events on the parent path - // - "child_changed" events on the parent path and its ancestors - // - ALL events on child/descendant paths - const pathInfo = new acebase_core_1.PathInfo(path); - const valueSubscribers = []; - Object.keys(this._eventSubscriptions).forEach(subscriptionPath => { - if (pathInfo.equals(subscriptionPath) || pathInfo.isDescendantOf(subscriptionPath)) { - // path being updated === subscriptionPath, or a child/descendant path of it - // eg path === "posts/123/title" - // and subscriptionPath is "posts/123/title", "posts/$postId/title", "posts/123", "posts/*", "posts" etc - const pathSubs = this._eventSubscriptions[subscriptionPath]; - const eventPath = acebase_core_1.PathInfo.fillVariables(subscriptionPath, path); - pathSubs - .filter(sub => !sub.type.startsWith('notify_')) // notify events don't need additional value loading - .forEach(sub => { - let dataPath = null; - if (sub.type === 'value') { // ["value", "notify_value"].includes(sub.type) - dataPath = eventPath; - } - else if (['mutated', 'mutations'].includes(sub.type) && pathInfo.isDescendantOf(eventPath)) { //["mutated", "notify_mutated"].includes(sub.type) - dataPath = path; // Only needed data is the properties being updated in the targeted path - } - else if (sub.type === 'child_changed' && path !== eventPath) { // ["child_changed", "notify_child_changed"].includes(sub.type) - const childKey = acebase_core_1.PathInfo.getPathKeys(path.slice(eventPath.length).replace(/^\//, ''))[0]; - dataPath = acebase_core_1.PathInfo.getChildPath(eventPath, childKey); - } - else if (['child_added', 'child_removed'].includes(sub.type) && pathInfo.isChildOf(eventPath)) { //["child_added", "child_removed", "notify_child_added", "notify_child_removed"] - const childKey = acebase_core_1.PathInfo.getPathKeys(path.slice(eventPath.length).replace(/^\//, ''))[0]; - dataPath = acebase_core_1.PathInfo.getChildPath(eventPath, childKey); - } - if (dataPath !== null && !valueSubscribers.some(s => s.type === sub.type && s.eventPath === eventPath)) { - valueSubscribers.push({ type: sub.type, eventPath, dataPath, subscriptionPath }); - } - }); - } - }); - return valueSubscribers; - }, - /** - * Gets all subscribers at given path that could possibly be invoked after a node is updated - */ - getAllSubscribersForPath: (path) => { - const pathInfo = acebase_core_1.PathInfo.get(path); - const subscribers = []; - Object.keys(this._eventSubscriptions).forEach(subscriptionPath => { - // if (pathInfo.equals(subscriptionPath) //path === subscriptionPath - // || pathInfo.isDescendantOf(subscriptionPath) - // || pathInfo.isAncestorOf(subscriptionPath) - // ) { - if (pathInfo.isOnTrailOf(subscriptionPath)) { - const pathSubs = this._eventSubscriptions[subscriptionPath]; - const eventPath = acebase_core_1.PathInfo.fillVariables(subscriptionPath, path); - pathSubs.forEach(sub => { - let dataPath = null; - if (sub.type === 'value' || sub.type === 'notify_value') { - dataPath = eventPath; - } - else if (['child_changed', 'notify_child_changed'].includes(sub.type)) { - const childKey = path === eventPath || pathInfo.isAncestorOf(eventPath) - ? '*' - : acebase_core_1.PathInfo.getPathKeys(path.slice(eventPath.length).replace(/^\//, ''))[0]; - dataPath = acebase_core_1.PathInfo.getChildPath(eventPath, childKey); - } - else if (['mutated', 'mutations', 'notify_mutated', 'notify_mutations'].includes(sub.type)) { - dataPath = path; - } - else if (['child_added', 'child_removed', 'notify_child_added', 'notify_child_removed'].includes(sub.type) - && (pathInfo.isChildOf(eventPath) - || path === eventPath - || pathInfo.isAncestorOf(eventPath))) { - const childKey = path === eventPath || pathInfo.isAncestorOf(eventPath) - ? '*' - : acebase_core_1.PathInfo.getPathKeys(path.slice(eventPath.length).replace(/^\//, ''))[0]; - dataPath = acebase_core_1.PathInfo.getChildPath(eventPath, childKey); //NodePath(subscriptionPath).childPath(childKey); - } - if (dataPath !== null && !subscribers.some(s => s.type === sub.type && s.eventPath === eventPath && s.subscriptionPath === subscriptionPath)) { // && subscribers.findIndex(s => s.type === sub.type && s.dataPath === dataPath) < 0 - subscribers.push({ type: sub.type, eventPath, dataPath, subscriptionPath }); - } - }); - } - }); - return subscribers; - }, - /** - * Triggers subscription events to run on relevant nodes - * @param event Event type: "value", "child_added", "child_changed", "child_removed" - * @param path Path to the node the subscription is on - * @param dataPath path to the node the value is stored - * @param oldValue old value - * @param newValue new value - * @param context context used by the client that updated this data - */ - trigger: (event, path, dataPath, oldValue, newValue, context) => { - //console.warn(`Event "${event}" triggered on node "/${path}" with data of "/${dataPath}": `, newValue); - const pathSubscriptions = this._eventSubscriptions[path] || []; - pathSubscriptions.filter(sub => sub.type === event) - .forEach(sub => { - sub.callback(null, dataPath, newValue, oldValue, context); - // if (event.startsWith('notify_')) { - // // Notify only event, run callback without data - // sub.callback(null, dataPath); - // } - // else { - // // Run callback with data - // sub.callback(null, dataPath, newValue, oldValue); - // } - }); - }, + /** + * For publishing side: let subscribers know their subscription is activated. Should be called only once + */ + const start = (allSubscriptionsStoppedCallback) => { + activationState = true; + noMoreSubscribersCallback = allSubscriptionsStoppedCallback; + subscribers.forEach(sub => { + var _a; + (_a = sub.activationCallback) === null || _a === void 0 ? void 0 : _a.call(sub, true); + }); }; - this.logger = (_a = env.logger) !== null && _a !== void 0 ? _a : new acebase_core_1.DebugLogger(env.logLevel, `[${name}${typeof settings.type === 'string' && settings.type !== 'data' ? `:${settings.type}` : ''}]`); // `├ ${name} ┤` // `[🧱${name}]` - // Setup IPC to allow vertical scaling (multiple threads sharing locks and data) - const ipcName = name + (typeof settings.type === 'string' ? `_${settings.type}` : ''); - const ipcSocketSettings = typeof settings.ipc === 'object' && settings.ipc !== null && 'role' in settings.ipc && settings.ipc.role === 'socket' - ? settings.ipc - : null; - if (ipcSocketSettings || settings.ipc === 'socket' || settings.ipc instanceof index_js_1.NetIPCServer) { - const ipcSettings = Object.assign({ ipcName, server: settings.ipc instanceof index_js_1.NetIPCServer ? settings.ipc : null }, (ipcSocketSettings && { maxIdleTime: ipcSocketSettings.maxIdleTime, loggerPluginPath: ipcSocketSettings.loggerPluginPath })); - this.ipc = new index_js_1.IPCSocketPeer(this, ipcSettings); - } - else if (settings.ipc) { - const ipcClientSettings = settings.ipc; - if (typeof ipcClientSettings.port !== 'number') { - throw new Error('IPC port number must be a number'); - } - if (!['master', 'worker'].includes(ipcClientSettings.role)) { - throw new Error(`IPC client role must be either "master" or "worker", not "${ipcClientSettings.role}"`); - } - const ipcSettings = Object.assign({ dbname: ipcName }, ipcClientSettings); - this.ipc = new index_js_1.RemoteIPCPeer(this, ipcSettings); - } - else { - this.ipc = new index_js_1.IPCPeer(this, ipcName); - } - this.ipc.once('exit', (code) => { - // We can perform any custom cleanup here: - // - storage-acebase should close the db file - // - storage-mssql / sqlite should close connection - // - indexes should close their files - if (this.indexes.supported) { - this.indexes.close(); - } - }); - this.nodeLocker = { - lock: async (path, tid, write, comment) => { - const lock = await this.ipc.lock({ path, tid, write, comment }); - return lock; - }, + /** + * For publishing side: let subscribers know their subscription has been canceled. Should be called only once + */ + const cancel = (reason) => { + activationState = reason; + subscribers.forEach(sub => { + var _a; + (_a = sub.activationCallback) === null || _a === void 0 ? void 0 : _a.call(sub, false, reason || new Error('unknown reason')); + }); + subscribers.splice(0); // Clear all }; - // this.transactionManager = new IPCTransactionManager(this.ipc); - this._lastTid = 0; - } // end of constructor - async close() { - // Close the database by calling exit on the ipc channel, which will emit an 'exit' event when the database can be safely closed. - await this.ipc.exit(); + const publisher = new EventPublisher(publish, start, cancel); + eventPublisherCallback(publisher); } - get path() { - return `${this.settings.path}/${this.name}.acebase`; +} +exports.EventStream = EventStream; + +},{}],25:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.deserialize2 = exports.serialize2 = exports.serialize = exports.detectSerializeVersion = exports.deserialize = void 0; +const path_reference_1 = require("./path-reference"); +const utils_1 = require("./utils"); +const ascii85_1 = require("./ascii85"); +const path_info_1 = require("./path-info"); +const partial_array_1 = require("./partial-array"); +/* + There are now 2 different serialization methods for transporting values. + + v1: + The original version (v1) created an object with "map" and "val" properties. + The "map" property was made optional in v1.14.1 so they won't be present for values needing no serializing + + v2: + The new version replaces serialized values inline by objects containing ".type" and ".val" properties. + This serializing method was introduced by `export` and `import` methods because they use streaming and + are unable to prepare type mappings up-front. This format is smaller in transmission (in many cases), + and easier to read and process. + + original: { "date": (some date) } + v1 serialized: { "map": { "date": "date" }, "val": { date: "2022-04-22T07:49:23Z" } } + v2 serialized: { "date": { ".type": "date", ".val": "2022-04-22T07:49:23Z" } } + + original: (some date) + v1 serialized: { "map": "date", "val": "2022-04-22T07:49:23Z" } + v2 serialized: { ".type": "date", ".val": "2022-04-22T07:49:23Z" } + comment: top level value that need serializing is wrapped in an object with ".type" and ".val". v1 is smaller in this case + + original: 'some string' + v1 serialized: { "map": {}, "val": "some string" } + v2 serialized: "some string" + comment: primitive types such as strings don't need serializing and are returned as is in v2 + + original: { "date": (some date), "text": "Some string" } + v1 serialized: { "map": { "date": "date" }, "val": { date: "2022-04-22T07:49:23Z", "text": "Some string" } } + v2 serialized: { "date": { ".type": "date", ".val": "2022-04-22T07:49:23Z" }, "text": "Some string" } +*/ +/** + * Original deserialization method using global `map` and `val` properties + * @param data + * @returns + */ +const deserialize = (data) => { + if (data.map === null || typeof data.map === 'undefined') { + if (typeof data.val === 'undefined') { + throw new Error('serialized value must have a val property'); + } + return data.val; } - /** - * Checks if a value can be stored in a parent object, or if it should - * move to a dedicated record. Uses settings.maxInlineValueSize - * @param value - */ - valueFitsInline(value) { - if (typeof value === 'number' || typeof value === 'boolean' || value instanceof Date) { - return true; + const deserializeValue = (type, val) => { + if (type === 'date') { + // Date was serialized as a string (UTC) + return new Date(val); } - else if (typeof value === 'string') { - if (value.length > this.settings.maxInlineValueSize) { - return false; - } - // if the string has unicode chars, its byte size will be bigger than value.length - const encoded = encodeString(value); - return encoded.length < this.settings.maxInlineValueSize; + else if (type === 'binary') { + // ascii85 encoded binary data + return ascii85_1.ascii85.decode(val); } - else if (value instanceof acebase_core_1.PathReference) { - if (value.path.length > this.settings.maxInlineValueSize) { - return false; - } - // if the path has unicode chars, its byte size will be bigger than value.path.length - const encoded = encodeString(value.path); - return encoded.length < this.settings.maxInlineValueSize; + else if (type === 'reference') { + return new path_reference_1.PathReference(val); } - else if (value instanceof ArrayBuffer) { - return value.byteLength < this.settings.maxInlineValueSize; + else if (type === 'regexp') { + return new RegExp(val.pattern, val.flags); } - else if (value instanceof Array) { - return value.length === 0; + else if (type === 'array') { + return new partial_array_1.PartialArray(val); } - else if (typeof value === 'object') { - return Object.keys(value).length === 0; + else if (type === 'bigint') { + return BigInt(val); } - else { - throw new TypeError('What else is there?'); + return val; + }; + if (typeof data.map === 'string') { + // Single value + return deserializeValue(data.map, data.val); + } + Object.keys(data.map).forEach(path => { + const type = data.map[path]; + const keys = path_info_1.PathInfo.getPathKeys(path); + let parent = data; + let key = 'val'; + let val = data.val; + keys.forEach(k => { + key = k; + parent = val; + val = val[key]; // If an error occurs here, there's something wrong with the calling code... + }); + parent[key] = deserializeValue(type, val); + }); + return data.val; +}; +exports.deserialize = deserialize; +/** + * Function to detect the used serialization method with for the given object + * @param data + * @returns + */ +const detectSerializeVersion = (data) => { + if (typeof data !== 'object' || data === null) { + // This can only be v2, which allows primitive types to bypass serializing + return 2; + } + if ('map' in data && 'val' in data) { + return 1; + } + else if ('val' in data) { + // If it's v1, 'val' will be the only key in the object because serialize2 adds ".version": 2 to the object to prevent confusion. + if (Object.keys(data).length > 1) { + return 2; } + return 1; } - /** - * 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 - */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _writeNode(path, value, options) { - throw new Error('This method must be implemented by subclass'); + return 2; +}; +exports.detectSerializeVersion = detectSerializeVersion; +/** + * Original serialization method using global `map` and `val` properties + * @param data + * @returns + */ +const serialize = (obj) => { + var _a; + // Recursively find dates and binary data + if (obj === null || typeof obj !== 'object' || obj instanceof Date || obj instanceof ArrayBuffer || obj instanceof path_reference_1.PathReference || obj instanceof RegExp) { + // Single value + const ser = (0, exports.serialize)({ value: obj }); + return { + map: (_a = ser.map) === null || _a === void 0 ? void 0 : _a.value, + val: ser.val.value, + }; } - getUpdateImpact(path, suppressEvents) { - let topEventPath = path; - let hasValueSubscribers = false; - // Get all subscriptions that should execute on the data (includes events on child nodes as well) - const eventSubscriptions = suppressEvents ? [] : this.subscriptions.getAllSubscribersForPath(path); - // Get all subscriptions for data on this or ancestor nodes, determines what data to load before processing - const valueSubscribers = suppressEvents ? [] : this.subscriptions.getValueSubscribersForPath(path); - if (valueSubscribers.length > 0) { - hasValueSubscribers = true; - const eventPaths = valueSubscribers - .map(sub => { return { path: sub.dataPath, keys: acebase_core_1.PathInfo.getPathKeys(sub.dataPath) }; }) - .sort((a, b) => { - if (a.keys.length < b.keys.length) { - return -1; - } - else if (a.keys.length > b.keys.length) { - return 1; - } - return 0; - }); - const first = eventPaths[0]; - topEventPath = first.path; - if (valueSubscribers.filter(sub => sub.dataPath === topEventPath).every(sub => sub.type === 'mutated' || sub.type.startsWith('notify_'))) { - // Prevent loading of all data on path, so it'll only load changing properties - hasValueSubscribers = false; - } - topEventPath = acebase_core_1.PathInfo.fillVariables(topEventPath, path); // fill in any wildcards in the subscription path + obj = (0, utils_1.cloneObject)(obj); // Make sure we don't alter the original object + const process = (obj, mappings, prefix) => { + if (obj instanceof partial_array_1.PartialArray) { + mappings[prefix] = 'array'; } - const indexes = this.indexes.getAll(path, { childPaths: true, parentPaths: true }) - .map(index => ({ index, keys: acebase_core_1.PathInfo.getPathKeys(index.path) })) - .sort((a, b) => { - if (a.keys.length < b.keys.length) { - return -1; + Object.keys(obj).forEach(key => { + const val = obj[key]; + const path = prefix.length === 0 ? key : `${prefix}/${key}`; + if (typeof val === 'bigint') { + obj[key] = val.toString(); + mappings[path] = 'bigint'; } - else if (a.keys.length > b.keys.length) { - return 1; + else if (val instanceof Date) { + // serialize date to UTC string + obj[key] = val.toISOString(); + mappings[path] = 'date'; } - return 0; - }) - .map(obj => obj.index); - const keysFilter = []; - if (indexes.length > 0) { - indexes.sort((a, b) => { - if (typeof a._pathKeys === 'undefined') { - a._pathKeys = acebase_core_1.PathInfo.getPathKeys(a.path); - } - if (typeof b._pathKeys === 'undefined') { - b._pathKeys = acebase_core_1.PathInfo.getPathKeys(b.path); - } - if (a._pathKeys.length < b._pathKeys.length) { - return -1; - } - else if (a._pathKeys.length > b._pathKeys.length) { - return 1; - } - return 0; - }); - const topIndex = indexes[0]; - const topIndexPath = topIndex.path === path ? path : acebase_core_1.PathInfo.fillVariables(`${topIndex.path}/*`, path); - if (topIndexPath.length < topEventPath.length) { - // index is on a higher path than any value subscriber. - // eg: - // path = 'restaurants/1/rating' - // topEventPath = 'restaurants/1/rating' (because of 'value' event on 'restaurants/*/rating') - // topIndexPath = 'restaurants/1' (because of index on 'restaurants(/*)', key 'name', included key 'rating') - // set topEventPath to topIndexPath, but include only: - // - indexed keys on that path, - // - any additional child keys for all value event subscriptions in that path (they can never be different though?) - topEventPath = topIndexPath; - indexes.filter(index => index.path === topIndex.path).forEach(index => { - const keys = [index.key].concat(index.includeKeys); - keys.forEach(key => !keysFilter.includes(key) && keysFilter.push(key)); - }); + else if (val instanceof ArrayBuffer) { + // Serialize binary data with ascii85 + obj[key] = ascii85_1.ascii85.encode(val); //ascii85.encode(Buffer.from(val)).toString(); + mappings[path] = 'binary'; } - } - return { topEventPath, eventSubscriptions, valueSubscribers, hasValueSubscribers, indexes, keysFilter }; - } - /** - * Wrapper for _writeNode, handles triggering change events, index updating. - * @returns Returns a promise that resolves with an object that contains storage specific details, - * plus the applied mutations if transaction logging is enabled - */ - async _writeNodeWithTracking(path, value, options = { - merge: false, - waitForIndexUpdates: true, - suppress_events: false, - context: null, - impact: null, - }) { - options = options || {}; - if (!options.tid && !options.transaction) { - throw new Error('_writeNodeWithTracking MUST be executed with a tid OR transaction!'); - } - options.merge = options.merge === true; - // Does the value meet schema requirements? - const validation = this.validateSchema(path, value, { updates: options.merge }); - if (!validation.ok) { - throw new errors_js_1.SchemaValidationError(validation.reason); - } - const tid = options.tid; - const transaction = options.transaction; - // Is anyone interested in the values changing on this path? - let topEventData = null; - const updateImpact = options.impact ? options.impact : this.getUpdateImpact(path, options.suppress_events); - const { topEventPath, eventSubscriptions, hasValueSubscribers, indexes } = updateImpact; - let { keysFilter } = updateImpact; - const writeNode = () => { - if (typeof options._customWriteFunction === 'function') { - return options._customWriteFunction(); + else if (val instanceof path_reference_1.PathReference) { + obj[key] = val.path; + mappings[path] = 'reference'; } - if (topEventData) { - // Pass loaded data to _writeNode, speeds up recursive calls - // This prevents reloading and/or overwriting of unchanged child nodes - const pathKeys = acebase_core_1.PathInfo.getPathKeys(path); - const eventPathKeys = acebase_core_1.PathInfo.getPathKeys(topEventPath); - const trailKeys = pathKeys.slice(eventPathKeys.length); - let currentValue = topEventData; - while (trailKeys.length > 0 && currentValue !== null) { - const childKey = trailKeys.shift(); - currentValue = typeof currentValue === 'object' && childKey in currentValue ? currentValue[childKey] : null; - } - options.currentValue = currentValue; + else if (val instanceof RegExp) { + // Queries using the 'matches' filter with a regular expression can now also be used on remote db's + obj[key] = { pattern: val.source, flags: val.flags }; + mappings[path] = 'regexp'; } - return this._writeNode(path, value, options); - }; - const transactionLoggingEnabled = this.settings.transactions && this.settings.transactions.log === true; - if (eventSubscriptions.length === 0 && indexes.length === 0 && !transactionLoggingEnabled) { - // Nobody's interested in value changes. Write node without tracking - return writeNode(); - } - if (!hasValueSubscribers && options.merge === true && keysFilter.length === 0) { - // only load properties being updated - keysFilter = Object.keys(value); - if (topEventPath !== path) { - const trailPath = path.slice(topEventPath.length); - keysFilter = keysFilter.map(key => `${trailPath}/${key}`); + else if (typeof val === 'object' && val !== null) { + process(val, mappings, path); } + }); + }; + const mappings = {}; + process(obj, mappings, ''); + const serialized = { val: obj }; + if (Object.keys(mappings).length > 0) { + serialized.map = mappings; + } + return serialized; +}; +exports.serialize = serialize; +/** + * New serialization method using inline `.type` and `.val` properties + * @param obj + * @returns + */ +const serialize2 = (obj) => { + // Recursively find data that needs serializing + const getSerializedValue = (val) => { + if (typeof val === 'bigint') { + // serialize bigint to string + return { + '.type': 'bigint', + '.val': val.toString(), + }; } - const eventNodeInfo = await this.getNodeInfo(topEventPath, { transaction, tid }); - let currentValue = null; - if (eventNodeInfo.exists) { - const valueOptions = { transaction, tid }; - if (keysFilter.length > 0) { - valueOptions.include = keysFilter; - } - if (topEventPath === '' && typeof valueOptions.include === 'undefined') { - this.logger.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. Use "mutated" or "mutations" events instead'); - } - const node = await this.getNode(topEventPath, valueOptions); - currentValue = node.value; - } - topEventData = currentValue; - // Now proceed with node updating - const result = (await writeNode()) || {}; - // Build data for old/new comparison - let newTopEventData, modifiedData; - if (path === topEventPath) { - if (options.merge) { - if (topEventData === null) { - newTopEventData = value instanceof Array ? [] : {}; - } - else { - // Create shallow copy of previous object value - newTopEventData = topEventData instanceof Array ? [] : {}; - Object.keys(topEventData).forEach(key => { - newTopEventData[key] = topEventData[key]; - }); + else if (val instanceof Date) { + // serialize date to UTC string + return { + '.type': 'date', + '.val': val.toISOString(), + }; + } + else if (val instanceof ArrayBuffer) { + // Serialize binary data with ascii85 + return { + '.type': 'binary', + '.val': ascii85_1.ascii85.encode(val), + }; + } + else if (val instanceof path_reference_1.PathReference) { + return { + '.type': 'reference', + '.val': val.path, + }; + } + else if (val instanceof RegExp) { + // Queries using the 'matches' filter with a regular expression can now also be used on remote db's + return { + '.type': 'regexp', + '.val': `/${val.source}/${val.flags}`, // new: shorter + // '.val': { + // pattern: val.source, + // flags: val.flags + // } + }; + } + else if (typeof val === 'object' && val !== null) { + if (val instanceof Array) { + const copy = []; + for (let i = 0; i < val.length; i++) { + copy[i] = getSerializedValue(val[i]); } + return copy; } else { - newTopEventData = value; + const copy = {}; //val instanceof Array ? [] : {} as SerializedValueV2; + if (val instanceof partial_array_1.PartialArray) { + // Mark the object as partial ("sparse") array + copy['.type'] = 'array'; + } + for (const prop in val) { + copy[prop] = getSerializedValue(val[prop]); + } + return copy; } - modifiedData = newTopEventData; } else { - // topEventPath is on a higher path, so we have to adjust the value deeper down - const trailPath = path.slice(topEventPath.length).replace(/^\//, ''); - const trailKeys = acebase_core_1.PathInfo.getPathKeys(trailPath); - // Create shallow copy of the original object (let unchanged properties reference existing objects) - if (topEventData === null) { - // the node didn't exist prior to the update (or was not loaded) - newTopEventData = typeof trailKeys[0] === 'number' ? [] : {}; + // Primitive value. Don't serialize + return val; + } + }; + const serialized = getSerializedValue(obj); + if (serialized !== null && typeof serialized === 'object' && 'val' in serialized && Object.keys(serialized).length === 1) { + // acebase-core v1.14.1 made the 'map' property optional. + // This v2 serialized object might be confused with a v1 without mappings, because it only has a "val" property + // To prevent this, mark the serialized object with version 2 + serialized['.version'] = 2; + } + return serialized; +}; +exports.serialize2 = serialize2; +/** + * New deserialization method using inline `.type` and `.val` properties + * @param obj + * @returns + */ +const deserialize2 = (data) => { + if (typeof data !== 'object' || data === null) { + // primitive value, not serialized + return data; + } + if (typeof data['.type'] === 'undefined') { + // No type given: this is a plain object or array + if (data instanceof Array) { + // Plain array, deserialize items into a copy + const copy = []; + const arr = data; + for (let i = 0; i < arr.length; i++) { + copy.push((0, exports.deserialize2)(arr[i])); } - else { - newTopEventData = topEventData instanceof Array ? [] : {}; - Object.keys(topEventData).forEach(key => { - newTopEventData[key] = topEventData[key]; - }); + return copy; + } + else { + // Plain object, deserialize properties into a copy + const copy = {}; + const obj = data; + for (const prop in obj) { + copy[prop] = (0, exports.deserialize2)(obj[prop]); } - modifiedData = newTopEventData; - while (trailKeys.length > 0) { - const childKey = trailKeys.shift(); - // Create shallow copy of object at target - if (!options.merge && trailKeys.length === 0) { - modifiedData[childKey] = value; - } - else { - const original = modifiedData[childKey]; - const shallowCopy = typeof childKey === 'number' ? [...original] : Object.assign({}, original); - modifiedData[childKey] = shallowCopy; - } - modifiedData = modifiedData[childKey]; + return copy; + } + } + else if (typeof data['.type'] === 'string') { + const dataType = data['.type'].toLowerCase(); + if (dataType === 'bigint') { + const val = data['.val']; + return BigInt(val); + } + else if (dataType === 'array') { + // partial ("sparse") array, deserialize children into a copy + const arr = data; + const copy = {}; + for (const index in arr) { + copy[index] = (0, exports.deserialize2)(arr[index]); } + delete copy['.type']; + return new partial_array_1.PartialArray(copy); } - if (options.merge) { - // Update target value with updates - Object.keys(value).forEach(key => { - modifiedData[key] = value[key]; - }); + else if (dataType === 'date') { + // Date was serialized as a string (UTC) + const val = data['.val']; + return new Date(val); } - // assert(topEventData !== newTopEventData, 'shallow copy must have been made!'); - const dataChanges = compareValues(topEventData, newTopEventData); - if (dataChanges === 'identical') { - result.mutations = []; - return result; + else if (dataType === 'binary') { + // ascii85 encoded binary data + const val = data['.val']; + return ascii85_1.ascii85.decode(val); } - // Fix: remove null property values (https://github.com/appy-one/acebase/issues/2) - function removeNulls(obj) { - if (obj === null || typeof obj !== 'object') { - return obj; - } // Nothing to do - Object.keys(obj).forEach(prop => { - const val = obj[prop]; - if (val === null) { - delete obj[prop]; - if (obj instanceof Array) { - obj.length--; - } // Array items can only be removed from the end, - } - if (typeof val === 'object') { - removeNulls(val); - } - }); + else if (dataType === 'reference') { + const val = data['.val']; + return new path_reference_1.PathReference(val); } - removeNulls(newTopEventData); - // Trigger all index updates - // TODO: Let indexes subscribe to "mutations" event, saves a lot of work because we are preparing - // before/after copies of the relevant data here, and then the indexes go check what data changed... - const indexUpdates = []; - indexes.map(index => ({ index, keys: acebase_core_1.PathInfo.getPathKeys(index.path) })) - .sort((a, b) => { - // Deepest paths should fire first, then bubble up the tree - if (a.keys.length < b.keys.length) { - return 1; - } - else if (a.keys.length > b.keys.length) { - return -1; + else if (dataType === 'regexp') { + const val = data['.val']; + if (typeof val === 'string') { + // serialized as '/(pattern)/flags' + const match = /^\/(.*)\/([a-z]+)$/.exec(val); + return new RegExp(match[1], match[2]); } - return 0; - }) - .forEach(({ index }) => { - // Index is either on the top event path, or on a child path - // Example situation: - // path = "users/ewout/posts/1" (a post was added) - // topEventPath = "users/ewout" (a "child_changed" event was on "users") - // index.path is "users/*/posts" - // index must be called with data of "users/ewout/posts/1" - const pathKeys = acebase_core_1.PathInfo.getPathKeys(topEventPath); - const indexPathKeys = acebase_core_1.PathInfo.getPathKeys(index.path + '/*'); - const trailKeys = indexPathKeys.slice(pathKeys.length); - // let { oldValue, newValue } = updatedData; - const oldValue = topEventData; - const newValue = newTopEventData; - if (trailKeys.length === 0) { - (0, assert_js_1.assert)(pathKeys.length === indexPathKeys.length, 'check logic'); - // Index is on updated path - const p = this.ipc.isMaster - ? index.handleRecordUpdate(topEventPath, oldValue, newValue) - : this.ipc.sendRequest({ type: 'index.update', fileName: index.fileName, path: topEventPath, oldValue, newValue }); - indexUpdates.push(p); - return; // next index + // serialized as object with pattern & flags properties + return new RegExp(val.pattern, val.flags); + } + } + throw new Error(`Unknown data type "${data['.type']}" in serialized value`); +}; +exports.deserialize2 = deserialize2; + +},{"./ascii85":3,"./partial-array":15,"./path-info":16,"./path-reference":17,"./utils":27}],26:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.TypeMappings = void 0; +const utils_1 = require("./utils"); +const path_info_1 = require("./path-info"); +const data_reference_1 = require("./data-reference"); +const data_snapshot_1 = require("./data-snapshot"); +/** + * (for internal use) - gets the mapping set for a specific path + */ +function get(mappings, path) { + // path points to the mapped (object container) location + path = path.replace(/^\/|\/$/g, ''); // trim slashes + const keys = path_info_1.PathInfo.getPathKeys(path); + const mappedPath = Object.keys(mappings).find(mpath => { + const mkeys = path_info_1.PathInfo.getPathKeys(mpath); + if (mkeys.length !== keys.length) { + return false; // Can't be a match + } + return mkeys.every((mkey, index) => { + if (mkey === '*' || (typeof mkey === 'string' && mkey[0] === '$')) { + return true; // wildcard } - const getAllIndexUpdates = (path, oldValue, newValue) => { - if (oldValue === null && newValue === null) { - return []; + return mkey === keys[index]; + }); + }); + const mapping = mappings[mappedPath]; + return mapping; +} +/** + * (for internal use) - gets the mapping set for a specific path's parent + */ +function map(mappings, path) { + // path points to the object location, its parent should have the mapping + const targetPath = path_info_1.PathInfo.get(path).parentPath; + if (targetPath === null) { + return; + } + return get(mappings, targetPath); +} +/** + * (for internal use) - gets all mappings set for a specific path and all subnodes + * @returns returns array of all matched mappings in path + */ +function mapDeep(mappings, entryPath) { + // returns mapping for this node, and all mappings for nested nodes + // entryPath: "users/ewout" + // mappingPath: "users" + // mappingPath: "users/*/posts" + entryPath = entryPath.replace(/^\/|\/$/g, ''); // trim slashes + // Start with current path's parent node + const pathInfo = path_info_1.PathInfo.get(entryPath); + const startPath = pathInfo.parentPath; + const keys = startPath ? path_info_1.PathInfo.getPathKeys(startPath) : []; + // Every path that starts with startPath, is a match + // TODO: refactor to return Object.keys(mappings),filter(...) + const matches = Object.keys(mappings).reduce((m, mpath) => { + //const mkeys = mpath.length > 0 ? mpath.split("/") : []; + const mkeys = path_info_1.PathInfo.getPathKeys(mpath); + if (mkeys.length < keys.length) { + return m; // Can't be a match + } + let isMatch = true; + if (keys.length === 0 && startPath !== null) { + // Only match first node's children if mapping pattern is "*" or "$variable" + isMatch = mkeys.length === 1 && (mkeys[0] === '*' || (typeof mkeys[0] === 'string' && mkeys[0][0] === '$')); + } + else { + mkeys.every((mkey, index) => { + if (index >= keys.length) { + return false; // stop .every loop } - const pathKeys = acebase_core_1.PathInfo.getPathKeys(path); - const indexPathKeys = acebase_core_1.PathInfo.getPathKeys(index.path + '/*'); - const trailKeys = indexPathKeys.slice(pathKeys.length); - if (trailKeys.length === 0) { - (0, assert_js_1.assert)(pathKeys.length === indexPathKeys.length, 'check logic'); - return [{ path, oldValue, newValue }]; + else if ((mkey === '*' || (typeof mkey === 'string' && mkey[0] === '$')) || mkey === keys[index]) { + return true; // continue .every loop } - let results = []; - let trailPath = ''; - while (trailKeys.length > 0) { - const subKey = trailKeys.shift(); - if (typeof subKey === 'string' && (subKey === '*' || subKey.startsWith('$'))) { - // Recursion needed - const 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 childPath = acebase_core_1.PathInfo.getChildPath(trailPath, key); - const childValues = getChildValues(key, oldValue, newValue); - const subTrailPath = acebase_core_1.PathInfo.getChildPath(path, childPath); - const childResults = getAllIndexUpdates(subTrailPath, childValues.oldValue, childValues.newValue); - results = results.concat(childResults); - }); - break; - } - else { - const values = getChildValues(subKey, oldValue, newValue); - oldValue = values.oldValue; - newValue = values.newValue; - if (oldValue === null && newValue === null) { - break; - } - trailPath = acebase_core_1.PathInfo.getChildPath(trailPath, subKey); - } + else { + isMatch = false; + return false; // stop .every loop } - return results; - }; - const results = getAllIndexUpdates(topEventPath, oldValue, newValue); - results.forEach(result => { - const p = this.ipc.isMaster - ? index.handleRecordUpdate(result.path, result.oldValue, result.newValue) - : this.ipc.sendRequest({ type: 'index.update', fileName: index.fileName, path: result.path, oldValue: result.oldValue, newValue: 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 === 'mutated') { - return; // Ignore here, requires different logic - } - else if (type === 'child_changed' && (oldValue === null || newValue === null)) { - trigger = false; + } + if (isMatch) { + const mapping = mappings[mpath]; + m.push({ path: mpath, type: mapping }); + } + return m; + }, []); + return matches; +} +/** + * (for internal use) - serializes or deserializes an object using type mappings + * @returns returns the (de)serialized value + */ +function process(db, mappings, path, obj, action) { + if (obj === null || typeof obj !== 'object') { + return obj; + } + const keys = path_info_1.PathInfo.getPathKeys(path); // path.length > 0 ? path.split("/") : []; + const m = mapDeep(mappings, path); + const changes = []; + m.sort((a, b) => path_info_1.PathInfo.getPathKeys(a.path).length > path_info_1.PathInfo.getPathKeys(b.path).length ? -1 : 1); // Deepest paths first + m.forEach(mapping => { + const mkeys = path_info_1.PathInfo.getPathKeys(mapping.path); //mapping.path.length > 0 ? mapping.path.split("/") : []; + mkeys.push('*'); + const mTrailKeys = mkeys.slice(keys.length); + if (mTrailKeys.length === 0) { + const vars = path_info_1.PathInfo.extractVariables(mapping.path, path); + const ref = new data_reference_1.DataReference(db, path, vars); + if (action === 'serialize') { + // serialize this object + obj = mapping.type.serialize(obj, ref); } - else if (type === 'value' || type === 'child_changed') { - const changes = compareValues(oldValue, newValue); - trigger = changes !== 'identical'; + else if (action === 'deserialize') { + // deserialize this object + const snap = new data_snapshot_1.DataSnapshot(ref, obj); + obj = mapping.type.deserialize(snap); } - else if (type === 'child_added') { - trigger = oldValue === null && newValue !== null; + return; + } + // Find all nested objects at this trail path + const process = (parentPath, parent, keys) => { + if (obj === null || typeof obj !== 'object') { + return obj; } - else if (type === 'child_removed') { - trigger = oldValue !== null && newValue === null; + const key = keys[0]; + let children = []; + if (key === '*' || (typeof key === 'string' && key[0] === '$')) { + // Include all children + if (parent instanceof Array) { + children = parent.map((val, index) => ({ key: index, val })); + } + else { + children = Object.keys(parent).map(k => ({ key: k, val: parent[k] })); + } } - if (!trigger) { - return; + else { + // Get the 1 child + const child = parent[key]; + if (typeof child === 'object') { + children.push({ key, val: child }); + } } - const pathKeys = acebase_core_1.PathInfo.getPathKeys(sub.dataPath); - variables.forEach(variable => { - // only replaces first occurrence (so multiple *'s will be processed 1 by 1) - const index = pathKeys.indexOf(variable.name); - (0, assert_js_1.assert)(index >= 0, `Variable "${variable.name}" not found in subscription dataPath "${sub.dataPath}"`); - pathKeys[index] = variable.value; + children.forEach(child => { + const childPath = path_info_1.PathInfo.getChildPath(parentPath, child.key); + const vars = path_info_1.PathInfo.extractVariables(mapping.path, childPath); + const ref = new data_reference_1.DataReference(db, childPath, vars); + if (keys.length === 1) { + // TODO: this alters the existing object, we must build our own copy! + if (action === 'serialize') { + // serialize this object + changes.push({ parent, key: child.key, original: parent[child.key] }); + parent[child.key] = mapping.type.serialize(child.val, ref); + } + else if (action === 'deserialize') { + // deserialize this object + const snap = new data_snapshot_1.DataSnapshot(ref, child.val); + parent[child.key] = mapping.type.deserialize(snap); + } + } + else { + // Dig deeper + process(childPath, child.val, keys.slice(1)); + } }); - const dataPath = pathKeys.reduce((path, key) => acebase_core_1.PathInfo.getChildPath(path, key), ''); - this.subscriptions.trigger(sub.type, sub.subscriptionPath, dataPath, oldValue, newValue, options.context); }; - const prepareMutationEvents = (currentPath, oldValue, newValue, compareResult) => { - const batch = []; - const result = compareResult || compareValues(oldValue, newValue); - if (result === 'identical') { - return batch; // no changes on subscribed path - } - else if (typeof result === 'string') { - // We are on a path that has an actual change - batch.push({ path: currentPath, oldValue, newValue }); - } - // else if (oldValue instanceof Array || newValue instanceof Array) { - // // Trigger mutated event on the array itself instead of on individual indexes. - // // DO convert both arrays to objects because they are sparse - // const oldObj = {}, newObj = {}; - // result.added.forEach(index => { - // oldObj[index] = null; - // newObj[index] = newValue[index]; - // }); - // result.removed.forEach(index => { - // oldObj[index] = oldValue[index]; - // newObj[index] = null; - // }); - // result.changed.forEach(index => { - // oldObj[index] = oldValue[index]; - // newObj[index] = newValue[index]; - // }); - // batch.push({ path: currentPath, oldValue: oldObj, newValue: newObj }); + process(path, obj, mTrailKeys); + }); + if (action === 'serialize') { + // Clone this serialized object so any types that remained + // will become plain objects without functions, and we can restore + // the original object's values if any mappings were processed. + // This will also prevent circular references + obj = (0, utils_1.cloneObject)(obj); + if (changes.length > 0) { + // Restore the changes made to the original object + changes.forEach(change => { + change.parent[change.key] = change.original; + }); + } + } + return obj; +} +const _mappings = Symbol('mappings'); +class TypeMappings { + constructor(db) { + this.db = db; + this[_mappings] = {}; + } + /** (for internal use) */ + get mappings() { return this[_mappings]; } + /** (for internal use) */ + map(path) { + return map(this[_mappings], path); + } + /** + * Maps objects that are stored in a specific path to a class, so they can automatically be + * serialized when stored to, and deserialized (instantiated) when loaded from the database. + * @param path path to an object container, eg "users" or "users/*\/posts" + * @param type class to bind all child objects of path to + * Best practice is to implement 2 methods for instantiation and serializing of your objects: + * 1) `static create(snap: DataSnapshot)` and 2) `serialize(ref: DataReference)`. See example + * @param options (optional) You can specify the functions to use to + * serialize and/or instantiate your class. If you do not specificy a creator (constructor) method, + * AceBase will call `YourClass.create(snapshot)` method if it exists, or create an instance of + * YourClass with `new YourClass(snapshot)`. + * If you do not specifiy a serializer method, AceBase will call `YourClass.prototype.serialize(ref)` + * if it exists, or tries storing your object's fields unaltered. NOTE: `this` in your creator + * function will point to `YourClass`, and `this` in your serializer function will point to the + * `instance` of `YourClass`. + * @example + * class User { + * static create(snap: DataSnapshot): User { + * // Deserialize (instantiate) User from plain database object + * let user = new User(); + * Object.assign(user, snap.val()); // Copy all properties to user + * user.id = snap.ref.key; // Add the key as id + * return user; + * } + * serialize(ref: DataReference) { + * // Serialize user for database storage + * return { + * name: this.name + * email: this.email + * }; + * } + * } + * db.types.bind('users', User); // Automatically uses serialize and static create methods + */ + bind(path, type, options = {}) { + // Maps objects that are stored in a specific path to a constructor method, + // so they are automatically deserialized + 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') { + // if (typeof type.prototype.serialize === 'function') { + // // Use .serialize instance method + // options.serializer = type.prototype.serialize; // } + // Use object's serialize method upon serialization (if available) + } + else if (typeof options.serializer === 'string') { + if (typeof type.prototype[options.serializer] === 'function') { + options.serializer = type.prototype[options.serializer]; + } else { - // DISABLED array handling here, because if a client is using a cache db this will cause problems - // because individual array entries should never be modified. - // if (oldValue instanceof Array && newValue instanceof Array) { - // // Make sure any removed events on arrays will be triggered from last to first - // result.removed.sort((a,b) => a < b ? 1 : -1); - // } - result.changed.forEach(info => { - const childPath = acebase_core_1.PathInfo.getChildPath(currentPath, info.key); - const childValues = getChildValues(info.key, oldValue, newValue); - const childBatch = prepareMutationEvents(childPath, childValues.oldValue, childValues.newValue, info.change); - batch.push(...childBatch); - }); - result.added.forEach(key => { - const childPath = acebase_core_1.PathInfo.getChildPath(currentPath, key); - batch.push({ path: childPath, oldValue: null, newValue: newValue[key] }); - }); - if (oldValue instanceof Array && newValue instanceof Array) { - result.removed.sort((a, b) => a < b ? 1 : -1); - } - result.removed.forEach(key => { - const childPath = acebase_core_1.PathInfo.getChildPath(currentPath, key); - batch.push({ path: childPath, oldValue: oldValue[key], newValue: null }); - }); + throw new TypeError(`${type.name}.prototype.${options.serializer} is not a function, cannot use it as serializer`); } - return batch; - }; - // Add mutations to result (only if transaction logging is enabled) - if (transactionLoggingEnabled && this.settings.type !== 'transaction') { - result.mutations = (() => { - const trailPath = path.slice(topEventPath.length).replace(/^\//, ''); - const trailKeys = acebase_core_1.PathInfo.getPathKeys(trailPath); - let oldValue = topEventData, newValue = newTopEventData; - while (trailKeys.length > 0) { - const key = trailKeys.shift(); - ({ oldValue, newValue } = getChildValues(key, oldValue, newValue)); - } - const compareResults = compareValues(oldValue, newValue); - const batch = prepareMutationEvents(path, oldValue, newValue, compareResults); - const mutations = batch.map(m => ({ target: acebase_core_1.PathInfo.getPathKeys(m.path.slice(path.length)), prev: m.oldValue, val: m.newValue })); // key: PathInfo.get(m.path).key - return mutations; - })(); } - const triggerAllEvents = () => { - // Notify all event subscriptions, should be executed with a delay - // this.logger.debug(`Triggering events caused by ${options && options.merge ? '(merge) ' : ''}write on "${path}":`, value); - eventSubscriptions - .filter(sub => !['mutated', 'mutations', 'notify_mutated', 'notify_mutations'].includes(sub.type)) - .map(sub => { - const keys = acebase_core_1.PathInfo.getPathKeys(sub.dataPath); - return { - sub, - keys, - }; - }) - .sort((a, b) => { - // Deepest paths should fire first, then bubble up the tree - if (a.keys.length < b.keys.length) { - return 1; + 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') { + // Use static .create as creator method + 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, ''); // trim slashes + this[_mappings][path] = { + db: this.db, + type, + creator: options.creator, + serializer: options.serializer, + deserialize(snap) { + // run constructor method + let obj; + if (this.creator) { + obj = this.creator.call(this.type, snap); } - else if (a.keys.length > b.keys.length) { - return -1; + else { + obj = new this.type(snap); } - return 0; - }) - .forEach(({ sub }) => { - const process = (currentPath, oldValue, newValue, variables = []) => { - const trailPath = sub.dataPath.slice(currentPath.length).replace(/^\//, ''); - const trailKeys = acebase_core_1.PathInfo.getPathKeys(trailPath); - while (trailKeys.length > 0) { - const subKey = trailKeys.shift(); - if (typeof subKey === 'string' && (subKey === '*' || subKey[0] === '$')) { - // Fire on all relevant child keys - const allKeys = oldValue === null ? [] : Object.keys(oldValue).map(key => oldValue instanceof Array ? parseInt(key) : key); - newValue !== null && Object.keys(newValue).forEach(key => { - const keyOrIndex = newValue instanceof Array ? parseInt(key) : key; - !allKeys.includes(keyOrIndex) && 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(acebase_core_1.PathInfo.getChildPath(currentPath, subKey), childValues.oldValue, childValues.newValue, vars); - } - }); - return; // We can stop processing - } - else { - currentPath = acebase_core_1.PathInfo.getChildPath(currentPath, subKey); - const childValues = getChildValues(subKey, oldValue, newValue); - oldValue = childValues.oldValue; - newValue = childValues.newValue; - } - } - callSubscriberWithValues(sub, oldValue, newValue, variables); - }; - if (sub.type.startsWith('notify_') && acebase_core_1.PathInfo.get(sub.eventPath).isAncestorOf(topEventPath)) { - // Notify event on a higher path than we have loaded data on - // We can trigger the notify event on the subscribed path - // Eg: - // path === 'users/ewout', updates === { name: 'Ewout Stortenbeker' } - // sub.path === 'users' or '', sub.type === 'notify_child_changed' - // => OK to trigger if dataChanges !== 'removed' and 'added' - const isOnParentPath = acebase_core_1.PathInfo.get(sub.eventPath).isParentOf(topEventPath); - const trigger = (sub.type === 'notify_value') - || (sub.type === 'notify_child_changed' && (!isOnParentPath || !['added', 'removed'].includes(dataChanges))) - || (sub.type === 'notify_child_removed' && dataChanges === 'removed' && isOnParentPath) - || (sub.type === 'notify_child_added' && dataChanges === 'added' && isOnParentPath); - trigger && this.subscriptions.trigger(sub.type, sub.subscriptionPath, sub.dataPath, null, null, options.context); + return obj; + }, + serialize(obj, ref) { + if (this.serializer) { + obj = this.serializer.call(obj, ref, obj); } - else { - // Subscription is on current or deeper path - process(topEventPath, topEventData, newTopEventData); + else if (obj && typeof obj.serialize === 'function') { + obj = obj.serialize(ref, obj); } - }); - // The only events we haven't processed now are 'mutated' events. - // They require different logic: we'll call them for all nested properties of the updated path, that - // actually did change. They do not bubble up like 'child_changed' does. - const mutationEvents = eventSubscriptions.filter(sub => ['mutated', 'mutations', 'notify_mutated', 'notify_mutations'].includes(sub.type)); - mutationEvents.forEach(sub => { - // Get the target data this subscription is interested in - const currentPath = topEventPath; - // const trailPath = sub.eventPath.slice(currentPath.length).replace(/^\//, ''); // eventPath can contain vars and * ? - const trailKeys = acebase_core_1.PathInfo.getPathKeys(sub.eventPath).slice(acebase_core_1.PathInfo.getPathKeys(currentPath).length); //PathInfo.getPathKeys(trailPath); - const events = []; - const oldValue = topEventData; - const newValue = newTopEventData; - const processNextTrailKey = (target, currentTarget, oldValue, newValue, vars) => { - if (target.length === 0) { - // Add it - return events.push({ target: currentTarget, oldValue, newValue, vars }); - } - const subKey = target[0]; - const keys = new Set(); - const isWildcardKey = typeof subKey === 'string' && (subKey === '*' || subKey.startsWith('$')); - if (isWildcardKey) { - // Recursive for each key in oldValue and newValue - if (oldValue !== null && typeof oldValue === 'object') { - Object.keys(oldValue).forEach(key => keys.add(key)); - } - if (newValue !== null && typeof newValue === 'object') { - Object.keys(newValue).forEach(key => keys.add(key)); - } - } - else { - keys.add(subKey); // just one specific key - } - for (const key of keys) { - const childValues = getChildValues(key, oldValue, newValue); - oldValue = childValues.oldValue; - newValue = childValues.newValue; - processNextTrailKey(target.slice(1), currentTarget.concat(key), oldValue, newValue, isWildcardKey ? vars.concat({ name: subKey, value: key }) : vars); - } - }; - processNextTrailKey(trailKeys, [], oldValue, newValue, []); - for (const event of events) { - const targetPath = acebase_core_1.PathInfo.get(currentPath).child(event.target).path; - const batch = prepareMutationEvents(targetPath, event.oldValue, event.newValue); - if (batch.length === 0) { - continue; - } - const isNotifyEvent = sub.type.startsWith('notify_'); - if (['mutated', 'notify_mutated'].includes(sub.type)) { - // Send all mutations 1 by 1 - batch.forEach((mutation, index) => { - const context = options.context; // const context = cloneObject(options.context); - // context.acebase_mutated_event = { nr: index + 1, total: batch.length }; // Add context info about number of mutations - const prevVal = isNotifyEvent ? null : mutation.oldValue; - const newVal = isNotifyEvent ? null : mutation.newValue; - this.subscriptions.trigger(sub.type, sub.subscriptionPath, mutation.path, prevVal, newVal, context); - }); - } - else if (['mutations', 'notify_mutations'].includes(sub.type)) { - // Send 1 batch with all mutations - // const oldValues = isNotifyEvent ? null : batch.map(m => ({ target: PathInfo.getPathKeys(mutation.path.slice(sub.subscriptionPath.length)), val: m.oldValue })); // batch.reduce((obj, mutation) => (obj[mutation.path.slice(sub.subscriptionPath.length).replace(/^\//, '') || '.'] = mutation.oldValue, obj), {}); - // const newValues = isNotifyEvent ? null : batch.map(m => ({ target: PathInfo.getPathKeys(mutation.path.slice(sub.subscriptionPath.length)), val: m.newValue })) //batch.reduce((obj, mutation) => (obj[mutation.path.slice(sub.subscriptionPath.length).replace(/^\//, '') || '.'] = mutation.newValue, obj), {}); - const subscriptionPathKeys = acebase_core_1.PathInfo.getPathKeys(sub.subscriptionPath); - const values = isNotifyEvent ? null : batch.map(m => ({ target: acebase_core_1.PathInfo.getPathKeys(m.path).slice(subscriptionPathKeys.length), prev: m.oldValue, val: m.newValue })); - const dataPath = acebase_core_1.PathInfo.get(acebase_core_1.PathInfo.getPathKeys(targetPath).slice(0, subscriptionPathKeys.length)).path; - this.subscriptions.trigger(sub.type, sub.subscriptionPath, dataPath, null, values, options.context); - } - } - }); + return obj; + }, }; - // Wait for all index updates to complete - if (options.waitForIndexUpdates === false) { - indexUpdates.splice(0); // Remove all index update promises, so we don't wait for them to resolve - } - await Promise.all(indexUpdates); - defer(triggerAllEvents); // Delayed execution - return result; } /** - * Enumerates all children of a given Node for reflection purposes - * @param path - * @param options optional options used by implementation for recursive calls - * @returns returns a generator object that calls .next for each child until the .next callback returns false + * @internal (for internal use) + * Serializes any child in given object that has a type mapping + * @param path | path to the object's location + * @param obj object to serialize */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - getChildren(path, options) { - throw new Error('This method must be implemented by subclass'); + serialize(path, obj) { + return process(this.db, this[_mappings], path, obj, 'serialize'); } /** - * @deprecated Use `getNode` instead - * Gets a node's value by delegating to getNode, returning only the value - * @param path - * @param options optional options that can limit the amount of (sub)data being loaded, and any other implementation specific options for recusrsive calls + * @internal (for internal use) + * Deserialzes any child in given object that has a type mapping + * @param path path to the object's location + * @param obj object to deserialize */ - async getNodeValue(path, options = {}) { - const node = await this.getNode(path, options); - return node.value; + deserialize(path, obj) { + return process(this.db, this[_mappings], path, obj, 'deserialize'); } - /** - * Gets a node's value and (if supported) revision - * @param path - * @param options optional options that can limit the amount of (sub)data being loaded, and any other implementation specific options for recusrsive calls - */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - getNode(path, options) { - throw new Error('This method must be implemented by subclass'); +} +exports.TypeMappings = TypeMappings; + +},{"./data-reference":8,"./data-snapshot":9,"./path-info":16,"./utils":27}],27:[function(require,module,exports){ +(function (global,Buffer){(function (){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getGlobalObject = exports.defer = exports.getChildValues = exports.getMutations = exports.compareValues = exports.ObjectDifferences = exports.valuesAreEqual = exports.cloneObject = exports.concatTypedArrays = exports.decodeString = exports.encodeString = exports.bytesToBigint = exports.bigintToBytes = exports.bytesToNumber = exports.numberToBytes = void 0; +const path_reference_1 = require("./path-reference"); +const process_1 = require("./process"); +const partial_array_1 = require("./partial-array"); +function numberToBytes(number) { + const bytes = new Uint8Array(8); + const view = new DataView(bytes.buffer); + view.setFloat64(0, number); + return new Array(...bytes); +} +exports.numberToBytes = numberToBytes; +function bytesToNumber(bytes) { + const length = Array.isArray(bytes) ? bytes.length : bytes.byteLength; + if (length !== 8) { + throw new TypeError('must be 8 bytes'); } - /** - * Retrieves info about a node (existence, wherabouts etc) - * @param {string} path - * @param {object} [options] optional options used by implementation for recursive calls - */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - getNodeInfo(path, options) { - throw new Error('This method must be implemented by subclass'); + const bin = new Uint8Array(bytes); + const view = new DataView(bin.buffer); + const nr = view.getFloat64(0); + return nr; +} +exports.bytesToNumber = bytesToNumber; +const hasBigIntSupport = (() => { + try { + return typeof BigInt(0) === 'bigint'; } - /** - * Creates or overwrites a node. Delegates to updateNode on a parent if - * path is not the root. - * @param path - * @param value - * @param options optional options used by implementation for recursive calls - * @returns Returns a new cursor if transaction logging is enabled - */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - setNode(path, value, options) { - throw new Error('This method must be implemented by subclass'); + catch (err) { + return false; } - /** - * Updates a node by merging an existing node with passed updates object, - * or creates it by delegating to updateNode on the parent path. - * @param path - * @param updates object with key/value pairs - * @returns Returns a new cursor if transaction logging is enabled - */ +})(); +const noBigIntError = 'BigInt is not supported on this platform'; +const bigIntFunctions = { // eslint-disable-next-line @typescript-eslint/no-unused-vars - updateNode(path, updates, options) { - throw new Error('This method must be implemented by subclass'); - } - /** - * Updates a node by getting its value, running a callback function that transforms - * the current value and returns the new value to be stored. Assures the read value - * does not change while the callback runs, or runs the callback again if it did. - * @param path - * @param callback function that transforms current value and returns the new value to be stored. Can return a Promise - * @param options optional options used by implementation for recursive calls - * @returns Returns a new cursor if transaction logging is enabled - */ - async transactNode(path, callback, options = { no_lock: false, suppress_events: false, context: null }) { - const useFakeLock = options && options.no_lock === true; - const tid = this.createTid(); - const lock = useFakeLock - ? { tid, release: NOOP } // Fake lock, we'll use revision checking & retrying instead - : await this.nodeLocker.lock(path, tid, true, 'transactNode'); - try { - let changed = false; - const changeCallback = () => { changed = true; }; - if (useFakeLock) { - // Monitor value changes - this.subscriptions.add(path, 'notify_value', changeCallback); - } - const node = await this.getNode(path, { tid }); - const checkRevision = node.revision; - let newValue; - try { - newValue = callback(node.value); - if (newValue instanceof Promise) { - newValue = await newValue.catch(err => { - this.logger.error(`Error in transaction callback: ${err.message}`); - }); - } - } - catch (err) { - this.logger.error(`Error in transaction callback: ${err.message}`); - } - if (typeof newValue === 'undefined') { - // Callback did not return value. Cancel transaction - return; - } - // asserting revision is only needed when no_lock option was specified - if (useFakeLock) { - this.subscriptions.remove(path, 'notify_value', changeCallback); - } - if (changed) { - throw new node_errors_js_1.NodeRevisionError('Node changed'); - } - const cursor = await this.setNode(path, newValue, { assert_revision: checkRevision, tid: lock.tid, suppress_events: options.suppress_events, context: options.context }); - return cursor; - } - catch (err) { - if (err instanceof node_errors_js_1.NodeRevisionError) { - // try again - console.warn(`node value changed, running again. Error: ${err.message}`); - return this.transactNode(path, callback, options); - } - else { - throw err; - } + bigintToBytes(number) { throw new Error(noBigIntError); }, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + bytesToBigint(bytes) { throw new Error(noBigIntError); }, +}; +if (hasBigIntSupport) { + const big = { + zero: BigInt(0), + one: BigInt(1), + two: BigInt(2), + eight: BigInt(8), + ff: BigInt(0xff), + }; + bigIntFunctions.bigintToBytes = function bigintToBytes(number) { + if (typeof number !== 'bigint') { + throw new Error('number must be a bigint'); } - finally { - lock.release(); + const bytes = []; + const negative = number < big.zero; + do { + const byte = Number(number & big.ff); // NOTE: bits are inverted on negative numbers + bytes.push(byte); + number = number >> big.eight; + } while (number !== (negative ? -big.one : big.zero)); + bytes.reverse(); // little-endian + if (negative ? bytes[0] < 128 : bytes[0] >= 128) { + bytes.unshift(negative ? 255 : 0); // extra sign byte needed } - } - /** - * Checks if a node's value matches the passed criteria - * @param path - * @param criteria criteria to test - * @param options optional options used by implementation for recursive calls - * @returns returns a promise that resolves with a boolean indicating if it matched the criteria - */ - async matchNode(path, criteria, options) { - var _a; - const tid = (_a = options === null || options === void 0 ? void 0 : options.tid) !== null && _a !== void 0 ? _a : acebase_core_1.ID.generate(); - const checkNode = async (path, criteria) => { - if (criteria.length === 0) { - return Promise.resolve(true); // No criteria, so yes... It matches! - } - const criteriaKeys = criteria.reduce((keys, cr) => { - let key = cr.key; - if (typeof key === 'string' && key.includes('/')) { - // Descendant key criterium, use child key only (eg 'address' of 'address/city') - key = key.slice(0, key.indexOf('/')); - } - if (keys.indexOf(key) < 0) { - keys.push(key); - } - return keys; - }, []); - const unseenKeys = criteriaKeys.slice(); - let isMatch = true; - const delayedMatchPromises = []; - try { - await this.getChildren(path, { tid, keyFilter: criteriaKeys }).next(childInfo => { - var _a; - const keyOrIndex = (_a = childInfo.key) !== null && _a !== void 0 ? _a : childInfo.index; - unseenKeys.includes(keyOrIndex) && unseenKeys.splice(unseenKeys.indexOf(childInfo.key), 1); - const keyCriteria = criteria - .filter(cr => cr.key === keyOrIndex) - .map(cr => ({ op: cr.op, compare: cr.compare })); - const keyResult = keyCriteria.length > 0 ? checkChild(childInfo, keyCriteria) : { isMatch: true, promises: [] }; - isMatch = keyResult.isMatch; - if (isMatch) { - delayedMatchPromises.push(...keyResult.promises); - const childCriteria = criteria - .filter(cr => typeof cr.key === 'string' && cr.key.startsWith(`${typeof keyOrIndex === 'number' ? `[${keyOrIndex}]` : keyOrIndex}/`)) - .map(cr => { - const key = cr.key.slice(cr.key.indexOf('/') + 1); - return { key, op: cr.op, compare: cr.compare }; - }); - if (childCriteria.length > 0) { - const childPath = acebase_core_1.PathInfo.getChildPath(path, childInfo.key); - const childPromise = checkNode(childPath, childCriteria) - .then(isMatch => ({ isMatch })); - delayedMatchPromises.push(childPromise); - } - } - if (!isMatch || unseenKeys.length === 0) { - return false; // Stop iterating - } - }); - if (isMatch) { - const results = await Promise.all(delayedMatchPromises); - isMatch = results.every(res => res.isMatch); - } - if (!isMatch) { - return false; - } - // Now, also check keys that weren't found in the node. (a criterium may be "!exists") - isMatch = unseenKeys.every(keyOrIndex => { - const childInfo = new node_info_js_1.NodeInfo(Object.assign(Object.assign(Object.assign({}, (typeof keyOrIndex === 'number' && { index: keyOrIndex })), (typeof keyOrIndex === 'string' && { key: keyOrIndex })), { exists: false })); - const childCriteria = criteria - .filter(cr => typeof cr.key === 'string' && cr.key.startsWith(`${typeof keyOrIndex === 'number' ? `[${keyOrIndex}]` : keyOrIndex}/`)) - .map(cr => ({ op: cr.op, compare: cr.compare })); - if (childCriteria.length > 0 && !checkChild(childInfo, childCriteria).isMatch) { - return false; - } - const keyCriteria = criteria - .filter(cr => cr.key === keyOrIndex) - .map(cr => ({ op: cr.op, compare: cr.compare })); - if (keyCriteria.length === 0) { - return true; // There were only child criteria, and they matched (otherwise we wouldn't be here) + return bytes; + }; + bigIntFunctions.bytesToBigint = function bytesToBigint(bytes) { + const negative = bytes[0] >= 128; + let number = big.zero; + for (let b of bytes) { + if (negative) { + b = ~b & 0xff; + } // Invert the bits + number = (number << big.eight) + BigInt(b); + } + if (negative) { + number = -(number + big.one); + } + return number; + }; +} +exports.bigintToBytes = bigIntFunctions.bigintToBytes; +exports.bytesToBigint = bigIntFunctions.bytesToBigint; +/** + * Converts a string to a utf-8 encoded Uint8Array + */ +function encodeString(str) { + if (typeof TextEncoder !== 'undefined') { + // Modern browsers, Node.js v11.0.0+ (or v8.3.0+ with util.TextEncoder) + const encoder = new TextEncoder(); + return encoder.encode(str); + } + else if (typeof Buffer === 'function') { + // Node.js + const buf = Buffer.from(str, 'utf-8'); + return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength); + } + else { + // Older browsers. Manually encode + const arr = []; + for (let i = 0; i < str.length; i++) { + let code = str.charCodeAt(i); + if (code > 128) { + // Attempt simple UTF-8 conversion. See https://en.wikipedia.org/wiki/UTF-8 + if ((code & 0xd800) === 0xd800) { + // code starts with 1101 10...: this is a 2-part utf-16 char code + const nextCode = str.charCodeAt(i + 1); + if ((nextCode & 0xdc00) !== 0xdc00) { + // next code must start with 1101 11... + throw new Error('follow-up utf-16 character does not start with 0xDC00'); } - const result = checkChild(childInfo, keyCriteria); - return result.isMatch; - }); - return isMatch; - } - catch (err) { - this.logger.error(`Error matching on "${path}": `, err); - throw err; - } - }; // checkNode - /** - * - * @param child - * @param criteria criteria to test - */ - 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; + i++; + const p1 = code & 0x3ff; // Only use last 10 bits + const p2 = nextCode & 0x3ff; + // Create code point from these 2: (see https://en.wikipedia.org/wiki/UTF-16) + code = 0x10000 | (p1 << 10) | p2; } - else if (f.op === 'exists' || (f.op === '!=' && (typeof f.compare === 'undefined' || f.compare === null))) { - proceed = child.exists; + if (code < 2048) { + // Use 2 bytes for 11 bit value, first byte starts with 110xxxxx (0xc0), 2nd byte with 10xxxxxx (0x80) + const b1 = 0xc0 | ((code >> 6) & 0x1f); // 0xc0 = 11000000, 0x1f = 11111 + const b2 = 0x80 | (code & 0x3f); // 0x80 = 10000000, 0x3f = 111111 + arr.push(b1, b2); } - else if ((f.op === 'contains' || f.op === '!contains') && f.compare instanceof Array && f.compare.length === 0) { - // Added for #135: empty compare array for contains/!contains must match all values - proceed = true; + else if (code < 65536) { + // Use 3 bytes for 16-bit value, bits per byte: 4, 6, 6 + const b1 = 0xe0 | ((code >> 12) & 0xf); // 0xe0 = 11100000, 0xf = 1111 + const b2 = 0x80 | ((code >> 6) & 0x3f); // 0x80 = 10000000, 0x3f = 111111 + const b3 = 0x80 | (code & 0x3f); + arr.push(b1, b2, b3); } - else if (!child.exists) { - proceed = false; + else if (code < 2097152) { + // Use 4 bytes for 21-bit value, bits per byte: 3, 6, 6, 6 + const b1 = 0xf0 | ((code >> 18) & 0x7); // 0xf0 = 11110000, 0x7 = 111 + const b2 = 0x80 | ((code >> 12) & 0x3f); // 0x80 = 10000000, 0x3f = 111111 + const b3 = 0x80 | ((code >> 6) & 0x3f); // 0x80 = 10000000, 0x3f = 111111 + const b4 = 0x80 | (code & 0x3f); + arr.push(b1, b2, b3, b4); } else { - if (child.address) { - if (child.valueType === node_value_types_js_1.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 }]) - .then(isMatch => { - return { key: child.key, isMatch }; - }); - promises.push(p); - proceed = true; - } - else if (child.valueType === node_value_types_js_1.VALUE_TYPES.ARRAY && ['contains', '!contains'].indexOf(f.op) >= 0) { - // TODO: refactor to use child stream - const p = this.getNode(child.path, { tid }) - .then(({ value: arr }) => { - // const i = arr.indexOf(f.compare); - // return { key: child.key, isMatch: (i >= 0 && f.op === "contains") || (i < 0 && f.op === "!contains") }; - const isMatch = f.op === 'contains' - // "contains" - ? f.compare instanceof Array - ? f.compare.every(val => arr.includes(val)) // Match if ALL of the passed values are in the array - : arr.includes(f.compare) - // "!contains" - : f.compare instanceof Array - ? !f.compare.some(val => arr.includes(val)) // DON'T match if ANY of the passed values is in the array - : !arr.includes(f.compare); - return { key: child.key, isMatch }; - }); - promises.push(p); - proceed = true; - } - else if (child.valueType === node_value_types_js_1.VALUE_TYPES.STRING) { - const p = this.getNode(child.path, { tid }) - .then(node => { - return { key: child.key, isMatch: this.test(node.value, f.op, f.compare) }; - }); - promises.push(p); - proceed = true; - } - else { - proceed = false; - } - } - else if (child.type === node_value_types_js_1.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 === node_value_types_js_1.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 { - let ret = this.test(child.value, f.op, f.compare); - if (ret instanceof Promise) { - promises.push(ret); - ret = true; - } - proceed = ret; - } + throw new Error(`Cannot convert character ${str.charAt(i)} (code ${code}) to utf-8`); } - return proceed; - }); // fs.every - return { isMatch, promises }; - }; // checkChild - return checkNode(path, criteria); - } - test(val, op, compare) { - if (op === '<') { - return val < compare; + } + else { + arr.push(code < 128 ? code : 63); // 63 = ? + } } - if (op === '<=') { - return val <= compare; + return new Uint8Array(arr); + } +} +exports.encodeString = encodeString; +/** + * Converts a utf-8 encoded buffer to string + */ +function decodeString(buffer) { + if (typeof TextDecoder !== 'undefined') { + // Modern browsers, Node.js v11.0.0+ (or v8.3.0+ with util.TextDecoder) + const decoder = new TextDecoder(); + if (buffer instanceof Uint8Array) { + return decoder.decode(buffer); } - if (op === '==') { - return val === compare; - } - if (op === '!=') { - return val !== compare; + const buf = Uint8Array.from(buffer); + return decoder.decode(buf); + } + else if (typeof Buffer === 'function') { + // Node.js (v10 and below) + if (buffer instanceof Array) { + buffer = Uint8Array.from(buffer); // convert to typed array } - if (op === '>') { - return val > compare; + if (!(buffer instanceof Buffer) && 'buffer' in buffer && buffer.buffer instanceof ArrayBuffer) { + const typedArray = buffer; + buffer = Buffer.from(typedArray.buffer, typedArray.byteOffset, typedArray.byteLength); // Convert typed array to node.js Buffer } - if (op === '>=') { - return val >= compare; + if (!(buffer instanceof Buffer)) { + throw new Error('Unsupported buffer argument'); } - if (op === 'in') { - return compare.indexOf(val) >= 0; + return buffer.toString('utf-8'); + } + else { + // Older browsers. Manually decode! + if (!(buffer instanceof Uint8Array) && 'buffer' in buffer && buffer['buffer'] instanceof ArrayBuffer) { + // Convert TypedArray to Uint8Array + const typedArray = buffer; + buffer = new Uint8Array(typedArray.buffer, typedArray.byteOffset, typedArray.byteLength); } - if (op === '!in') { - return compare.indexOf(val) < 0; + if (buffer instanceof Buffer || buffer instanceof Array || buffer instanceof Uint8Array) { + let str = ''; + for (let i = 0; i < buffer.length; i++) { + let code = buffer[i]; + if (code > 128) { + // Decode Unicode character + if ((code & 0xf0) === 0xf0) { + // 4 byte char + const b1 = code, b2 = buffer[i + 1], b3 = buffer[i + 2], b4 = buffer[i + 3]; + code = ((b1 & 0x7) << 18) | ((b2 & 0x3f) << 12) | ((b3 & 0x3f) << 6) | (b4 & 0x3f); + i += 3; + } + else if ((code & 0xe0) === 0xe0) { + // 3 byte char + const b1 = code, b2 = buffer[i + 1], b3 = buffer[i + 2]; + code = ((b1 & 0xf) << 12) | ((b2 & 0x3f) << 6) | (b3 & 0x3f); + i += 2; + } + else if ((code & 0xc0) === 0xc0) { + // 2 byte char + const b1 = code, b2 = buffer[i + 1]; + code = ((b1 & 0x1f) << 6) | (b2 & 0x3f); + i++; + } + else { + throw new Error('invalid utf-8 data'); + } + } + if (code >= 65536) { + // Split into 2-part utf-16 char codes + code ^= 0x10000; + const p1 = 0xd800 | (code >> 10); + const p2 = 0xdc00 | (code & 0x3ff); + str += String.fromCharCode(p1); + str += String.fromCharCode(p2); + } + else { + str += String.fromCharCode(code); + } + } + return str; } - 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; + else { + throw new Error('Unsupported buffer argument'); } - if (op === 'matches') { - return compare.test(val.toString()); + } +} +exports.decodeString = decodeString; +function concatTypedArrays(a, b) { + const c = new a.constructor(a.length + b.length); + c.set(a); + c.set(b, a.length); + return c; +} +exports.concatTypedArrays = concatTypedArrays; +function cloneObject(original, stack) { + var _a; + if (((_a = original === null || original === void 0 ? void 0 : original.constructor) === null || _a === void 0 ? void 0 : _a.name) === '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)) { + // FIX for typed array being converted to objects with numeric properties: + // Convert Buffer or TypedArray to ArrayBuffer + obj = obj.buffer.slice(obj.byteOffset, obj.byteOffset + obj.byteLength); } - if (op === '!matches') { - return !compare.test(val.toString()); + return obj; + }; + original = checkAndFixTypedArray(original); + if (typeof original !== 'object' || original === null || original instanceof Date || original instanceof ArrayBuffer || original instanceof path_reference_1.PathReference || original instanceof RegExp) { + return original; + } + const cloneValue = (val) => { + if (stack.indexOf(val) >= 0) { + throw new ReferenceError('object contains a circular reference'); } - if (op === 'between') { - return val >= compare[0] && val <= compare[1]; + val = checkAndFixTypedArray(val); + if (val === null || val instanceof Date || val instanceof ArrayBuffer || val instanceof path_reference_1.PathReference || val instanceof RegExp) { // || val instanceof ID + return val; } - if (op === '!between') { - return val < compare[0] || val > compare[1]; + else if (typeof val === 'object') { + stack.push(val); + val = cloneObject(val, stack); + stack.pop(); + return val; } - if (op === 'has' || op === '!has') { - const has = typeof val === 'object' && compare in val; - return op === 'has' ? has : !has; + else { + return val; // Anything other can just be copied } - if (op === 'contains' || op === '!contains') { - // TODO: rename to "includes"? - const includes = typeof val === 'object' && val instanceof Array && val.includes(compare); - return op === 'contains' ? includes : !includes; + }; + if (typeof stack === 'undefined') { + stack = [original]; + } + const clone = original instanceof Array ? [] : original instanceof partial_array_1.PartialArray ? new partial_array_1.PartialArray() : {}; + Object.keys(original).forEach(key => { + const val = original[key]; + if (typeof val === 'function') { + return; // skip functions } + clone[key] = cloneValue(val); + }); + return clone; +} +exports.cloneObject = cloneObject; +const isTypedArray = (val) => typeof val === 'object' && ['ArrayBuffer', 'Buffer', 'Uint8Array', 'Uint16Array', 'Uint32Array', 'Int8Array', 'Int16Array', 'Int32Array'].includes(val.constructor.name); +// CONSIDER: updating isTypedArray to: const isTypedArray = val => typeof val === 'object' && 'buffer' in val && 'byteOffset' in val && 'byteLength' in val; +function valuesAreEqual(val1, val2) { + if (val1 === val2) { + return true; + } + if (typeof val1 !== typeof val2) { return false; } - /** - * Export a specific path's data to a stream - * @param path - * @param write function that writes to a stream, or stream object that has a write method that (optionally) returns a promise the export needs to wait for before continuing - * @returns returns a promise that resolves once all data is exported - */ - async exportNode(path, writeFn, options = { format: 'json', type_safe: true }) { - if ((options === null || options === void 0 ? void 0 : options.format) !== 'json') { - throw new Error('Only json output is currently supported'); - } - const write = typeof writeFn !== 'function' - ? writeFn.write.bind(writeFn) // Using the "old" stream argument. Use its write method for backward compatibility - : writeFn; - const stringifyValue = (type, val) => { - const escape = (str) => str - .replace(/\\/g, '\\\\') // forward slashes - .replace(/"/g, '\\"') // quotes - .replace(/\r/g, '\\r') // carriage return - .replace(/\n/g, '\\n') // line feed - .replace(/\t/g, '\\t') // tabs - .replace(/[\u0000-\u001f]/g, // other control characters - // other control characters - ch => `\\u${ch.charCodeAt(0).toString(16).padStart(4, '0')}`); - if (type === node_value_types_js_1.VALUE_TYPES.DATETIME) { - val = `"${val.toISOString()}"`; - if (options.type_safe) { - val = `{".type":"date",".val":${val}}`; // Previously: "Date" - } - } - else if (type === node_value_types_js_1.VALUE_TYPES.STRING) { - val = `"${escape(val)}"`; - } - else if (type === node_value_types_js_1.VALUE_TYPES.ARRAY) { - val = '[]'; - } - else if (type === node_value_types_js_1.VALUE_TYPES.OBJECT) { - val = '{}'; - } - else if (type === node_value_types_js_1.VALUE_TYPES.BINARY) { - val = `"${escape(acebase_core_1.ascii85.encode(val))}"`; // TODO: use base64 instead, no escaping needed - if (options.type_safe) { - val = `{".type":"binary",".val":${val}}`; // Previously: "Buffer" - } - } - else if (type === node_value_types_js_1.VALUE_TYPES.REFERENCE) { - val = `"${val.path}"`; - if (options.type_safe) { - val = `{".type":"reference",".val":${val}}`; // Previously: "PathReference" - } - } - else if (type === node_value_types_js_1.VALUE_TYPES.BIGINT) { - // Unfortnately, JSON.parse does not support 0n bigint json notation - val = `"${val}"`; - if (options.type_safe) { - val = `{".type":"bigint",".val":${val}}`; - } - } - return val; - }; - let objStart = '', objEnd = ''; - const nodeInfo = await this.getNodeInfo(path); - if (!nodeInfo.exists) { - return write('null'); + if (typeof val1 === 'object' || typeof val2 === 'object') { + if (val1 === null || val2 === null) { + return false; } - else if (nodeInfo.type === node_value_types_js_1.VALUE_TYPES.OBJECT) { - objStart = '{'; - objEnd = '}'; + if (val1 instanceof path_reference_1.PathReference || val2 instanceof path_reference_1.PathReference) { + return val1 instanceof path_reference_1.PathReference && val2 instanceof path_reference_1.PathReference && val1.path === val2.path; } - else if (nodeInfo.type === node_value_types_js_1.VALUE_TYPES.ARRAY) { - objStart = '['; - objEnd = ']'; + if (val1 instanceof Date || val2 instanceof Date) { + return val1 instanceof Date && val2 instanceof Date && val1.getTime() === val2.getTime(); } - else { - // Node has no children, get and export its value - const node = await this.getNode(path); - const val = stringifyValue(nodeInfo.type, node.value); - return write(val); + if (val1 instanceof Array || val2 instanceof Array) { + return val1 instanceof Array && val2 instanceof Array && val1.length === val2.length && val1.every((item, i) => valuesAreEqual(val1[i], val2[i])); } - if (objStart) { - const p = write(objStart); - if (p instanceof Promise) { - await p; + if (isTypedArray(val1) || isTypedArray(val2)) { + if (!isTypedArray(val1) || !isTypedArray(val2) || val1.byteLength === val2.byteLength) { + return false; } + const typed1 = val1 instanceof ArrayBuffer ? new Uint8Array(val1) : new Uint8Array(val1.buffer, val1.byteOffset, val1.byteLength), typed2 = val2 instanceof ArrayBuffer ? new Uint8Array(val2) : new Uint8Array(val2.buffer, val2.byteOffset, val2.byteLength); + return typed1.every((val, i) => typed2[i] === val); } - let output = '', outputCount = 0; - const pending = []; - await this.getChildren(path) - .next(childInfo => { - if (childInfo.address) { - // Export child recursively - pending.push(childInfo); - } - else { - if (outputCount++ > 0) { - output += ','; - } - if (typeof childInfo.key === 'string') { - output += `"${childInfo.key}":`; - } - output += stringifyValue(childInfo.type, childInfo.value); - } - }); - if (output) { - const p = write(output); - if (p instanceof Promise) { - await p; - } + const keys1 = Object.keys(val1), keys2 = Object.keys(val2); + return keys1.length === keys2.length && keys1.every(key => keys2.includes(key)) && keys1.every(key => valuesAreEqual(val1[key], val2[key])); + } + return false; +} +exports.valuesAreEqual = valuesAreEqual; +class ObjectDifferences { + constructor(added, removed, changed) { + this.added = added; + this.removed = removed; + this.changed = changed; + } + forChild(key) { + if (this.added.includes(key)) { + return 'added'; } - while (pending.length > 0) { - const childInfo = pending.shift(); - let output = outputCount++ > 0 ? ',' : ''; - const key = typeof childInfo.index === 'number' ? childInfo.index : childInfo.key; - if (typeof key === 'string') { - output += `"${key}":`; + if (this.removed.includes(key)) { + return 'removed'; + } + const changed = this.changed.find(ch => ch.key === key); + return changed ? changed.change : 'identical'; + } +} +exports.ObjectDifferences = ObjectDifferences; +function compareValues(oldVal, newVal, sortedResults = false) { + 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 (isTypedArray(oldVal) || isTypedArray(newVal)) { + // One or both values are typed arrays. + if (!isTypedArray(oldVal) || !isTypedArray(newVal)) { + return 'changed'; + } + // Both are typed. Compare lengths and byte content of typed arrays + const typed1 = oldVal instanceof Uint8Array ? oldVal : oldVal instanceof ArrayBuffer ? new Uint8Array(oldVal) : new Uint8Array(oldVal.buffer, oldVal.byteOffset, oldVal.byteLength); + const typed2 = newVal instanceof Uint8Array ? newVal : newVal instanceof ArrayBuffer ? new Uint8Array(newVal) : new Uint8Array(newVal.buffer, newVal.byteOffset, newVal.byteLength); + return typed1.byteLength === typed2.byteLength && typed1.every((val, i) => typed2[i] === val) ? 'identical' : 'changed'; + } + else if (oldVal instanceof Date || newVal instanceof Date) { + return oldVal instanceof Date && newVal instanceof Date && oldVal.getTime() === newVal.getTime() ? 'identical' : 'changed'; + } + else if (oldVal instanceof path_reference_1.PathReference || newVal instanceof path_reference_1.PathReference) { + return oldVal instanceof path_reference_1.PathReference && newVal instanceof path_reference_1.PathReference && oldVal.path === newVal.path ? 'identical' : 'changed'; + } + else if (typeof oldVal === 'object') { + // Do key-by-key comparison of objects + const isArray = oldVal instanceof Array; + const getKeys = (obj) => { + let keys = Object.keys(obj).filter(key => !voids.includes(obj[key])); + if (isArray) { + keys = keys.map((v) => parseInt(v)); } - if (output) { - const p = write(output); - if (p instanceof Promise) { - await p; + return keys; + }; + const oldKeys = getKeys(oldVal); + const newKeys = getKeys(newVal); + const removedKeys = oldKeys.filter(key => !newKeys.includes(key)); + const addedKeys = newKeys.filter(key => !oldKeys.includes(key)); + const changedKeys = newKeys.reduce((changed, key) => { + if (oldKeys.includes(key)) { + const val1 = oldVal[key]; + const val2 = newVal[key]; + const c = compareValues(val1, val2); + if (c !== 'identical') { + changed.push({ key, change: c }); } } - await this.exportNode(acebase_core_1.PathInfo.getChildPath(path, key), write, options); + return changed; + }, []); + if (addedKeys.length === 0 && removedKeys.length === 0 && changedKeys.length === 0) { + return 'identical'; } - if (objEnd) { - const p = write(objEnd); - if (p instanceof Promise) { - await p; + else { + return new ObjectDifferences(addedKeys, removedKeys, sortedResults ? changedKeys.sort((a, b) => a.key < b.key ? -1 : 1) : changedKeys); + } + } + return 'changed'; +} +exports.compareValues = compareValues; +function getMutations(oldVal, newVal, sortedResults = false) { + const process = (target, compareResult, prev, val) => { + switch (compareResult) { + case 'identical': return []; + case 'changed': return [{ target, prev, val }]; + case 'added': return [{ target, prev: null, val }]; + case 'removed': return [{ target, prev, val: null }]; + default: { + let changes = []; + compareResult.added.forEach(key => changes.push({ target: target.concat(key), prev: null, val: val[key] })); + compareResult.removed.forEach(key => changes.push({ target: target.concat(key), prev: prev[key], val: null })); + compareResult.changed.forEach(item => { + const childChanges = process(target.concat(item.key), item.change, prev[item.key], val[item.key]); + changes = changes.concat(childChanges); + }); + return changes; } } + }; + const compareResult = compareValues(oldVal, newVal, sortedResults); + return process([], compareResult, oldVal, newVal); +} +exports.getMutations = getMutations; +function 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, newValue }; +} +exports.getChildValues = getChildValues; +function defer(fn) { + process_1.default.nextTick(fn); +} +exports.defer = defer; +function getGlobalObject() { + var _a; + if (typeof globalThis !== 'undefined') { + return globalThis; + } + if (typeof global !== 'undefined') { + return global; + } + if (typeof window !== 'undefined') { + return window; + } + if (typeof self !== 'undefined') { + return self; } + return (_a = (function () { return this; }())) !== null && _a !== void 0 ? _a : Function('return this')(); +} +exports.getGlobalObject = getGlobalObject; + +}).call(this)}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {},require("buffer").Buffer) +},{"./partial-array":15,"./path-reference":17,"./process":18,"buffer":62}],28:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.BrowserAceBase = void 0; +const acebase_local_js_1 = require("./acebase-local.js"); +const index_js_1 = require("./storage/custom/indexed-db/index.js"); +const deprecatedConstructorError = `Using AceBase constructor in the browser to use localStorage is deprecated! +Switch to: +IndexedDB implementation (FASTER, MORE RELIABLE): + let db = AceBase.WithIndexedDB(name, settings) +Or, new LocalStorage implementation: + let db = AceBase.WithLocalStorage(name, settings) +Or, write your own CustomStorage adapter: + let myCustomStorage = new CustomStorageSettings({ ... }); + let db = new AceBase(name, { storage: myCustomStorage })`; +class BrowserAceBase extends acebase_local_js_1.AceBase { /** - * Import a specific path's data from a stream - * @param path - * @param read read function that streams a new chunk of data - * @returns returns a promise that resolves once all data is imported + * Constructor that is used in browser context + * @param name database name + * @param settings settings */ - async importNode(path, read, options = { format: 'json', method: 'set' }) { - const chunkSize = 256 * 1024; // 256KB - const maxQueueBytes = 1024 * 1024; // 1MB - const state = { - data: '', - index: 0, - offset: 0, - queue: [], - queueStartByte: 0, - timesFlushed: 0, - get processedBytes() { - return this.offset + this.index; - }, - }; - const readNextChunk = async (append = false) => { - let data = await read(chunkSize); - if (data === null) { - if (state.data) { - throw new Error(`Unexpected EOF at index ${state.offset + state.data.length}`); - } - else { - throw new Error('Unable to read data from stream'); - } - } - else if (typeof data === 'object') { - data = acebase_core_1.Utils.decodeString(data); + constructor(name, settings) { + if (typeof settings !== 'object' || typeof settings.storage !== 'object') { + // Client is using old AceBaseBrowser signature, eg: + // let db = new AceBase('name', { temp: false }) + // + // Don't allow this anymore. If client wants to use localStorage, + // they need to switch to AceBase.WithLocalStorage('name', settings). + // If they want to use custom storage in the browser, they must + // use the same constructor signature AceBase has: + // let db = new AceBase('name', { storage: new CustomStorageSettings({ ... }) }); + throw new Error(deprecatedConstructorError); + } + super(name, settings); + this.settings.ipcEvents = settings.multipleTabs === true; + } + /** + * Creates an AceBase database instance using IndexedDB as storage engine + * @param dbname Name of the database + * @param settings optional settings + */ + static WithIndexedDB(dbname, init = {}) { + return (0, index_js_1.createIndexedDBInstance)(dbname, init); + } +} +exports.BrowserAceBase = BrowserAceBase; + +},{"./acebase-local.js":29,"./storage/custom/indexed-db/index.js":49}],29:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.AceBase = exports.AceBaseLocalSettings = exports.IndexedDBStorageSettings = exports.LocalStorageSettings = void 0; +const acebase_core_1 = require("acebase-core"); +const index_js_1 = require("./storage/binary/index.js"); +const api_local_js_1 = require("./api-local.js"); +const index_js_2 = require("./storage/custom/local-storage/index.js"); +Object.defineProperty(exports, "LocalStorageSettings", { enumerable: true, get: function () { return index_js_2.LocalStorageSettings; } }); +const settings_js_1 = require("./storage/custom/indexed-db/settings.js"); +Object.defineProperty(exports, "IndexedDBStorageSettings", { enumerable: true, get: function () { return settings_js_1.IndexedDBStorageSettings; } }); +class AceBaseLocalSettings extends acebase_core_1.AceBaseBaseSettings { + constructor(options = {}) { + super(options); + if (options.storage) { + this.storage = options.storage; + // If they were set on global settings, copy IPC and transaction settings to storage settings + if (options.ipc) { + this.storage.ipc = options.ipc; } - if (append) { - state.data += data; + if (options.transactions) { + this.storage.transactions = options.transactions; } - else { - state.offset += state.data.length; - state.data = data; - state.index = 0; - } - }; - const readBytes = async (length) => { - let str = ''; - if (state.index + length >= state.data.length) { - str = state.data.slice(state.index); - length -= str.length; - await readNextChunk(); - } - str += state.data.slice(state.index, state.index + length); - state.index += length; - return str; - }; - const assertBytes = async (length) => { - if (state.index + length > state.data.length) { - await readNextChunk(true); - } - if (state.index + length > state.data.length) { - throw new Error('Not enough data available from stream'); - } - }; - const consumeToken = async (token) => { - // const str = state.data.slice(state.index, state.index + token.length); - const str = await readBytes(token.length); - if (str !== token) { - throw new Error(`Unexpected character "${str[0]}" at index ${state.offset + state.index}, expected "${token}"`); - } - }; - const consumeSpaces = async () => { - const spaces = [' ', '\t', '\r', '\n']; - while (true) { - if (state.index >= state.data.length) { - await readNextChunk(); - } - if (spaces.includes(state.data[state.index])) { - state.index++; + } + } +} +exports.AceBaseLocalSettings = AceBaseLocalSettings; +class AceBase extends acebase_core_1.AceBaseBase { + /** + * @param dbname Name of the database to open or create + */ + constructor(dbname, init = {}) { + const settings = new AceBaseLocalSettings(init); + super(dbname, settings); + this.recovery = { + /** + * Repairs a node that cannot be loaded by removing the reference from its parent, or marking it as removed + */ + repairNode: async (path, options) => { + await this.ready(); + if (this.api.storage instanceof index_js_1.AceBaseStorage) { + await this.api.storage.repairNode(path, options); } - else { - break; + else if (!this.api.storage.repairNode) { + throw new Error(`repairNode is not supported with chosen storage engine`); } - } - }; - /** - * Reads number of bytes from the stream but does not consume them - */ - const peekBytes = async (length) => { - await assertBytes(length); - const index = state.index; - return state.data.slice(index, index + length); + }, + /** + * Repairs a node that uses a B+Tree for its keys (100+ children). + * See https://github.com/appy-one/acebase/issues/183 + * @param path Target path to fix + */ + repairNodeTree: async (path) => { + await this.ready(); + const storage = this.api.storage; + await storage.repairNodeTree(path); + }, }; - /** - * Tries to detect what type of value to expect, but does not read it - * @returns - */ - const peekValueType = async () => { - await consumeSpaces(); - const ch = await peekBytes(1); - switch (ch) { - case '"': return 'string'; - case '{': return 'object'; - case '[': return 'array'; - case 'n': return 'null'; - case 'u': return 'undefined'; - case 't': - case 'f': - return 'boolean'; - default: { - if (ch === '-' || (ch >= '0' && ch <= '9')) { - return 'number'; - } - throw new Error(`Unknown value at index ${state.offset + state.index}`); - } - } + const apiSettings = { + db: this, + settings, }; - /** - * Reads a string from the stream at current index. Expects current character to be " - */ - const readString = async () => { - await consumeToken('"'); - let str = ''; - let i = state.index; - // Read until next (unescaped) quote - while (state.data[i] !== '"' || state.data[i - 1] === '\\') { - i++; - if (i >= state.data.length) { - str += state.data.slice(state.index); - await readNextChunk(); - i = 0; - } - } - str += state.data.slice(state.index, i); - state.index = i + 1; - return unescape(str); + this.api = new api_local_js_1.LocalApi(dbname, apiSettings, () => { + this.emit('ready'); + }); + } + async close() { + // Close the database by calling exit on the ipc channel, which will emit an 'exit' event when the database can be safely closed. + await this.api.storage.close(); + } + get settings() { + const ipc = this.api.storage.ipc, debug = this.debug; + return { + get logLevel() { return debug.level; }, + set logLevel(level) { debug.setLevel(level); }, + get ipcEvents() { return ipc.eventsEnabled; }, + set ipcEvents(enabled) { ipc.eventsEnabled = enabled; }, }; - const readBoolean = async () => { - if (state.data[state.index] === 't') { - await consumeToken('true'); + } + /** + * Creates an AceBase database instance using LocalStorage or SessionStorage as storage engine. When running in non-browser environments, set + * settings.provider to a custom LocalStorage provider, eg 'node-localstorage' + * @param dbname Name of the database + * @param settings optional settings + */ + static WithLocalStorage(dbname, settings = {}) { + const db = (0, index_js_2.createLocalStorageInstance)(dbname, settings); + return db; + } + /** + * Creates an AceBase database instance using IndexedDB as storage engine. Only available in browser contexts! + * @param dbname Name of the database + * @param settings optional settings + */ + static WithIndexedDB(dbname, init = {}) { + throw new Error(`IndexedDB storage can only be used in browser contexts`); + } +} +exports.AceBase = AceBase; + +},{"./api-local.js":30,"./storage/binary/index.js":45,"./storage/custom/indexed-db/settings.js":50,"./storage/custom/local-storage/index.js":52,"acebase-core":12}],30:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.LocalApi = void 0; +const acebase_core_1 = require("acebase-core"); +const index_js_1 = require("./storage/binary/index.js"); +const index_js_2 = require("./storage/sqlite/index.js"); +const index_js_3 = require("./storage/mssql/index.js"); +const index_js_4 = require("./storage/custom/index.js"); +const node_value_types_js_1 = require("./node-value-types.js"); +const query_js_1 = require("./query.js"); +const node_errors_js_1 = require("./node-errors.js"); +class LocalApi extends acebase_core_1.Api { + constructor(dbname = 'default', init, readyCallback) { + super(); + this.db = init.db; + this.logger = init.db.logger; + const storageEnv = { logLevel: init.settings.logLevel, logColors: init.settings.logColors, logger: init.settings.logger }; + if (typeof init.settings.storage === 'object') { + // settings.storage.logLevel = settings.logLevel; + if (index_js_2.SQLiteStorageSettings && (init.settings.storage instanceof index_js_2.SQLiteStorageSettings)) { // || env.settings.storage.type === 'sqlite' + this.storage = new index_js_2.SQLiteStorage(dbname, init.settings.storage, storageEnv); } - else if (state.data[state.index] === 'f') { - await consumeToken('false'); + else if (index_js_3.MSSQLStorageSettings && (init.settings.storage instanceof index_js_3.MSSQLStorageSettings)) { // || env.settings.storage.type === 'mssql' + this.storage = new index_js_3.MSSQLStorage(dbname, init.settings.storage, storageEnv); } - throw new Error(`Expected true or false at index ${state.offset + state.index}`); - }; - const readNumber = async () => { - let str = ''; - let i = state.index; - // Read until non-number character is encountered - const nrChars = ['-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', 'e', 'b', 'f', 'x', 'o', 'n']; // b: 0b110101, x: 0x3a, o: 0o01, n: 29723n, e: 10e+23, f: ? - while (nrChars.includes(state.data[i])) { - i++; - if (i >= state.data.length) { - str += state.data.slice(state.index); - await readNextChunk(); - i = 0; - } + else if (index_js_4.CustomStorageSettings && (init.settings.storage instanceof index_js_4.CustomStorageSettings)) { // || settings.storage.type === 'custom' + this.storage = new index_js_4.CustomStorage(dbname, init.settings.storage, storageEnv); } - str += state.data.slice(state.index, i); - state.index = i; - const nr = str.endsWith('n') ? BigInt(str.slice(0, -1)) : str.includes('.') ? parseFloat(str) : parseInt(str); - return nr; - }; - const readValue = async () => { - await consumeSpaces(); - const type = await peekValueType(); - const value = await (() => { - switch (type) { - case 'string': return readString(); - case 'object': return {}; - case 'array': return []; - case 'number': return readNumber(); - case 'null': return null; - case 'undefined': return undefined; - case 'boolean': return readBoolean(); - } - })(); - return { type, value }; - }; - const unescape = (str) => str.replace(/\\n/g, '\n').replace(/\\"/g, '"'); - const getTypeSafeValue = (path, obj) => { - const type = obj['.type']; - let val = obj['.val']; - switch (type) { - case 'Date': - case 'date': { - val = new Date(val); - break; - } - case 'Buffer': - case 'binary': { - val = unescape(val); - if (val.startsWith('<~')) { - // Ascii85 encoded - val = acebase_core_1.ascii85.decode(val); - } - else { - // base64 not implemented yet - throw new Error(`Import error: Unexpected encoding for value for value at path "/${path}"`); - } - break; - } - case 'PathReference': - case 'reference': { - val = new acebase_core_1.PathReference(val); - break; - } - case 'bigint': { - val = BigInt(val); - break; - } - default: - throw new Error(`Import error: Unsupported type "${type}" for value at path "/${path}"`); + else { + const storageSettings = init.settings.storage instanceof index_js_1.AceBaseStorageSettings + ? init.settings.storage + : new index_js_1.AceBaseStorageSettings(init.settings.storage); + this.storage = new index_js_1.AceBaseStorage(dbname, storageSettings, storageEnv); } - return val; - }; - const context = { acebase_import_id: acebase_core_1.ID.generate() }; - const childOptions = { suppress_events: options.suppress_events, context }; - /** - * Work in progress (not used yet): queue nodes to store to improve performance - */ - const enqueue = async (target, value) => { - state.queue.push({ target, value }); - if (state.processedBytes >= state.queueStartByte + maxQueueBytes) { - // Flush queue, group queued (set) items as update operations on their parents - const operations = state.queue.reduce((updates, item) => { - // Optimization idea: find all data we know is complete, add that as 1 set if method !== 'merge' - // Example: queue is something like [ - // "users/user1": {}, - // "users/user1/email": "user@example.com" - // "users/user1/addresses": {}, - // "users/user1/addresses/address1": {}, - // "users/user1/addresses/address1/city": "Amsterdam", - // "users/user1/addresses/address2": {}, // We KNOW "users/user1/addresses/address1" is not coming back - // "users/user1/addresses/address2/city": "Berlin", - // "users/user2": {} // <-- We KNOW "users/user1" is not coming back! - //] - if (item.target.path === path) { - // This is the import target. If method is 'set' and this is the first flush, add it as 'set' operation. - // Use 'update' in all other cases - updates.push(Object.assign({ op: options.method === 'set' && state.timesFlushed === 0 ? 'set' : 'update' }, item)); - } - else { - // Find parent to merge with - const parent = updates.find(other => other.target.isParentOf(item.target)); - if (parent) { - parent.value[item.target.key] = item.value; - } - else { - // Parent not found. If method is 'merge', use 'update', otherwise use or 'set' - updates.push(Object.assign({ op: options.method === 'merge' ? 'update' : 'set' }, item)); - } - } - return updates; - }, []); - // Fresh state - state.queueStartByte = state.processedBytes; - state.queue = []; - state.timesFlushed++; - // Execute db updates - } - if (target.path === path) { - // This is the import target. If method === 'set' - } - }; - const importObject = async (target) => { - await consumeToken('{'); - await consumeSpaces(); - const nextChar = await peekBytes(1); - if (nextChar === '}') { - state.index++; - return this.setNode(target.path, {}, childOptions); - } - let childCount = 0; - let obj = {}; - let flushedBefore = false; - const flushObject = async () => { - let p; - if (!flushedBefore) { - flushedBefore = true; - p = this.setNode(target.path, obj, childOptions); - } - else if (Object.keys(obj).length > 0) { - p = this.updateNode(target.path, obj, childOptions); - } - obj = {}; - if (p) { - await p; - } - }; - const promises = []; - while (true) { - await consumeSpaces(); - const property = await readString(); // readPropertyName(); - await consumeSpaces(); - await consumeToken(':'); - await consumeSpaces(); - const { value, type } = await readValue(); - obj[property] = value; - childCount++; - if (['object', 'array'].includes(type)) { - // Flush current imported value before proceeding with object/array child - promises.push(flushObject()); - if (type === 'object') { - // Import child object/array - await importObject(target.child(property)); - } - else { - await importArray(target.child(property)); - } - } - // What comes next? End of object ('}') or new property (',')? - await consumeSpaces(); - const nextChar = await peekBytes(1); - if (nextChar === '}') { - // Done importing this object - state.index++; - break; - } - // Assume comma now - await consumeToken(','); - } - const isTypedValue = childCount === 2 && '.type' in obj && '.val' in obj; - if (isTypedValue) { - // This is a value that was exported with type safety. - // Do not store as object, but convert to original value - // Note that this is done regardless of options.type_safe - const val = getTypeSafeValue(target.path, obj); - return this.setNode(target.path, val, childOptions); - } - promises.push(flushObject()); - await Promise.all(promises); - }; - const importArray = async (target) => { - await consumeToken('['); - await consumeSpaces(); - const nextChar = await peekBytes(1); - if (nextChar === ']') { - state.index++; - return this.setNode(target.path, [], childOptions); - } - let flushedBefore = false; - let arr = []; - let updates = {}; - const flushArray = async () => { - let p; - if (!flushedBefore) { - // Store array - flushedBefore = true; - p = this.setNode(target.path, arr, childOptions); - arr = null; // GC - } - else if (Object.keys(updates).length > 0) { - // Flush updates - p = this.updateNode(target.path, updates, childOptions); - updates = {}; - } - if (p) { - await p; - } - }; - const pushChild = (value, index) => { - if (flushedBefore) { - updates[index] = value; - } - else { - arr.push(value); - } - }; - const promises = []; - let index = 0; - while (true) { - await consumeSpaces(); - const { value, type } = await readValue(); - pushChild(value, index); - if (['object', 'array'].includes(type)) { - // Flush current imported value before proceeding with object/array child - promises.push(flushArray()); // No need to await now - if (type === 'object') { - // Import child object/array - await importObject(target.child(index)); - } - else { - await importArray(target.child(index)); - } - } - // What comes next? End of array (']') or new property (',')? - await consumeSpaces(); - const nextChar = await peekBytes(1); - if (nextChar === ']') { - // Done importing this array - state.index++; - break; - } - // Assume comma now - await consumeToken(','); - index++; - } - promises.push(flushArray()); - await Promise.all(promises); - }; - const start = async () => { - const { value, type } = await readValue(); - if (['object', 'array'].includes(type)) { - // Object or array value, has not been read yet - const target = acebase_core_1.PathInfo.get(path); - if (type === 'object') { - await importObject(target); - } - else { - await importArray(target); - } - } - else { - // Simple value - await this.setNode(path, value, childOptions); - } - }; - return start(); - } - /** - * Adds, updates or removes a schema definition to validate node values before they are stored at the specified path - * @param path target path to enforce the schema on, can include wildcards. Eg: 'users/*\/posts/*' or 'users/$uid/posts/$postid' - * @param schema schema type definitions. When null value is passed, a previously set schema is removed. - */ - setSchema(path, schema, warnOnly = false) { - if (typeof schema === 'undefined') { - throw new TypeError('schema argument must be given'); - } - if (schema === null) { - // Remove previously set schema on path - const i = this._schemas.findIndex(s => s.path === path); - i >= 0 && this._schemas.splice(i, 1); - return; - } - // Parse schema, add or update it - const definition = new acebase_core_1.SchemaDefinition(schema, { - warnOnly, - warnCallback: (message) => this.logger.warn(message), - }); - const item = this._schemas.find(s => s.path === path); - if (item) { - item.schema = definition; } else { - this._schemas.push({ path, schema: definition }); - this._schemas.sort((a, b) => { - const ka = acebase_core_1.PathInfo.getPathKeys(a.path), kb = acebase_core_1.PathInfo.getPathKeys(b.path); - if (ka.length === kb.length) { - return 0; - } - return ka.length < kb.length ? -1 : 1; - }); + this.storage = new index_js_1.AceBaseStorage(dbname, new index_js_1.AceBaseStorageSettings(), storageEnv); } + this.storage.on('ready', readyCallback); + } + async stats(options) { + return this.storage.stats; + } + subscribe(path, event, callback) { + this.storage.subscriptions.add(path, event, callback); + } + unsubscribe(path, event, callback) { + this.storage.subscriptions.remove(path, event, callback); } /** - * Gets currently active schema definition for the specified path + * Creates a new node or overwrites an existing node + * @param path + * @param value Any value will do. If the value is small enough to be stored in a parent record, it will take care of it + * @returns returns a promise with the new cursor (if transaction logging is enabled) */ - getSchema(path) { - const item = this._schemas.find(item => item.path === path); - return item ? { path, schema: item.schema.source, text: item.schema.text } : null; + async set(path, value, options = { + suppress_events: false, + context: null, + }) { + const cursor = await this.storage.setNode(path, value, { suppress_events: options.suppress_events, context: options.context }); + return Object.assign({}, (cursor && { cursor })); } /** - * Gets all currently active schema definitions + * Updates an existing node, or creates a new node. + * @returns returns a promise with the new cursor (if transaction logging is enabled) */ - getSchemas() { - return this._schemas.map(item => ({ path: item.path, schema: item.schema.source, text: item.schema.text })); + async update(path, updates, options = { + suppress_events: false, + context: null, + }) { + const cursor = await this.storage.updateNode(path, updates, { suppress_events: options.suppress_events, context: options.context }); + return Object.assign({}, (cursor && { cursor })); + } + get transactionLoggingEnabled() { + return this.storage.settings.transactions && this.storage.settings.transactions.log === true; } /** - * Validates the schemas of the node being updated and its children - * @param path path being written to - * @param value the new value, or updates to current value - * @example - * // define schema for each tag of each user post: - * db.schema.set( - * 'users/$uid/posts/$postId/tags/$tagId', - * { name: 'string', 'link_id?': 'number' } - * ); - * - * // Insert that will fail: - * db.ref('users/352352/posts/572245').set({ - * text: 'this is my post', - * tags: { sometag: 'deny this' } // <-- sometag must be typeof object - * }); - * - * // Insert that will fail: - * db.ref('users/352352/posts/572245').set({ - * text: 'this is my post', - * tags: { - * tag1: { name: 'firstpost', link_id: 234 }, - * tag2: { name: 'newbie' }, - * tag3: { title: 'Not allowed' } // <-- title property not allowed - * } - * }); - * - * // Update that fails if post does not exist: - * db.ref('users/352352/posts/572245/tags/tag1').update({ - * name: 'firstpost' - * }); // <-- post is missing property text + * Gets the value of a node + * @param options when omitted retrieves all nested data. If `include` is set to an array of keys it will only return those children. + * If `exclude` is set to an array of keys, those values will not be included */ - validateSchema(path, value, options = { updates: false }) { - let result = { ok: true }; - const pathInfo = acebase_core_1.PathInfo.get(path); - this._schemas.filter(s => pathInfo.isOnTrailOf(s.path)).every(s => { - if (pathInfo.isDescendantOf(s.path)) { - // Given check path is a descendant of this schema definition's path - const ancestorPath = acebase_core_1.PathInfo.fillVariables(s.path, path); - const trailKeys = pathInfo.keys.slice(acebase_core_1.PathInfo.getPathKeys(s.path).length); - result = s.schema.check(ancestorPath, value, options.updates, trailKeys); - return result.ok; - } - // Given check path is on schema definition's path or on a higher path - const trailKeys = acebase_core_1.PathInfo.getPathKeys(s.path).slice(pathInfo.keys.length); - if (options.updates === true && trailKeys.length > 0 && !(trailKeys[0] in value)) { - // Fixes #217: this update on a higher path does not affect any data at schema's target path - return result.ok; - } - const partial = options.updates === true && trailKeys.length === 0; - const check = (path, value, trailKeys) => { - if (trailKeys.length === 0) { - // Check this node - return s.schema.check(path, value, partial); - } - else if (value === null) { - return { ok: true }; // Not at the end of trail, but nothing more to check - } - const key = trailKeys[0]; - if (typeof key === 'string' && (key === '*' || key[0] === '$')) { - // Wildcard. Check each key in value recursively - if (value === null || typeof value !== 'object') { - // Can't check children, because there are none. This is - // possible if another rule permits the value at current path - // to be something else than an object. - return { ok: true }; - } - let result; - Object.keys(value).every(childKey => { - const childPath = acebase_core_1.PathInfo.getChildPath(path, childKey); - const childValue = value[childKey]; - result = check(childPath, childValue, trailKeys.slice(1)); - return result.ok; - }); - return result; - } - else { - const childPath = acebase_core_1.PathInfo.getChildPath(path, key); - const childValue = value[key]; - return check(childPath, childValue, trailKeys.slice(1)); - } - }; - result = check(path, value, trailKeys); - return result.ok; - }); - return result; - } -} -exports.Storage = Storage; - -},{"../assert.js":4,"../data-index/index.js":7,"../ipc/index.js":8,"../node-errors.js":11,"../node-info.js":12,"../node-value-types.js":14,"../promise-fs/index.js":16,"./errors.js":28,"./indexes.js":30,"acebase-core":46}],35:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.AceBaseBase = exports.AceBaseBaseSettings = void 0; -/** - ________________________________________________________________________________ - - ___ ______ - / _ \ | ___ \ - / /_\ \ ___ ___| |_/ / __ _ ___ ___ - | _ |/ __/ _ \ ___ \/ _` / __|/ _ \ - | | | | (_| __/ |_/ / (_| \__ \ __/ - \_| |_/\___\___\____/ \__,_|___/\___| - realtime database - - Copyright 2018-2022 by Ewout Stortenbeker (me@appy.one) - Published under MIT license - - See docs at https://github.com/appy-one/acebase - ________________________________________________________________________________ - -*/ -const simple_event_emitter_1 = require("./simple-event-emitter"); -const data_reference_1 = require("./data-reference"); -const type_mappings_1 = require("./type-mappings"); -const optional_observable_1 = require("./optional-observable"); -const debug_1 = require("./debug"); -const simple_colors_1 = require("./simple-colors"); -class AceBaseBaseSettings { - constructor(options) { - /** - * What level to use for console logging. - * @default 'log' - */ - this.logLevel = 'log'; - /** - * Whether to use colors in the console logs output - * @default true - */ - this.logColors = true; - /** - * @internal (for internal use) - */ - this.info = 'realtime database'; - /** - * You can turn this on if you are a sponsor. See https://github.com/appy-one/acebase/discussions/100 for more info - */ - this.sponsor = false; - if (typeof options !== 'object') { + async get(path, options) { + if (!options) { options = {}; } - if (typeof options.logger === 'object') { - this.logger = options.logger; - } - if (typeof options.logLevel === 'string') { - this.logLevel = options.logLevel; - } - if (typeof options.logColors === 'boolean') { - this.logColors = options.logColors; + if (typeof options.include !== 'undefined' && !(options.include instanceof Array)) { + throw new TypeError(`options.include must be an array of key names`); } - if (typeof options.info === 'string') { - this.info = options.info; + if (typeof options.exclude !== 'undefined' && !(options.exclude instanceof Array)) { + throw new TypeError(`options.exclude must be an array of key names`); } - if (typeof options.sponsor === 'boolean') { - this.sponsor = options.sponsor; + if (['undefined', 'boolean'].indexOf(typeof options.child_objects) < 0) { + throw new TypeError(`options.child_objects must be a boolean`); } + const node = await this.storage.getNode(path, options); + return { value: node.value, context: { acebase_cursor: node.cursor }, cursor: node.cursor }; } -} -exports.AceBaseBaseSettings = AceBaseBaseSettings; -class AceBaseBase extends simple_event_emitter_1.SimpleEventEmitter { /** - * @param dbname Name of the database to open or create + * Performs a transaction on a Node + * @param path + * @param callback callback is called with the current value. The returned value (or promise) will be used as the new value. When the callbacks returns undefined, the transaction will be canceled. When callback returns null, the node will be removed. + * @returns returns a promise with the new cursor (if transaction logging is enabled) */ - constructor(dbname, options = {}) { - var _a; - super(); - this._ready = false; - options = new AceBaseBaseSettings(options); - this.name = dbname; - // Setup console logging - const legacyLogger = new debug_1.DebugLogger(options.logLevel, `[${dbname}]`); - this.debug = legacyLogger; // For backward compatibility - this.logger = (_a = options.logger) !== null && _a !== void 0 ? _a : legacyLogger; - // Enable/disable logging with colors - (0, simple_colors_1.SetColorsEnabled)(options.logColors); - // ASCI art: http://patorjk.com/software/taag/#p=display&f=Doom&t=AceBase - const logoStyle = [simple_colors_1.ColorStyle.magenta, simple_colors_1.ColorStyle.bold]; - const logo = ' ___ ______ ' + '\n' + - ' / _ \\ | ___ \\ ' + '\n' + - ' / /_\\ \\ ___ ___| |_/ / __ _ ___ ___ ' + '\n' + - ' | _ |/ __/ _ \\ ___ \\/ _` / __|/ _ \\' + '\n' + - ' | | | | (_| __/ |_/ / (_| \\__ \\ __/' + '\n' + - ' \\_| |_/\\___\\___\\____/ \\__,_|___/\\___|'; - const info = (options.info ? ''.padStart(40 - options.info.length, ' ') + options.info + '\n' : ''); - if (!options.sponsor) { - // if you are a sponsor, you can switch off the "AceBase banner ad" - legacyLogger.write(logo.colorize(logoStyle)); - info && legacyLogger.write(info.colorize(simple_colors_1.ColorStyle.magenta)); - } - // Setup type mapping functionality - this.types = new type_mappings_1.TypeMappings(this); - this.once('ready', () => { - // console.log(`database "${dbname}" (${this.constructor.name}) is ready to use`); - this._ready = true; - }); + async transaction(path, callback, options = { + suppress_events: false, + context: null, + }) { + const cursor = await this.storage.transactNode(path, callback, { suppress_events: options.suppress_events, context: options.context }); + return Object.assign({}, (cursor && { cursor })); + } + async exists(path) { + const nodeInfo = await this.storage.getNodeInfo(path); + return nodeInfo.exists; } + // query2(path, query, options = { snapshots: false, include: undefined, exclude: undefined, child_objects: undefined }) { + // /* + // Now that we're using indexes to filter data and order upon, each query requires a different strategy + // to get the results the quickest. + // So, we'll analyze the query first, build a strategy and then execute the strategy + // Analyze stage: + // - what path is being queried (wildcard path or single parent) + // - which indexes are available for the path + // - which indexes can be used for filtering + // - which indexes can be used for sorting + // - is take/skip used to limit the result set + // Strategy stage: + // - chain index filtering + // - .... + // TODO! + // */ + // } /** - * Waits for the database to be ready before running your callback. - * @param callback (optional) callback function that is called when the database is ready to be used. You can also use the returned promise. - * @returns returns a promise that resolves when ready + * @returns Returns a promise that resolves with matching data or paths in `results` */ - async ready(callback) { - if (!this._ready) { - // Wait for ready event - await new Promise(resolve => this.on('ready', resolve)); - } - callback === null || callback === void 0 ? void 0 : callback(); - } - get isReady() { - return this._ready; + async query(path, query, options = { snapshots: false }) { + const results = await (0, query_js_1.executeQuery)(this, path, query, options); + return results; } /** - * Allow specific observable implementation to be used - * @param ObservableImpl Implementation to use - */ - setObservable(ObservableImpl) { - (0, optional_observable_1.setObservable)(ObservableImpl); - } - /** - * Creates a reference to a node - * @param path - * @returns reference to the requested node + * Creates an index on key for all child nodes at path */ - ref(path) { - return new data_reference_1.DataReference(this, path); + createIndex(path, key, options) { + return this.storage.indexes.create(path, key, options); } /** - * Get a reference to the root database node - * @returns reference to root node + * Gets all indexes */ - get root() { - return this.ref(''); + async getIndexes() { + return this.storage.indexes.list(); } /** - * Creates a query on the requested node - * @param path - * @returns query for the requested node + * Deletes an existing index from the database */ - query(path) { - const ref = new data_reference_1.DataReference(this, path); - return new data_reference_1.DataReferenceQuery(ref); + async deleteIndex(filePath) { + return this.storage.indexes.delete(filePath); } - get indexes() { - return { - /** - * Gets all indexes - */ - get: () => { - return this.api.getIndexes(); - }, - /** - * Creates an index on "key" for all child nodes at "path". If the index already exists, nothing happens. - * Example: creating an index on all "name" keys of child objects of path "system/users", - * will index "system/users/user1/name", "system/users/user2/name" etc. - * You can also use wildcard paths to enable indexing and quering of fragmented data. - * Example: path "users/*\/posts", key "title": will index all "title" keys in all posts of all users. - * @param path path to the container node - * @param key name of the key to index every container child node - * @param options any additional options - */ - create: (path, key, options) => { - return this.api.createIndex(path, key, options); - }, - /** - * Deletes an existing index from the database - */ - delete: async (filePath) => { - return this.api.deleteIndex(filePath); - }, + async reflect(path, type, args) { + args = args || {}; + const getChildren = async (path, limit = 50, skip = 0, from = null) => { + if (typeof limit === 'string') { + limit = parseInt(limit); + } + if (typeof skip === 'string') { + skip = parseInt(skip); + } + const children = []; // Array<{ key: string | number; type: string; value: any; address?: any }>; + let n = 0, stop = false, more = false; //stop = skip + limit, + await this.storage.getChildren(path, Object.assign({}, (['number', 'string'].includes(typeof from) && { fromKey: from }))) + .next(childInfo => { + if (stop) { + // Stop 1 child too late on purpose to make sure there's more + more = true; + return false; // Stop iterating + } + n++; + // const include = from !== null ? childInfo.key > from : skip === 0 || n > skip; + const include = skip === 0 || n > skip; + if (include) { + children.push(Object.assign({ key: typeof childInfo.key === 'string' ? childInfo.key : childInfo.index, type: childInfo.valueTypeName, value: childInfo.value }, (typeof childInfo.address === 'object' && 'pageNr' in childInfo.address && { + address: { + pageNr: childInfo.address.pageNr, + recordNr: childInfo.address.recordNr, + }, + }))); + } + stop = limit > 0 && children.length === limit; // flag, but don't stop now. Otherwise we won't know if there's more + }) + .catch(err => { + // Node doesn't exist? No children.. + if (!(err instanceof node_errors_js_1.NodeNotFoundError)) { + throw err; + } + }); + return { + more, + list: children, + }; }; + switch (type) { + case 'children': { + const result = await getChildren(path, args.limit, args.skip, args.from); + return result; + } + case 'info': { + const info = { + key: '', + exists: false, + type: 'unknown', + value: undefined, + address: undefined, + children: { + count: 0, + more: false, + list: [], + }, + }; + const nodeInfo = await this.storage.getNodeInfo(path, { include_child_count: args.child_count === true }); + info.key = typeof nodeInfo.key !== 'undefined' ? nodeInfo.key : nodeInfo.index; + info.exists = nodeInfo.exists; + info.type = nodeInfo.exists ? nodeInfo.valueTypeName : undefined; + info.value = nodeInfo.value; + info.address = typeof nodeInfo.address === 'object' && 'pageNr' in nodeInfo.address + ? { + pageNr: nodeInfo.address.pageNr, + recordNr: nodeInfo.address.recordNr, + } + : undefined; + const isObjectOrArray = nodeInfo.exists && nodeInfo.address && [node_value_types_js_1.VALUE_TYPES.OBJECT, node_value_types_js_1.VALUE_TYPES.ARRAY].includes(nodeInfo.type); + if (args.child_count === true) { + // set child count instead of enumerating + info.children = { count: isObjectOrArray ? nodeInfo.childCount : 0 }; + } + else if (typeof args.child_limit === 'number' && args.child_limit > 0) { + if (isObjectOrArray) { + info.children = await getChildren(path, args.child_limit, args.child_skip, args.child_from); + } + } + return info; + } + } } - get schema() { - return { - get: (path) => { - return this.api.getSchema(path); - }, - set: (path, schema, warnOnly = false) => { - return this.api.setSchema(path, schema, warnOnly); - }, - all: () => { - return this.api.getSchemas(); - }, - check: (path, value, isUpdate) => { - return this.api.validateSchema(path, value, isUpdate); - }, - }; + export(path, stream, options = { + format: 'json', + type_safe: true, + }) { + return this.storage.exportNode(path, stream, options); } -} -exports.AceBaseBase = AceBaseBase; - -},{"./data-reference":42,"./debug":44,"./optional-observable":48,"./simple-colors":55,"./simple-event-emitter":56,"./type-mappings":60}],36:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.Api = void 0; -/* eslint-disable @typescript-eslint/no-unused-vars */ -const simple_event_emitter_1 = require("./simple-event-emitter"); -class NotImplementedError extends Error { - constructor(name) { super(`${name} is not implemented`); } -} -/** - * Refactor to type/interface once acebase and acebase-client have been ported to TS - */ -class Api extends simple_event_emitter_1.SimpleEventEmitter { - constructor() { - super(); + import(path, read, options = { + format: 'json', + suppress_events: false, + method: 'set', + }) { + return this.storage.importNode(path, read, options); + } + async setSchema(path, schema, warnOnly = false) { + return this.storage.setSchema(path, schema, warnOnly); + } + async getSchema(path) { + return this.storage.getSchema(path); + } + async getSchemas() { + return this.storage.getSchemas(); + } + async validateSchema(path, value, isUpdate) { + return this.storage.validateSchema(path, value, { updates: isUpdate }); } /** - * Provides statistics - * @param options + * Gets all relevant mutations for specific events on a path and since specified cursor */ - stats(options) { throw new NotImplementedError('stats'); } + async getMutations(filter) { + if (typeof this.storage.getMutations !== 'function') { + throw new Error('Used storage type does not support getMutations'); + } + if (typeof filter !== 'object') { + throw new Error('No filter specified'); + } + if (typeof filter.cursor !== 'string' && typeof filter.timestamp !== 'number') { + throw new Error('No cursor or timestamp given'); + } + return this.storage.getMutations(filter); + } /** - * @param path - * @param event event to subscribe to ("value", "child_added" etc) - * @param callback callback function + * Gets all relevant effective changes for specific events on a path and since specified cursor */ - subscribe(path, event, callback, settings) { throw new NotImplementedError('subscribe'); } - unsubscribe(path, event, callback) { throw new NotImplementedError('unsubscribe'); } - update(path, updates, options) { throw new NotImplementedError('update'); } - set(path, value, options) { throw new NotImplementedError('set'); } - get(path, options) { throw new NotImplementedError('get'); } - transaction(path, callback, options) { throw new NotImplementedError('transaction'); } - exists(path) { throw new NotImplementedError('exists'); } - query(path, query, options) { throw new NotImplementedError('query'); } - reflect(path, type, args) { throw new NotImplementedError('reflect'); } - export(path, write, options) { throw new NotImplementedError('export'); } - import(path, read, options) { throw new NotImplementedError('import'); } - /** Creates an index on key for all child nodes at path */ - createIndex(path, key, options) { throw new NotImplementedError('createIndex'); } - getIndexes() { throw new NotImplementedError('getIndexes'); } - deleteIndex(filePath) { throw new NotImplementedError('deleteIndex'); } - setSchema(path, schema, warnOnly) { throw new NotImplementedError('setSchema'); } - getSchema(path) { throw new NotImplementedError('getSchema'); } - getSchemas() { throw new NotImplementedError('getSchemas'); } - validateSchema(path, value, isUpdate) { throw new NotImplementedError('validateSchema'); } - getMutations(filter) { throw new NotImplementedError('getMutations'); } - getChanges(filter) { throw new NotImplementedError('getChanges'); } + async getChanges(filter) { + if (typeof this.storage.getChanges !== 'function') { + throw new Error('Used storage type does not support getChanges'); + } + if (typeof filter !== 'object') { + throw new Error('No filter specified'); + } + if (typeof filter.cursor !== 'string' && typeof filter.timestamp !== 'number') { + throw new Error('No cursor or timestamp given'); + } + return this.storage.getChanges(filter); + } } -exports.Api = Api; +exports.LocalApi = LocalApi; -},{"./simple-event-emitter":56}],37:[function(require,module,exports){ +},{"./node-errors.js":38,"./node-value-types.js":41,"./query.js":44,"./storage/binary/index.js":45,"./storage/custom/index.js":48,"./storage/mssql/index.js":58,"./storage/sqlite/index.js":59,"acebase-core":12}],31:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.ascii85 = void 0; -function c(input, length, result) { - const b = [0, 0, 0, 0, 0]; - for (let i = 0; i < length; i += 4) { - let n = ((input[i] * 256 + input[i + 1]) * 256 + input[i + 2]) * 256 + input[i + 3]; - if (!n) { - result.push('z'); - } - else { - for (let j = 0; j < 5; b[j++] = n % 85 + 33, n = Math.floor(n / 85)) { } - result.push(String.fromCharCode(b[4], b[3], b[2], b[1], b[0])); - } +exports.assert = void 0; +/** +* Replacement for console.assert, throws an error if condition is not met. +* @param condition 'truthy' condition +* @param error +*/ +function assert(condition, error) { + if (!condition) { + throw new Error(`Assertion failed: ${error !== null && error !== void 0 ? error : 'check your code'}`); } } -function encode(arr) { - // summary: encodes input data in ascii85 string - // input: ArrayLike - const input = arr, result = [], remainder = input.length % 4, length = input.length - remainder; - c(input, length, result); - if (remainder) { - const t = new Uint8Array(4); - t.set(input.slice(length), 0); - c(t, 4, result); - let x = result.pop(); - if (x == 'z') { - x = '!!!!!'; +exports.assert = assert; + +},{}],32:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.AsyncTaskBatch = void 0; +class AsyncTaskBatch { + /** + * Creates a new batch: runs a maximum amount of async tasks simultaniously and waits until they are all resolved. + * If all tasks succeed, returns the results in the same order tasks were added (like `Promise.all` would do), but + * cancels any waiting tasks upon failure of one task. Note that the execution order of tasks added after the set + * limit is unknown. + * @param limit Max amount of async functions to execute simultaniously. Default is `1000` + * @param options Additional options + */ + constructor(limit = 1000, options) { + this.limit = limit; + this.options = options; + this.added = 0; + this.scheduled = []; + this.running = 0; + this.results = []; + this.done = false; + } + async execute(task, index) { + var _a, _b; + try { + this.running++; + const result = await task(); + this.results[index] = result; + this.running--; + if (this.running === 0 && this.scheduled.length === 0) { + // Finished + this.done = true; + (_a = this.doneCallback) === null || _a === void 0 ? void 0 : _a.call(this, this.results); + } + else if (this.scheduled.length > 0) { + // Run next scheduled task + const next = this.scheduled.shift(); + this.execute(next.task, next.index); + } + } + catch (err) { + this.done = true; + (_b = this.errorCallback) === null || _b === void 0 ? void 0 : _b.call(this, err); } - result.push(x.substr(0, remainder + 1)); } - let ret = result.join(''); // String - ret = '<~' + ret + '~>'; - return ret; -} -exports.ascii85 = { - encode: function (arr) { - if (arr instanceof ArrayBuffer) { - arr = new Uint8Array(arr, 0, arr.byteLength); + add(task) { + var _a; + if (this.done) { + throw new Error(`Cannot add to a batch that has already finished. Use wait option and start batch processing manually if you are adding tasks in an async loop`); } - return encode(arr); - }, - decode: function (input) { - // summary: decodes the input string back to an ArrayBuffer - // input: String: the input string to decode - if (!input.startsWith('<~') || !input.endsWith('~>')) { - throw new Error('Invalid input string'); + const index = this.added++; + if (((_a = this.options) === null || _a === void 0 ? void 0 : _a.wait) !== true && this.running < this.limit) { + this.execute(task, index); } - input = input.substr(2, input.length - 4); - const n = input.length, r = [], b = [0, 0, 0, 0, 0]; - let t, x, y, d; - for (let i = 0; i < n; ++i) { - if (input.charAt(i) == 'z') { - r.push(0, 0, 0, 0); - continue; - } - for (let j = 0; j < 5; ++j) { - b[j] = input.charCodeAt(i + j) - 33; - } - d = n - i; - if (d < 5) { - for (let j = d; j < 4; b[++j] = 0) { } - b[d] = 85; - } - t = (((b[0] * 85 + b[1]) * 85 + b[2]) * 85 + b[3]) * 85 + b[4]; - x = t & 255; - t >>>= 8; - y = t & 255; - t >>>= 8; - r.push(t >>> 8, t & 255, y, x); - for (let j = d; j < 5; ++j, r.pop()) { } - i += 4; + else { + this.scheduled.push({ task, index }); } - const data = new Uint8Array(r); - return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength); - }, -}; + } + /** + * Manually starts batch processing, mus be done if the `wait` option was used + */ + start() { + while (this.running < this.limit) { + const next = this.scheduled.shift(); + this.execute(next.task, next.index); + } + } + async finish() { + if (this.running === 0 && this.scheduled.length === 0) { + return this.results; + } + await new Promise((resolve, reject) => { + this.doneCallback = resolve; + this.errorCallback = reject; + }); + return this.results; + } +} +exports.AsyncTaskBatch = AsyncTaskBatch; -},{}],38:[function(require,module,exports){ +},{}],33:[function(require,module,exports){ "use strict"; -var _a, _b; +/** + ________________________________________________________________________________ + + ___ ______ + / _ \ | ___ \ + / /_\ \ ___ ___| |_/ / __ _ ___ ___ + | _ |/ __/ _ \ ___ \/ _` / __|/ _ \ + | | | | (_| __/ |_/ / (_| \__ \ __/ + \_| |_/\___\___\____/ \__,_|___/\___| + realtime database + + Copyright 2018-2022 by Ewout Stortenbeker (me@appy.one) + Published under MIT license + + See docs at https://github.com/appy-one/acebase + ________________________________________________________________________________ + +*/ Object.defineProperty(exports, "__esModule", { value: true }); -const pad_1 = require("../pad"); -const env = typeof window === 'object' ? window : self, globalCount = Object.keys(env).length, mimeTypesLength = (_b = (_a = navigator.mimeTypes) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0, clientId = (0, pad_1.default)((mimeTypesLength - + navigator.userAgent.length).toString(36) - + globalCount.toString(36), 4); -function fingerprint() { - return clientId; +exports.SchemaValidationError = exports.StorageSettings = exports.ICustomStorageNodeMetaData = exports.ICustomStorageNode = exports.CustomStorageHelpers = exports.CustomStorageSettings = exports.CustomStorageTransaction = exports.MSSQLStorageSettings = exports.SQLiteStorageSettings = exports.AceBaseStorageSettings = exports.IndexedDBStorageSettings = exports.LocalStorageSettings = exports.AceBaseLocalSettings = exports.AceBase = exports.PartialArray = exports.proxyAccess = exports.ID = exports.ObjectCollection = exports.TypeMappings = exports.PathReference = exports.EventSubscription = exports.EventStream = exports.DataReferencesArray = exports.DataSnapshotsArray = exports.DataReference = exports.DataSnapshot = void 0; +const acebase_core_1 = require("acebase-core"); +Object.defineProperty(exports, "DataReference", { enumerable: true, get: function () { return acebase_core_1.DataReference; } }); +Object.defineProperty(exports, "DataSnapshot", { enumerable: true, get: function () { return acebase_core_1.DataSnapshot; } }); +Object.defineProperty(exports, "EventSubscription", { enumerable: true, get: function () { return acebase_core_1.EventSubscription; } }); +Object.defineProperty(exports, "PathReference", { enumerable: true, get: function () { return acebase_core_1.PathReference; } }); +Object.defineProperty(exports, "TypeMappings", { enumerable: true, get: function () { return acebase_core_1.TypeMappings; } }); +Object.defineProperty(exports, "ID", { enumerable: true, get: function () { return acebase_core_1.ID; } }); +Object.defineProperty(exports, "proxyAccess", { enumerable: true, get: function () { return acebase_core_1.proxyAccess; } }); +Object.defineProperty(exports, "DataSnapshotsArray", { enumerable: true, get: function () { return acebase_core_1.DataSnapshotsArray; } }); +Object.defineProperty(exports, "ObjectCollection", { enumerable: true, get: function () { return acebase_core_1.ObjectCollection; } }); +Object.defineProperty(exports, "DataReferencesArray", { enumerable: true, get: function () { return acebase_core_1.DataReferencesArray; } }); +Object.defineProperty(exports, "EventStream", { enumerable: true, get: function () { return acebase_core_1.EventStream; } }); +Object.defineProperty(exports, "PartialArray", { enumerable: true, get: function () { return acebase_core_1.PartialArray; } }); +const acebase_local_js_1 = require("./acebase-local.js"); +const acebase_browser_js_1 = require("./acebase-browser.js"); +Object.defineProperty(exports, "AceBase", { enumerable: true, get: function () { return acebase_browser_js_1.BrowserAceBase; } }); +const index_js_1 = require("./storage/custom/index.js"); +const acebase = { + AceBase: acebase_browser_js_1.BrowserAceBase, + AceBaseLocalSettings: acebase_local_js_1.AceBaseLocalSettings, + DataReference: acebase_core_1.DataReference, + DataSnapshot: acebase_core_1.DataSnapshot, + EventSubscription: acebase_core_1.EventSubscription, + PathReference: acebase_core_1.PathReference, + TypeMappings: acebase_core_1.TypeMappings, + CustomStorageSettings: index_js_1.CustomStorageSettings, + CustomStorageTransaction: index_js_1.CustomStorageTransaction, + CustomStorageHelpers: index_js_1.CustomStorageHelpers, + ID: acebase_core_1.ID, + proxyAccess: acebase_core_1.proxyAccess, + DataSnapshotsArray: acebase_core_1.DataSnapshotsArray, +}; +if (typeof window !== 'undefined') { + // Expose classes to window.acebase: + window.acebase = acebase; + // Expose BrowserAceBase class as window.AceBase: + window.AceBase = acebase_browser_js_1.BrowserAceBase; } -exports.default = fingerprint; +// Expose classes for module imports: +exports.default = acebase; +var acebase_local_js_2 = require("./acebase-local.js"); +Object.defineProperty(exports, "AceBaseLocalSettings", { enumerable: true, get: function () { return acebase_local_js_2.AceBaseLocalSettings; } }); +Object.defineProperty(exports, "LocalStorageSettings", { enumerable: true, get: function () { return acebase_local_js_2.LocalStorageSettings; } }); +Object.defineProperty(exports, "IndexedDBStorageSettings", { enumerable: true, get: function () { return acebase_local_js_2.IndexedDBStorageSettings; } }); +var index_js_2 = require("./storage/binary/index.js"); +Object.defineProperty(exports, "AceBaseStorageSettings", { enumerable: true, get: function () { return index_js_2.AceBaseStorageSettings; } }); +var index_js_3 = require("./storage/sqlite/index.js"); +Object.defineProperty(exports, "SQLiteStorageSettings", { enumerable: true, get: function () { return index_js_3.SQLiteStorageSettings; } }); +var index_js_4 = require("./storage/mssql/index.js"); +Object.defineProperty(exports, "MSSQLStorageSettings", { enumerable: true, get: function () { return index_js_4.MSSQLStorageSettings; } }); +var index_js_5 = require("./storage/custom/index.js"); +Object.defineProperty(exports, "CustomStorageTransaction", { enumerable: true, get: function () { return index_js_5.CustomStorageTransaction; } }); +Object.defineProperty(exports, "CustomStorageSettings", { enumerable: true, get: function () { return index_js_5.CustomStorageSettings; } }); +Object.defineProperty(exports, "CustomStorageHelpers", { enumerable: true, get: function () { return index_js_5.CustomStorageHelpers; } }); +Object.defineProperty(exports, "ICustomStorageNode", { enumerable: true, get: function () { return index_js_5.ICustomStorageNode; } }); +Object.defineProperty(exports, "ICustomStorageNodeMetaData", { enumerable: true, get: function () { return index_js_5.ICustomStorageNodeMetaData; } }); +var index_js_6 = require("./storage/index.js"); +Object.defineProperty(exports, "StorageSettings", { enumerable: true, get: function () { return index_js_6.StorageSettings; } }); +Object.defineProperty(exports, "SchemaValidationError", { enumerable: true, get: function () { return index_js_6.SchemaValidationError; } }); -},{"../pad":40}],39:[function(require,module,exports){ +},{"./acebase-browser.js":28,"./acebase-local.js":29,"./storage/binary/index.js":45,"./storage/custom/index.js":48,"./storage/index.js":56,"./storage/mssql/index.js":58,"./storage/sqlite/index.js":59,"acebase-core":12}],34:[function(require,module,exports){ "use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ArrayIndex = exports.GeoIndex = exports.FullTextIndex = exports.DataIndex = void 0; +const not_supported_js_1 = require("../not-supported.js"); /** - * cuid.js - * Collision-resistant UID generator for browsers and node. - * Sequential for fast db lookups and recency sorting. - * Safe for element IDs and server-side lookups. - * - * Extracted from CLCTR - * - * Copyright (c) Eric Elliott 2012 - * MIT License - * - * time biasing added by Ewout Stortenbeker for AceBase + * Not supported in browser context */ -Object.defineProperty(exports, "__esModule", { value: true }); -const fingerprint_1 = require("./fingerprint"); -const pad_1 = require("./pad"); -let c = 0; -const blockSize = 4, base = 36, discreteValues = Math.pow(base, blockSize); -function randomBlock() { - return (0, pad_1.default)((Math.random() * discreteValues << 0).toString(base), blockSize); +class DataIndex extends not_supported_js_1.NotSupported { } -function safeCounter() { - c = c < discreteValues ? c : 0; - c++; // this is not subliminal - return c - 1; +exports.DataIndex = DataIndex; +/** + * Not supported in browser context + */ +class FullTextIndex extends not_supported_js_1.NotSupported { } -function cuid(timebias = 0) { - // Starting with a lowercase letter makes - // it HTML element ID friendly. - const letter = 'c', // hard-coded allows for sequential access - // timestamp - // warning: this exposes the exact date and time - // that the uid was created. - // NOTES Ewout: - // - added timebias - // - at '2059/05/25 19:38:27.456', timestamp will become 1 character larger! - timestamp = (new Date().getTime() + timebias).toString(base), - // Prevent same-machine collisions. - counter = (0, pad_1.default)(safeCounter().toString(base), blockSize), - // A few chars to generate distinct ids for different - // clients (so different computers are far less - // likely to generate the same id) - print = (0, fingerprint_1.default)(), - // Grab some more chars from Math.random() - random = randomBlock() + randomBlock(); - return letter + timestamp + counter + print + random; +exports.FullTextIndex = FullTextIndex; +/** + * Not supported in browser context + */ +class GeoIndex extends not_supported_js_1.NotSupported { } -exports.default = cuid; -// Not using slugs, removed code - -},{"./fingerprint":38,"./pad":40}],40:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -function pad(num, size) { - const s = '000000000' + num; - return s.substr(s.length - size); +exports.GeoIndex = GeoIndex; +/** + * Not supported in browser context + */ +class ArrayIndex extends not_supported_js_1.NotSupported { } -exports.default = pad; +exports.ArrayIndex = ArrayIndex; -},{}],41:[function(require,module,exports){ +},{"../not-supported.js":42}],35:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.OrderedCollectionProxy = exports.proxyAccess = exports.LiveDataProxy = void 0; -const utils_1 = require("./utils"); -const data_reference_1 = require("./data-reference"); -const data_snapshot_1 = require("./data-snapshot"); -const path_reference_1 = require("./path-reference"); -const id_1 = require("./id"); -const optional_observable_1 = require("./optional-observable"); -const process_1 = require("./process"); -const path_info_1 = require("./path-info"); -const simple_event_emitter_1 = require("./simple-event-emitter"); -class RelativeNodeTarget extends Array { - static areEqual(t1, t2) { - return t1.length === t2.length && t1.every((key, i) => t2[i] === key); - } - static isAncestor(ancestor, other) { - return ancestor.length < other.length && ancestor.every((key, i) => other[i] === key); - } - static isDescendant(descendant, other) { - return descendant.length > other.length && other.every((key, i) => descendant[i] === key); - } -} -const isProxy = Symbol('isProxy'); -class LiveDataProxy { - /** - * Creates a live data proxy for the given reference. The data of the reference's path will be loaded, and kept in-sync - * with live data by listening for 'mutations' events. Any changes made to the value by the client will be synced back - * to the database. - * @param ref DataReference to create proxy for. - * @param options proxy initialization options - * be written to the database. - */ - static async create(ref, options) { - var _a; - ref = new data_reference_1.DataReference(ref.db, ref.path); // Use copy to prevent context pollution on original reference - let cache, loaded = false; - let latestCursor = options === null || options === void 0 ? void 0 : options.cursor; - let proxy; - const proxyId = id_1.ID.generate(); //ref.push().key; - // let onMutationCallback: ProxyObserveMutationsCallback; - // let onErrorCallback: ProxyObserveErrorCallback = err => { - // console.error(err.message, err.details); - // }; - const clientSubscriptions = []; - const clientEventEmitter = new simple_event_emitter_1.SimpleEventEmitter(); - clientEventEmitter.on('cursor', (cursor) => latestCursor = cursor); - clientEventEmitter.on('error', (err) => { - console.error(err.message, err.details); +exports.NetIPCServer = exports.IPCSocketPeer = exports.RemoteIPCPeer = exports.IPCPeer = void 0; +const acebase_core_1 = require("acebase-core"); +const ipc_js_1 = require("./ipc.js"); +const not_supported_js_1 = require("../not-supported.js"); +Object.defineProperty(exports, "RemoteIPCPeer", { enumerable: true, get: function () { return not_supported_js_1.NotSupported; } }); +Object.defineProperty(exports, "IPCSocketPeer", { enumerable: true, get: function () { return not_supported_js_1.NotSupported; } }); +Object.defineProperty(exports, "NetIPCServer", { enumerable: true, get: function () { return not_supported_js_1.NotSupported; } }); +/** + * Browser tabs IPC. Database changes and events will be synchronized automatically. + * Locking of resources will be done by the election of a single locking master: + * the one with the lowest id. + */ +class IPCPeer extends ipc_js_1.AceBaseIPCPeer { + constructor(storage) { + super(storage, acebase_core_1.ID.generate()); + this.masterPeerId = this.id; // We don't know who the master is yet... + this.ipcType = 'browser.bcc'; + // Setup process exit handler + // Monitor onbeforeunload event to say goodbye when the window is closed + addEventListener('beforeunload', () => { + this.exit(); }); - const applyChange = (keys, newValue) => { - // Make changes to cache - if (keys.length === 0) { - cache = newValue; - return true; - } - const allowCreation = false; //cache === null; // If the proxy'd target did not exist upon load, we must allow it to be created now. - if (allowCreation) { - cache = typeof keys[0] === 'number' ? [] : {}; - } - let target = cache; - const trailKeys = keys.slice(); - while (trailKeys.length > 1) { - const key = trailKeys.shift(); - if (!(key in target)) { - if (allowCreation) { - target[key] = typeof key === 'number' ? [] : {}; - } - else { - // Have we missed an event, or are local pending mutations creating this conflict? - return false; // Do not proceed + // Create BroadcastChannel to allow multi-tab communication + // This allows other tabs to make changes to the database, notifying us of those changes. + if (typeof BroadcastChannel !== 'undefined') { + this.channel = new BroadcastChannel(`acebase:${storage.name}`); + } + else if (typeof localStorage !== 'undefined') { + // Use localStorage as polyfill for Safari & iOS WebKit + const listeners = [null]; // first callback reserved for onmessage handler + const notImplemented = () => { throw new Error('Not implemented'); }; + this.channel = { + name: `acebase:${storage.name}`, + postMessage: (message) => { + const messageId = acebase_core_1.ID.generate(), key = `acebase:${storage.name}:${this.id}:${messageId}`, payload = JSON.stringify(acebase_core_1.Transport.serialize(message)); + // Store message, triggers 'storage' event in other tabs + localStorage.setItem(key, payload); + // Remove after 10ms + setTimeout(() => localStorage.removeItem(key), 10); + }, + set onmessage(handler) { listeners[0] = handler; }, + set onmessageerror(handler) { notImplemented(); }, + close() { notImplemented(); }, + addEventListener(event, callback) { + if (event !== 'message') { + notImplemented(); } + listeners.push(callback); + }, + removeEventListener(event, callback) { + const i = listeners.indexOf(callback); + i >= 1 && listeners.splice(i, 1); + }, + dispatchEvent(event) { + listeners.forEach(callback => { + try { + callback && callback(event); + } + catch (err) { + console.error(err); + } + }); + return true; + }, + }; + // Listen for storage events to intercept possible messages + addEventListener('storage', event => { + const [acebase, dbname, peerId, messageId] = event.key.split(':'); + if (acebase !== 'acebase' || dbname !== storage.name || peerId === this.id || event.newValue === null) { + return; } - target = target[key]; - } - const prop = trailKeys.shift(); - if (newValue === null) { - // Remove it - target instanceof Array ? target.splice(prop, 1) : delete target[prop]; - } - else { - // Set or update it - target[prop] = newValue; - } - return true; - }; - // Subscribe to mutations events on the target path - const syncFallback = async () => { - if (!loaded) { - return; - } - await reload(); - }; - const subscription = ref.on('mutations', { syncFallback }).subscribe(async (snap) => { - var _a; - if (!loaded) { + const message = acebase_core_1.Transport.deserialize(JSON.parse(event.newValue)); + this.channel.dispatchEvent({ data: message }); + }); + } + else { + // No localStorage either, this is probably an old browser running in a webworker + this.logger.warn(`[BroadcastChannel] not supported`); + this.sendMessage = () => { }; + return; + } + // Monitor incoming messages + this.channel.addEventListener('message', async (event) => { + const message = event.data; + if (message.to && message.to !== this.id) { + // Message is for somebody else. Ignore return; } - const context = snap.context(); - const isRemote = ((_a = context.acebase_proxy) === null || _a === void 0 ? void 0 : _a.id) !== proxyId; - if (!isRemote) { - return; // Update was done through this proxy, no need to update cache or trigger local value subscriptions - } - const mutations = snap.val(false); - const proceed = mutations.every(mutation => { - if (!applyChange(mutation.target, mutation.val)) { - return false; - } - // if (onMutationCallback) { - const changeRef = mutation.target.reduce((ref, key) => ref.child(key), ref); - const changeSnap = new data_snapshot_1.DataSnapshot(changeRef, mutation.val, false, mutation.prev, snap.context()); - // onMutationCallback(changeSnap, isRemote); // onMutationCallback uses try/catch for client callback - clientEventEmitter.emit('mutation', { snapshot: changeSnap, isRemote }); - // } - return true; - }); - if (proceed) { - clientEventEmitter.emit('cursor', context.acebase_cursor); // // NOTE: cursor is only present in mutations done remotely. For our own updates, server cursors are returned by ref.set and ref.update - localMutationsEmitter.emit('mutations', { origin: 'remote', snap }); + this.logger.trace(`[BroadcastChannel] received: `, message); + if (message.type === 'hello' && message.from < this.masterPeerId) { + // This peer was created before other peer we thought was the master + this.masterPeerId = message.from; + this.logger.info(`[BroadcastChannel] Tab ${this.masterPeerId} is the master.`); } - else { - console.warn(`Cached value of live data proxy on "${ref.path}" appears outdated, will be reloaded`); - await reload(); + else if (message.type === 'bye' && message.from === this.masterPeerId) { + // The master tab is leaving + this.logger.info(`[BroadcastChannel] Master tab ${this.masterPeerId} is leaving`); + // Elect new master + const allPeerIds = this.peers.map(peer => peer.id).concat(this.id).filter(id => id !== this.masterPeerId); // All peers, including us, excluding the leaving master peer + this.masterPeerId = allPeerIds.sort()[0]; + this.logger.info(`[BroadcastChannel] ${this.masterPeerId === this.id ? 'We are' : `tab ${this.masterPeerId} is`} the new master. Requesting ${this._locks.length} locks (${this._locks.filter(r => !r.granted).length} pending)`); + // Let the new master take over any locks and lock requests. + const requests = this._locks.splice(0); // Copy and clear current lock requests before granted locks are requested again. + // Request previously granted locks again + await Promise.all(requests.filter(req => req.granted).map(async (req) => { + // Prevent race conditions: if the existing lock is released or moved to parent before it was + // moved to the new master peer, we'll resolve their promises after releasing/moving the new lock + let released, movedToParent; + req.lock.release = () => { return new Promise(resolve => released = resolve); }; + req.lock.moveToParent = () => { return new Promise(resolve => movedToParent = resolve); }; + // Request lock again: + const lock = await this.lock({ path: req.lock.path, write: req.lock.forWriting, tid: req.lock.tid, comment: req.lock.comment }); + if (movedToParent) { + const newLock = await lock.moveToParent(); + movedToParent(newLock); + } + if (released) { + await lock.release(); + released(); + } + })); + // Now request pending locks again + await Promise.all(requests.filter(req => !req.granted).map(async (req) => { + await this.lock(req.request); + })); } + return this.handleMessage(message); }); - // Setup updating functionality: enqueue all updates, process them at next tick in the order they were issued - let processPromise = Promise.resolve(); - const mutationQueue = []; - const transactions = []; - const pushLocalMutations = async () => { - // Sync all local mutations that are not in a transaction - const mutations = []; - for (let i = 0, m = mutationQueue[0]; i < mutationQueue.length; i++, m = mutationQueue[i]) { - if (!transactions.find(t => RelativeNodeTarget.areEqual(t.target, m.target) || RelativeNodeTarget.isAncestor(t.target, m.target))) { - mutationQueue.splice(i, 1); - i--; - mutations.push(m); - } - } - if (mutations.length === 0) { + // // Schedule periodic "pulse" to let others know we're still around + // setInterval(() => { + // sendMessage({ from: tabId, type: 'pulse' }); + // }, 30000); + // Send hello to other peers + const helloMsg = { type: 'hello', from: this.id, data: undefined }; + this.sendMessage(helloMsg); + } + sendMessage(message) { + this.logger.trace(`[BroadcastChannel] sending: `, message); + this.channel.postMessage(message); + } +} +exports.IPCPeer = IPCPeer; + +},{"../not-supported.js":42,"./ipc.js":36,"acebase-core":12}],36:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.AceBaseIPCPeer = exports.AceBaseIPCPeerExitingError = void 0; +const acebase_core_1 = require("acebase-core"); +const node_lock_js_1 = require("../node-lock.js"); +class AceBaseIPCPeerExitingError extends Error { + constructor(message) { super(`Exiting: ${message}`); } +} +exports.AceBaseIPCPeerExitingError = AceBaseIPCPeerExitingError; +/** + * Base class for Inter Process Communication, enables vertical scaling: using more CPU's on the same machine to share workload. + * These processes will have to communicate with eachother because they are reading and writing to the same database file + */ +class AceBaseIPCPeer extends acebase_core_1.SimpleEventEmitter { + get isMaster() { return this.masterPeerId === this.id; } + constructor(storage, id, dbname = storage.name) { + super(); + this.storage = storage; + this.id = id; + this.dbname = dbname; + this.ipcType = 'ipc'; + this.ourSubscriptions = []; + this.remoteSubscriptions = []; + this.peers = []; + this._exiting = false; + this._locks = []; + this._requests = new Map(); + this._eventsEnabled = true; + this._nodeLocker = new node_lock_js_1.NodeLocker(storage.logger, storage.settings.lockTimeout); + this.logger = storage.logger; + // Setup db event listeners + storage.on('subscribe', (subscription) => { + // Subscription was added to db + // this.logger.trace(`database subscription being added on peer ${this.id}`); + const remoteSubscription = this.remoteSubscriptions.find(sub => sub.callback === subscription.callback); + if (remoteSubscription) { + // Send ack + // return sendMessage({ type: 'subscribe_ack', from: tabId, to: remoteSubscription.for, data: { path: subscription.path, event: subscription.event } }); return; } - // Add current (new) values to mutations - mutations.forEach(mutation => { - mutation.value = (0, utils_1.cloneObject)(getTargetValue(cache, mutation.target)); + const othersAlreadyNotifying = this.ourSubscriptions.some(sub => sub.event === subscription.event && sub.path === subscription.path); + // Add subscription + this.ourSubscriptions.push(subscription); + if (othersAlreadyNotifying) { + // Same subscription as other previously added. Others already know we want to be notified + return; + } + // Request other tabs to keep us updated of this event + const message = { type: 'subscribe', from: this.id, data: { path: subscription.path, event: subscription.event } }; + this.sendMessage(message); + }); + storage.on('unsubscribe', (subscription) => { + // Subscription was removed from db + const remoteSubscription = this.remoteSubscriptions.find(sub => sub.callback === subscription.callback); + if (remoteSubscription) { + // Remove + this.remoteSubscriptions.splice(this.remoteSubscriptions.indexOf(remoteSubscription), 1); + // Send ack + // return sendMessage({ type: 'unsubscribe_ack', from: tabId, to: remoteSubscription.for, data: { path: subscription.path, event: subscription.event } }); + return; + } + this.ourSubscriptions + .filter(sub => sub.path === subscription.path && (!subscription.event || sub.event === subscription.event) && (!subscription.callback || sub.callback === subscription.callback)) + .forEach(sub => { + // Remove from our subscriptions + this.ourSubscriptions.splice(this.ourSubscriptions.indexOf(sub), 1); + // Request other tabs to stop notifying + const message = { type: 'unsubscribe', from: this.id, data: { path: sub.path, event: sub.event } }; + this.sendMessage(message); }); - // Run local onMutation & onChange callbacks in the next tick - process_1.default.nextTick(() => { - // Run onMutation callback for each changed node - const context = { acebase_proxy: { id: proxyId, source: 'update' } }; - // if (onMutationCallback) { - mutations.forEach(mutation => { - const mutationRef = mutation.target.reduce((ref, key) => ref.child(key), ref); - const mutationSnap = new data_snapshot_1.DataSnapshot(mutationRef, mutation.value, false, mutation.previous, context); - // onMutationCallback(mutationSnap, false); - clientEventEmitter.emit('mutation', { snapshot: mutationSnap, isRemote: false }); - }); - // } - // Notify local subscribers - const snap = new data_snapshot_1.MutationsDataSnapshot(ref, mutations.map(m => ({ target: m.target, val: m.value, prev: m.previous })), context); - localMutationsEmitter.emit('mutations', { origin: 'local', snap }); + }); + } + /** + * Requests the peer to shut down. Resolves once its locks are cleared and 'exit' event has been emitted. + * Has to be overridden by the IPC implementation to perform custom shutdown tasks + * @param code optional exit code (eg one provided by SIGINT event) + */ + async exit(code = 0) { + if (this._exiting) { + // Already exiting... + return this.once('exit'); + } + this._exiting = true; + this.logger.warn(`Received ${this.isMaster ? 'master' : 'worker ' + this.id} process exit request`); + if (this._locks.length > 0) { + this.logger.warn(`Waiting for ${this.isMaster ? 'master' : 'worker'} ${this.id} locks to clear`); + await this.once('locks-cleared'); + } + // Send "bye" + this.sayGoodbye(this.id); + this.logger.warn(`${this.isMaster ? 'Master' : 'Worker ' + this.id} will now exit`); + this.emitOnce('exit', code); + } + sayGoodbye(forPeerId) { + // Send "bye" message on their behalf + const bye = { type: 'bye', from: forPeerId, data: undefined }; + this.sendMessage(bye); + } + addPeer(id, sendReply = true) { + if (this._exiting) { + return; + } + const peer = this.peers.find(w => w.id === id); + if (!peer) { + this.peers.push({ id, lastSeen: Date.now() }); + } + if (sendReply) { + // Send hello back to sender + const helloMessage = { type: 'hello', from: this.id, to: id, data: undefined }; + this.sendMessage(helloMessage); + // Send our active subscriptions through + this.ourSubscriptions.forEach(sub => { + // Request to keep us updated + const message = { type: 'subscribe', from: this.id, to: id, data: { path: sub.path, event: sub.event } }; + this.sendMessage(message); }); - // Update database async - // const batchId = ID.generate(); - processPromise = mutations - .reduce((mutations, m, i, arr) => { - // Only keep top path mutations to prevent unneccessary child path updates - if (!arr.some(other => RelativeNodeTarget.isAncestor(other.target, m.target))) { - mutations.push(m); + } + } + removePeer(id, ignoreUnknown = false) { + if (this._exiting) { + return; + } + const peer = this.peers.find(peer => peer.id === id); + if (!peer) { + if (!ignoreUnknown) { + throw new Error(`We are supposed to know this peer!`); + } + return; + } + this.peers.splice(this.peers.indexOf(peer), 1); + // Remove their subscriptions + const subscriptions = this.remoteSubscriptions.filter(sub => sub.for === id); + subscriptions.forEach(sub => { + // Remove & stop their subscription + this.remoteSubscriptions.splice(this.remoteSubscriptions.indexOf(sub), 1); + this.storage.subscriptions.remove(sub.path, sub.event, sub.callback); + }); + } + addRemoteSubscription(peerId, details) { + if (this._exiting) { + return; + } + // this.logger.debug(`remote subscription being added`); + if (this.remoteSubscriptions.some(sub => sub.for === peerId && sub.event === details.event && sub.path === details.path)) { + // We're already serving this event for the other peer. Ignore + return; + } + // Add remote subscription + const subscribeCallback = (err, path, val, previous, context) => { + // db triggered an event, send notification to remote subscriber + const eventMessage = { + type: 'event', + from: this.id, + to: peerId, + path: details.path, + event: details.event, + data: { + path, + val, + previous, + context, + }, + }; + this.sendMessage(eventMessage); + }; + this.remoteSubscriptions.push({ for: peerId, event: details.event, path: details.path, callback: subscribeCallback }); + this.storage.subscriptions.add(details.path, details.event, subscribeCallback); + } + cancelRemoteSubscription(peerId, details) { + // Other tab requests to remove previously subscribed event + const sub = this.remoteSubscriptions.find(sub => sub.for === peerId && sub.event === details.event && sub.path === details.event); + if (!sub) { + // We don't know this subscription so we weren't notifying in the first place. Ignore + return; + } + // Stop subscription + this.storage.subscriptions.remove(details.path, details.event, sub.callback); + } + async handleMessage(message) { + switch (message.type) { + case 'hello': return this.addPeer(message.from, message.to !== this.id); + case 'bye': return this.removePeer(message.from, true); + case 'subscribe': return this.addRemoteSubscription(message.from, message.data); + case 'unsubscribe': return this.cancelRemoteSubscription(message.from, message.data); + case 'event': { + if (!this._eventsEnabled) { + // IPC event handling is disabled for this client. Ignore message. + break; } - return mutations; - }, []) - .reduce((updates, m) => { - // Prepare db updates - const target = m.target; - if (target.length === 0) { - // Overwrite this proxy's root value - updates.push({ ref, target, value: cache, type: 'set', previous: m.previous }); + const eventMessage = message; + const context = eventMessage.data.context || {}; + context.acebase_ipc = { type: this.ipcType, origin: eventMessage.from }; // Add IPC details + // Other peer raised an event we are monitoring + const subscriptions = this.ourSubscriptions.filter(sub => sub.event === eventMessage.event && sub.path === eventMessage.path); + subscriptions.forEach(sub => { + sub.callback(null, eventMessage.data.path, eventMessage.data.val, eventMessage.data.previous, context); + }); + break; + } + case 'lock-request': { + // Lock request sent by worker to master + if (!this.isMaster) { + throw new Error(`Workers are not supposed to receive lock requests!`); } - else { - const parentTarget = target.slice(0, -1); - const key = target.slice(-1)[0]; - const parentRef = parentTarget.reduce((ref, key) => ref.child(key), ref); - const parentUpdate = updates.find(update => update.ref.path === parentRef.path); - const cacheValue = getTargetValue(cache, target); // m.value? - const prevValue = m.previous; - if (parentUpdate) { - parentUpdate.value[key] = cacheValue; - parentUpdate.previous[key] = prevValue; - } - else { - updates.push({ ref: parentRef, target: parentTarget, value: { [key]: cacheValue }, type: 'update', previous: { [key]: prevValue } }); - } + const request = message; + const result = { type: 'lock-result', id: request.id, from: this.id, to: request.from, ok: true, data: undefined }; + try { + const lock = await this.lock(request.data); + result.data = { + id: lock.id, + path: lock.path, + tid: lock.tid, + write: lock.forWriting, + expires: lock.expires, + comment: lock.comment, + }; } - return updates; - }, []) - .reduce(async (promise, update /*, i, updates */) => { - // Execute db update - // i === 0 && console.log(`Proxy: processing ${updates.length} db updates to paths:`, updates.map(update => update.ref.path)); - const context = { - acebase_proxy: { - id: proxyId, - source: update.type, - // update_id: ID.generate(), - // batch_id: batchId, - // batch_updates: updates.length - }, - }; - await promise; - await update.ref - .context(context)[update.type](update.value) // .set or .update - .catch(async (err) => { - // console.warn(`Proxy could not update DB, should rollback (${update.type}) the proxy value of "${update.ref.path}" to: `, update.previous); - if (options === null || options === void 0 ? void 0 : options.shouldRollback) { - const rollback = await options.shouldRollback(err, { type: update.type, ref: update.ref, value: update.value, previous: update.previous }); - if (rollback === false) { - // Cancel rollback - return; - } - } - clientEventEmitter.emit('error', { source: 'update', message: `Error processing update of "/${ref.path}"`, details: err }); - const context = { acebase_proxy: { id: proxyId, source: 'update-rollback' } }; - const mutations = []; - if (update.type === 'set') { - setTargetValue(cache, update.target, update.previous); - const mutationSnap = new data_snapshot_1.DataSnapshot(update.ref, update.previous, false, update.value, context); - clientEventEmitter.emit('mutation', { snapshot: mutationSnap, isRemote: false }); - mutations.push({ target: update.target, val: update.previous, prev: update.value }); - } - else { - // update - Object.keys(update.previous).forEach(key => { - setTargetValue(cache, update.target.concat(key), update.previous[key]); - const mutationSnap = new data_snapshot_1.DataSnapshot(update.ref.child(key), update.previous[key], false, update.value[key], context); - clientEventEmitter.emit('mutation', { snapshot: mutationSnap, isRemote: false }); - mutations.push({ target: update.target.concat(key), val: update.previous[key], prev: update.value[key] }); - }); - } - // Run onMutation callback for each node being rolled back - mutations.forEach(m => { - const mutationRef = m.target.reduce((ref, key) => ref.child(key), ref); - const mutationSnap = new data_snapshot_1.DataSnapshot(mutationRef, m.val, false, m.prev, context); - clientEventEmitter.emit('mutation', { snapshot: mutationSnap, isRemote: false }); - }); - // Notify local subscribers: - const snap = new data_snapshot_1.MutationsDataSnapshot(update.ref, mutations, context); - localMutationsEmitter.emit('mutations', { origin: 'local', snap }); - }); - if (update.ref.cursor) { - // Should also be available in context.acebase_cursor now - clientEventEmitter.emit('cursor', update.ref.cursor); + catch (err) { + result.ok = false; + result.reason = err.stack || err.message || err; } - }, processPromise); - await processPromise; - }; - let syncInProgress = false; - const syncPromises = []; - const syncCompleted = () => { - let resolve; - const promise = new Promise(rs => resolve = rs); - syncPromises.push({ resolve }); - return promise; - }; - let processQueueTimeout = null; - const scheduleSync = () => { - if (!processQueueTimeout) { - processQueueTimeout = setTimeout(async () => { - syncInProgress = true; - processQueueTimeout = null; - await pushLocalMutations(); - syncInProgress = false; - syncPromises.splice(0).forEach(p => p.resolve()); - }, 0); - } - }; - const flagOverwritten = (target) => { - if (!mutationQueue.find(m => RelativeNodeTarget.areEqual(m.target, target))) { - mutationQueue.push({ target, previous: (0, utils_1.cloneObject)(getTargetValue(cache, target)) }); + return this.sendMessage(result); } - // schedule database updates - scheduleSync(); - }; - const localMutationsEmitter = new simple_event_emitter_1.SimpleEventEmitter(); - const addOnChangeHandler = (target, callback) => { - const isObject = (val) => val !== null && typeof val === 'object'; - const mutationsHandler = async (details) => { - var _a; - const { snap, origin } = details; - const context = snap.context(); - const causedByOurProxy = ((_a = context.acebase_proxy) === null || _a === void 0 ? void 0 : _a.id) === proxyId; - if (details.origin === 'remote' && causedByOurProxy) { - // Any local changes already triggered subscription callbacks - console.error('DEV ISSUE: mutationsHandler was called from remote event originating from our own proxy'); - return; + case 'lock-result': { + // Lock result sent from master to worker + if (this.isMaster) { + throw new Error(`Masters are not supposed to receive results for lock requests!`); } - const mutations = snap.val(false).filter(mutation => { - // Keep mutations impacting the subscribed target: mutations on target, or descendant or ancestor of target - return mutation.target.slice(0, target.length).every((key, i) => target[i] === key); - }); - if (mutations.length === 0) { - return; + const result = message; + const request = this._requests.get(result.id); + if (typeof request !== 'object') { + throw new Error(`The request must be known to us!`); } - let newValue, previousValue; - // If there is a mutation on the target itself, or parent/ancestor path, there can only be one. We can take a shortcut - const singleMutation = mutations.find(m => m.target.length <= target.length); - if (singleMutation) { - const trailKeys = target.slice(singleMutation.target.length); - newValue = trailKeys.reduce((val, key) => !isObject(val) || !(key in val) ? null : val[key], singleMutation.val); - previousValue = trailKeys.reduce((val, key) => !isObject(val) || !(key in val) ? null : val[key], singleMutation.prev); + if (result.ok) { + request.resolve(result.data); } else { - // All mutations are on children/descendants of our target - // Construct new & previous values by combining cache and snapshot - const currentValue = getTargetValue(cache, target); - newValue = (0, utils_1.cloneObject)(currentValue); - previousValue = (0, utils_1.cloneObject)(newValue); - mutations.forEach(mutation => { - // mutation.target is relative to proxy root - const trailKeys = mutation.target.slice(target.length); - for (let i = 0, val = newValue, prev = previousValue; i < trailKeys.length; i++) { // arr = PathInfo.getPathKeys(mutationPath).slice(PathInfo.getPathKeys(targetRef.path).length) - const last = i + 1 === trailKeys.length, key = trailKeys[i]; - if (last) { - val[key] = mutation.val; - if (val[key] === null) { - delete val[key]; - } - prev[key] = mutation.prev; - if (prev[key] === null) { - delete prev[key]; - } - } - else { - val = val[key] = key in val ? val[key] : {}; - prev = prev[key] = key in prev ? prev[key] : {}; - } - } - }); + request.reject(new Error(result.reason)); } - process_1.default.nextTick(() => { - // Run callback with read-only (frozen) values in next tick - let keepSubscription = true; - try { - keepSubscription = false !== callback(Object.freeze(newValue), Object.freeze(previousValue), !causedByOurProxy, context); - } - catch (err) { - clientEventEmitter.emit('error', { source: origin === 'remote' ? 'remote_update' : 'local_update', message: 'Error running subscription callback', details: err }); - } - if (keepSubscription === false) { - stop(); - } - }); - }; - localMutationsEmitter.on('mutations', mutationsHandler); - const stop = () => { - localMutationsEmitter.off('mutations', mutationsHandler); - clientSubscriptions.splice(clientSubscriptions.findIndex(cs => cs.stop === stop), 1); - }; - clientSubscriptions.push({ target, stop }); - return { stop }; - }; - const handleFlag = (flag, target, args) => { - if (flag === 'write') { - return flagOverwritten(target); + return; } - else if (flag === 'onChange') { - return addOnChangeHandler(target, args.callback); + case 'unlock-request': { + // lock release request sent from worker to master + if (!this.isMaster) { + throw new Error(`Workers are not supposed to receive unlock requests!`); + } + const request = message; + const result = { type: 'unlock-result', id: request.id, from: this.id, to: request.from, ok: true, data: { id: request.data.id } }; + try { + const lockInfo = this._locks.find(l => { var _a; return ((_a = l.lock) === null || _a === void 0 ? void 0 : _a.id) === request.data.id; }); // this._locks.get(request.data.id); + await lockInfo.lock.release(); //this.unlock(request.data.id); + } + catch (err) { + result.ok = false; + result.reason = err.stack || err.message || err; + } + return this.sendMessage(result); } - else if (flag === 'subscribe' || flag === 'observe') { - const subscribe = (subscriber) => { - const currentValue = getTargetValue(cache, target); - subscriber.next(currentValue); - const subscription = addOnChangeHandler(target, (value /*, previous, isRemote, context */) => { - subscriber.next(value); - }); - return function unsubscribe() { - subscription.stop(); - }; - }; - if (flag === 'subscribe') { - return subscribe; + case 'unlock-result': { + // lock release result sent from master to worker + if (this.isMaster) { + throw new Error(`Masters are not supposed to receive results for unlock requests!`); } - // Try to load Observable - const Observable = (0, optional_observable_1.getObservable)(); - return new Observable(subscribe); + const result = message; + const request = this._requests.get(result.id); + if (typeof request !== 'object') { + throw new Error(`The request must be known to us!`); + } + if (result.ok) { + request.resolve(result.data); + } + else { + request.reject(new Error(result.reason)); + } + return; } - else if (flag === 'transaction') { - const hasConflictingTransaction = transactions.some(t => RelativeNodeTarget.areEqual(target, t.target) || RelativeNodeTarget.isAncestor(target, t.target) || RelativeNodeTarget.isDescendant(target, t.target)); - if (hasConflictingTransaction) { - // TODO: Wait for this transaction to finish, then try again - return Promise.reject(new Error('Cannot start transaction because it conflicts with another transaction')); + case 'move-lock-request': { + // move lock request sent from worker to master + if (!this.isMaster) { + throw new Error(`Workers are not supposed to receive move lock requests!`); } - return new Promise(async (resolve) => { - // If there are pending mutations on target (or deeper), wait until they have been synchronized - const hasPendingMutations = mutationQueue.some(m => RelativeNodeTarget.areEqual(target, m.target) || RelativeNodeTarget.isAncestor(target, m.target)); - if (hasPendingMutations) { - if (!syncInProgress) { - scheduleSync(); - } - await syncCompleted(); + const request = message; + const result = { type: 'lock-result', id: request.id, from: this.id, to: request.from, ok: true, data: undefined }; + try { + let movedLock; + // const lock = this._locks.get(request.data.id); + const lockRequest = this._locks.find(r => { var _a; return ((_a = r.lock) === null || _a === void 0 ? void 0 : _a.id) === request.data.id; }); + if (request.data.move_to === 'parent') { + movedLock = await lockRequest.lock.moveToParent(); } - const tx = { target, status: 'started', transaction: null }; - transactions.push(tx); - tx.transaction = { - get status() { return tx.status; }, - get completed() { return tx.status !== 'started'; }, - get mutations() { - return mutationQueue.filter(m => RelativeNodeTarget.areEqual(tx.target, m.target) || RelativeNodeTarget.isAncestor(tx.target, m.target)); - }, - get hasMutations() { - return this.mutations.length > 0; - }, - async commit() { - if (this.completed) { - throw new Error(`Transaction has completed already (status '${tx.status}')`); - } - tx.status = 'finished'; - transactions.splice(transactions.indexOf(tx), 1); - if (syncInProgress) { - // Currently syncing without our mutations - await syncCompleted(); - } - scheduleSync(); - await syncCompleted(); - }, - rollback() { - // Remove mutations from queue - if (this.completed) { - throw new Error(`Transaction has completed already (status '${tx.status}')`); - } - tx.status = 'canceled'; - const mutations = []; - for (let i = 0; i < mutationQueue.length; i++) { - const m = mutationQueue[i]; - if (RelativeNodeTarget.areEqual(tx.target, m.target) || RelativeNodeTarget.isAncestor(tx.target, m.target)) { - mutationQueue.splice(i, 1); - i--; - mutations.push(m); - } - } - // Replay mutations in reverse order - mutations.reverse() - .forEach(m => { - if (m.target.length === 0) { - cache = m.previous; - } - else { - setTargetValue(cache, m.target, m.previous); - } - }); - // Remove transaction - transactions.splice(transactions.indexOf(tx), 1); - }, + else { + throw new Error(`Unknown lock move_to "${request.data.move_to}"`); + } + // this._locks.delete(request.data.id); + // this._locks.set(movedLock.id, movedLock); + lockRequest.lock = movedLock; + result.data = { + id: movedLock.id, + path: movedLock.path, + tid: movedLock.tid, + write: movedLock.forWriting, + expires: movedLock.expires, + comment: movedLock.comment, }; - resolve(tx.transaction); - }); + } + catch (err) { + result.ok = false; + result.reason = err.stack || err.message || err; + } + return this.sendMessage(result); + } + case 'notification': { + // Custom notification received - raise event + return this.emit('notification', message); + } + case 'request': { + // Custom message received - raise event + return this.emit('request', message); + } + case 'result': { + // Result of custom request received - raise event + const result = message; + const request = this._requests.get(result.id); + if (typeof request !== 'object') { + throw new Error(`Result of unknown request received`); + } + if (result.ok) { + request.resolve(result.data); + } + else { + request.reject(new Error(result.reason)); + } } - }; - const snap = await ref.get({ cache_mode: 'allow', cache_cursor: options === null || options === void 0 ? void 0 : options.cursor }); - // const gotOfflineStartValue = snap.context().acebase_origin === 'cache'; - // if (gotOfflineStartValue) { - // console.warn(`Started data proxy with cached value of "${ref.path}", check if its value is reloaded on next connection!`); - // } - if (snap.context().acebase_origin !== 'cache') { - clientEventEmitter.emit('cursor', (_a = ref.cursor) !== null && _a !== void 0 ? _a : null); // latestCursor = snap.context().acebase_cursor ?? null; } - loaded = true; - cache = snap.val(); - if (cache === null && typeof (options === null || options === void 0 ? void 0 : options.defaultValue) !== 'undefined') { - cache = options.defaultValue; - const context = { - acebase_proxy: { - id: proxyId, - source: 'default', - // update_id: ID.generate() - }, - }; - await ref.context(context).set(cache); + } + /** + * Acquires a lock. If this peer is a worker, it will request the lock from the master + * @param details + */ + async lock(details) { + if (this._exiting) { + // Peer is exiting. Do we have an existing lock with requested tid? If not, deny request. + const tidApproved = this._locks.find(l => l.tid === details.tid && l.granted); + if (!tidApproved) { + // We have no previously granted locks for this transaction. Deny. + throw new AceBaseIPCPeerExitingError('new transaction lock denied because the IPC peer is exiting'); + } } - proxy = createProxy({ root: { ref, get cache() { return cache; } }, target: [], id: proxyId, flag: handleFlag }); - const assertProxyAvailable = () => { - if (proxy === null) { - throw new Error('Proxy was destroyed'); + const removeLock = (lockDetails) => { + this._locks.splice(this._locks.indexOf(lockDetails), 1); + if (this._locks.length === 0) { + // this.logger.debug(`No more locks in worker ${this.id}`); + this.emit('locks-cleared'); } }; - const reload = async () => { - // Manually reloads current value when cache is out of sync, which should only - // be able to happen if an AceBaseClient is used without cache database, - // and the connection to the server was lost for a while. In all other cases, - // there should be no need to call this method. - assertProxyAvailable(); - mutationQueue.splice(0); // Remove pending mutations. Will be empty in production, but might not be while debugging, leading to weird behaviour. - const snap = await ref.get({ allow_cache: false }); - const oldVal = cache, newVal = snap.val(); - cache = newVal; - // Compare old and new values - const mutations = (0, utils_1.getMutations)(oldVal, newVal); - if (mutations.length === 0) { - return; // Nothing changed + if (this.isMaster) { + // Master + const lockInfo = { tid: details.tid, granted: false, request: details, lock: null }; + this._locks.push(lockInfo); + const lock = await this._nodeLocker.lock(details.path, details.tid, details.write, details.comment); + lockInfo.tid = lock.tid; + lockInfo.granted = true; + const createIPCLock = (lock) => { + return { + get id() { return lock.id; }, + get tid() { return lock.tid; }, + get path() { return lock.path; }, + get forWriting() { return lock.forWriting; }, + get expires() { return lock.expires; }, + get comment() { return lock.comment; }, + get state() { return lock.state; }, + release: async () => { + await lock.release(); + removeLock(lockInfo); + }, + moveToParent: async () => { + const parentLock = await lock.moveToParent(); + lockInfo.lock = createIPCLock(parentLock); + return lockInfo.lock; + }, + }; + }; + lockInfo.lock = createIPCLock(lock); + return lockInfo.lock; + } + else { + // Worker + const lockInfo = { tid: details.tid, granted: false, request: details, lock: null }; + this._locks.push(lockInfo); + const createIPCLock = (result) => { + lockInfo.granted = true; + lockInfo.tid = result.tid; + lockInfo.lock = { + id: result.id, + tid: result.tid, + path: result.path, + forWriting: result.write, + state: node_lock_js_1.LOCK_STATE.LOCKED, + expires: result.expires, + comment: result.comment, + release: async () => { + const req = { type: 'unlock-request', id: acebase_core_1.ID.generate(), from: this.id, to: this.masterPeerId, data: { id: lockInfo.lock.id } }; + await this.request(req); + lockInfo.lock.state = node_lock_js_1.LOCK_STATE.DONE; + // this.logger.trace(`Worker ${this.id} released lock ${lockInfo.lock.id} (tid ${lockInfo.lock.tid}, ${lockInfo.lock.comment}, "/${lockInfo.lock.path}", ${lockInfo.lock.forWriting ? 'write' : 'read'})`); + removeLock(lockInfo); + }, + moveToParent: async () => { + const req = { type: 'move-lock-request', id: acebase_core_1.ID.generate(), from: this.id, to: this.masterPeerId, data: { id: lockInfo.lock.id, move_to: 'parent' } }; + let result; + try { + result = await this.request(req); + } + catch (err) { + // We didn't get new lock?! + lockInfo.lock.state = node_lock_js_1.LOCK_STATE.DONE; + removeLock(lockInfo); + throw err; + } + lockInfo.lock = createIPCLock(result); + return lockInfo.lock; + }, + }; + // this.logger.debug(`Worker ${this.id} received lock ${lock.id} (tid ${lock.tid}, ${lock.comment}, "/${lock.path}", ${lock.forWriting ? 'write' : 'read'})`); + return lockInfo.lock; + }; + const req = { type: 'lock-request', id: acebase_core_1.ID.generate(), from: this.id, to: this.masterPeerId, data: details }; + let result, err; + try { + result = await this.request(req); } - // Run onMutation callback for each changed node - const context = snap.context(); // context might contain acebase_cursor if server support that - context.acebase_proxy = { id: proxyId, source: 'reload' }; - // if (onMutationCallback) { - mutations.forEach(m => { - const targetRef = getTargetRef(ref, m.target); - const newSnap = new data_snapshot_1.DataSnapshot(targetRef, m.val, m.val === null, m.prev, context); - clientEventEmitter.emit('mutation', { snapshot: newSnap, isRemote: true }); - }); - // } - // Notify local subscribers - const mutationsSnap = new data_snapshot_1.MutationsDataSnapshot(ref, mutations, context); - localMutationsEmitter.emit('mutations', { origin: 'local', snap: mutationsSnap }); - }; - return { - async destroy() { - await processPromise; - const promises = [ - subscription.stop(), - ...clientSubscriptions.map(cs => cs.stop()), - ]; - await Promise.all(promises); - ['cursor', 'mutation', 'error'].forEach(event => clientEventEmitter.off(event)); - cache = null; // Remove cache - proxy = null; - }, - stop() { - this.destroy(); - }, - get value() { - assertProxyAvailable(); - return proxy; - }, - get hasValue() { - assertProxyAvailable(); - return cache !== null; - }, - set value(val) { - // Overwrite the value of the proxied path itself! - assertProxyAvailable(); - if (val !== null && typeof val === 'object' && val[isProxy]) { - // Assigning one proxied value to another - val = val.valueOf(); - } - flagOverwritten([]); - cache = val; - }, - get ref() { - return ref; - }, - get cursor() { - return latestCursor; - }, - reload, - onMutation(callback) { - // Fires callback each time anything changes - assertProxyAvailable(); - clientEventEmitter.off('mutation'); // Mimic legacy behaviour that overwrites handler - clientEventEmitter.on('mutation', ({ snapshot, isRemote }) => { - try { - callback(snapshot, isRemote); - } - catch (err) { - clientEventEmitter.emit('error', { source: 'mutation_callback', message: 'Error in dataproxy onMutation callback', details: err }); - } - }); - }, - onError(callback) { - // Fires callback each time anything goes wrong - assertProxyAvailable(); - clientEventEmitter.off('error'); // Mimic legacy behaviour that overwrites handler - clientEventEmitter.on('error', (err) => { - try { - callback(err); - } - catch (err) { - console.error(`Error in dataproxy onError callback: ${err.message}`); - } - }); - }, - on(event, callback) { - clientEventEmitter.on(event, callback); - }, - off(event, callback) { - clientEventEmitter.off(event, callback); - }, - }; + catch (e) { + err = e; + result = null; + } + if (err) { + removeLock(lockInfo); + throw err; + } + return createIPCLock(result); + } + } + async request(req) { + // Send request, return result promise + let resolve, reject; + const promise = new Promise((rs, rj) => { + resolve = (result) => { + this._requests.delete(req.id); + rs(result); + }; + reject = (err) => { + this._requests.delete(req.id); + rj(err); + }; + }); + this._requests.set(req.id, { resolve, reject, request: req }); + this.sendMessage(req); + return promise; + } + /** + * Sends a custom request to the IPC master + * @param request + * @returns + */ + sendRequest(request) { + const req = { type: 'request', from: this.id, to: this.masterPeerId, id: acebase_core_1.ID.generate(), data: request }; + return this.request(req) + .catch(err => { + this.logger.error(err); + throw err; + }); + } + replyRequest(requestMessage, result) { + const reply = { type: 'result', id: requestMessage.id, ok: true, from: this.id, to: requestMessage.from, data: result }; + this.sendMessage(reply); + } + /** + * Sends a custom notification to all IPC peers + * @param notification + * @returns + */ + sendNotification(notification) { + const msg = { type: 'notification', from: this.id, data: notification }; + this.sendMessage(msg); + } + /** + * If ipc event handling is currently enabled + */ + get eventsEnabled() { return this._eventsEnabled; } + /** + * Enables or disables ipc event handling. When disabled, incoming event messages will be ignored. + */ + set eventsEnabled(enabled) { + this.logger.info(`ipc events ${enabled ? 'enabled' : 'disabled'}`); + this._eventsEnabled = enabled; } } -exports.LiveDataProxy = LiveDataProxy; -function getTargetValue(obj, target) { - let val = obj; - for (const key of target) { - val = typeof val === 'object' && val !== null && key in val ? val[key] : null; +exports.AceBaseIPCPeer = AceBaseIPCPeer; + +},{"../node-lock.js":40,"acebase-core":12}],37:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.RemovedNodeAddress = exports.NodeAddress = void 0; +class NodeAddress { + constructor(path) { + this.path = path; + } + toString() { + return `"/${this.path}"`; + } + /** + * Compares this address to another address + */ + equals(address) { + return this.path === address.path; } - return val; } -function setTargetValue(obj, target, value) { - if (target.length === 0) { - throw new Error('Cannot update root target, caller must do that itself!'); +exports.NodeAddress = NodeAddress; +class RemovedNodeAddress extends NodeAddress { + constructor(path) { + super(path); } - const targetObject = target.slice(0, -1).reduce((obj, key) => obj[key], obj); - const prop = target.slice(-1)[0]; - if (value === null || typeof value === 'undefined') { - // Remove it - targetObject instanceof Array ? targetObject.splice(prop, 1) : delete targetObject[prop]; + toString() { + return `"/${this.path}" (removed)`; } - else { - // Set or update it - targetObject[prop] = value; + /** + * Compares this address to another address + */ + equals(address) { + return address instanceof RemovedNodeAddress && this.path === address.path; } } -function getTargetRef(ref, target) { - // Create new DataReference to prevent context reuse - const path = path_info_1.PathInfo.get(ref.path).childPath(target); - return new data_reference_1.DataReference(ref.db, path); +exports.RemovedNodeAddress = RemovedNodeAddress; + +},{}],38:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.NodeRevisionError = exports.NodeNotFoundError = void 0; +class NodeNotFoundError extends Error { } -function createProxy(context) { - const targetRef = getTargetRef(context.root.ref, context.target); - const childProxies = []; - const handler = { - get(target, prop, receiver) { - target = getTargetValue(context.root.cache, context.target); - if (typeof prop === 'symbol') { - if (prop.toString() === Symbol.iterator.toString()) { - // Use .values for @@iterator symbol - prop = 'values'; - } - else if (prop.toString() === isProxy.toString()) { - return true; - } - else { - return Reflect.get(target, prop, receiver); - } - } - if (prop === 'valueOf') { - return function valueOf() { return target; }; - } - if (target === null || typeof target !== 'object') { - throw new Error(`Cannot read property "${prop}" of ${target}. Value of path "/${targetRef.path}" is not an object (anymore)`); - } - if (target instanceof Array && typeof prop === 'string' && /^[0-9]+$/.test(prop)) { - // Proxy type definitions say prop can be a number, but this is never the case. - prop = parseInt(prop); - } - const value = target[prop]; - if (value === null) { - // Removed property. Should never happen, but if it does: - delete target[prop]; - return; // undefined +exports.NodeNotFoundError = NodeNotFoundError; +class NodeRevisionError extends Error { +} +exports.NodeRevisionError = NodeRevisionError; + +},{}],39:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.NodeInfo = void 0; +const acebase_core_1 = require("acebase-core"); +const node_value_types_js_1 = require("./node-value-types.js"); +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; + this.childCount = info.childCount; + if (typeof this.path === 'string' && (typeof this.key === 'undefined' && typeof this.index === 'undefined')) { + const pathInfo = acebase_core_1.PathInfo.get(this.path); + if (typeof pathInfo.key === 'number') { + this.index = pathInfo.key; } - // Check if we have a child proxy for this property already. - // If so, and the properties' typeof value did not change, return that - const childProxy = childProxies.find(proxy => proxy.prop === prop); - if (childProxy) { - if (childProxy.typeof === typeof value) { - return childProxy.value; - } - childProxies.splice(childProxies.indexOf(childProxy), 1); + else { + this.key = pathInfo.key; } - const proxifyChildValue = (prop) => { - const value = target[prop]; // - const childProxy = childProxies.find(child => child.prop === prop); - if (childProxy) { - if (childProxy.typeof === typeof value) { - return childProxy.value; - } - childProxies.splice(childProxies.indexOf(childProxy), 1); - } - if (typeof value !== 'object') { - // Can't proxify non-object values - return value; - } - const newChildProxy = createProxy({ root: context.root, target: context.target.concat(prop), id: context.id, flag: context.flag }); - childProxies.push({ typeof: typeof value, prop, value: newChildProxy }); - return newChildProxy; - }; - const unproxyValue = (value) => { - return value !== null && typeof value === 'object' && value[isProxy] - ? value.getTarget() - : value; - }; - // If the property contains a simple value, return it. - if (['string', 'number', 'boolean'].includes(typeof value) - || value instanceof Date - || value instanceof path_reference_1.PathReference - || value instanceof ArrayBuffer - || (typeof value === 'object' && 'buffer' in value) // Typed Arrays - ) { - return value; + } + if (typeof this.exists === 'undefined') { + this.exists = true; + } + } + get valueType() { + return this.type; + } + get valueTypeName() { + return (0, node_value_types_js_1.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.toString()}`; + } + else { + return `"${this.path}" is ${this.valueTypeName} with value ${this.value}`; + } + } +} +exports.NodeInfo = NodeInfo; + +},{"./node-value-types.js":41,"acebase-core":12}],40:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.NodeLock = exports.NodeLocker = exports.NodeLockError = exports.LOCK_STATE = void 0; +const acebase_core_1 = require("acebase-core"); +const assert_js_1 = require("./assert.js"); +const DEBUG_MODE = false; +const DEFAULT_LOCK_TIMEOUT = 120; // in seconds +exports.LOCK_STATE = { + PENDING: 'pending', + LOCKED: 'locked', + EXPIRED: 'expired', + DONE: 'done', +}; +class NodeLockError extends Error { + constructor(message, lock) { + super(message); + this.lock = lock; + } +} +exports.NodeLockError = NodeLockError; +class NodeLocker { + /** + * Provides locking mechanism for nodes, ensures no simultanious read and writes happen to overlapping paths + */ + constructor(logger, lockTimeout = DEFAULT_LOCK_TIMEOUT) { + this.logger = logger; + this._locks = []; + this._lastTid = 0; + this.timeout = lockTimeout * 1000; + } + setTimeout(timeout) { + this.timeout = timeout * 1000; + } + createTid() { + return DEBUG_MODE ? ++this._lastTid : acebase_core_1.ID.generate(); + } + _allowLock(path, tid, forWriting) { + /** + * Disabled path locking because of the following issue: + * + * Process 1 requests WRITE lock on "/users/ewout", is GRANTED + * Process 2 requests READ lock on "", is DENIED (process 1 writing to a descendant) + * Process 3 requests WRITE lock on "/posts/post1", is GRANTED + * Process 1 requests READ lock on "/" because of bound events, is DENIED (3 is writing to a descendant) + * Process 3 requests READ lock on "/" because of bound events, is DENIED (1 is writing to a descendant) + * + * --> DEADLOCK! + * + * Now simply makes sure one transaction has write access at the same time, + * might change again in the future... + */ + const conflict = this._locks + .find(otherLock => { + return (otherLock.tid !== tid + && otherLock.state === exports.LOCK_STATE.LOCKED + && (forWriting || otherLock.forWriting)); + }); + return { allow: !conflict, conflict }; + } + quit() { + return new Promise(resolve => { + if (this._locks.length === 0) { + return resolve(); } - const isArray = target instanceof Array; - if (prop === 'toString') { - return function toString() { - return `[LiveDataProxy for "${targetRef.path}"]`; - }; + this._quit = resolve; + }); + } + /** + * Safely reject a pending lock, catching any unhandled promise rejections (that should not happen in the first place, obviously) + * @param lock + */ + _rejectLock(lock, err) { + this._locks.splice(this._locks.indexOf(lock), 1); // Remove from queue + clearTimeout(lock.timeout); + try { + lock.reject(err); + } + catch (err) { + console.error(`Unhandled promise rejection:`, err); + } + } + _processLockQueue() { + if (this._quit) { + // Reject all pending locks + const quitError = new Error('Quitting'); + this._locks + .filter(lock => lock.state === exports.LOCK_STATE.PENDING) + .forEach(lock => this._rejectLock(lock, quitError)); + // Resolve quit promise if queue is empty: + if (this._locks.length === 0) { + this._quit(); } - if (typeof value === 'undefined') { - if (prop === 'push') { - // Push item to an object collection - return function push(item) { - const childRef = targetRef.push(); - context.flag('write', context.target.concat(childRef.key)); //, { previous: null } - target[childRef.key] = item; - return childRef.key; + } + const pending = this._locks + .filter(lock => lock.state === exports.LOCK_STATE.PENDING) + .sort((a, b) => { + // // Writes get higher priority so all reads get the most recent data + // if (a.forWriting === b.forWriting) { + // if (a.requested < b.requested) { return -1; } + // else { return 1; } + // } + // else if (a.forWriting) { return -1; } + if (a.priority && !b.priority) { + return -1; + } + else if (!a.priority && b.priority) { + return 1; + } + return a.requested - b.requested; + }); + pending.forEach(lock => { + 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(err => this._rejectLock(lock, err)); + } + }); + } + async 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 === exports.LOCK_STATE.EXPIRED)) >= 0) { + const expiredLock = this._locks.find((l => l.tid === tid && l.state === exports.LOCK_STATE.EXPIRED)); + throw new NodeLockError(`lock on tid ${tid} has expired, not allowed to continue`, expiredLock !== null && expiredLock !== void 0 ? expiredLock : null); + } + else if (this._quit && !options.withPriority) { + const refLock = this._locks.find((l => l.tid === tid && l.path === path)); + throw new NodeLockError(`Quitting`, refLock !== null && refLock !== void 0 ? refLock : null); + } + else { + DEBUG_MODE && console.error(`${forWriting ? 'write' : 'read'} lock requested on "${path}" by tid ${tid} (${comment})`); + // // Test the requested lock path + // let duplicateKeys = getPathKeys(path) + // .reduce((r, key) => { + // let i = r.findIndex(c => c.key === key); + // if (i >= 0) { r[i].count++; } + // else { r.push({ key, count: 1 }) } + // return r; + // }, []) + // .filter(c => c.count > 1) + // .map(c => c.key); + // if (duplicateKeys.length > 0) { + // console.log(`ALERT: Duplicate keys found in path "/${path}"`.colorize([ColorStyle.dim, ColorStyle.bgRed]); + // } + 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) { + DEBUG_MODE && console.error(`${lock.forWriting ? 'write' : 'read'} lock ALLOWED on "${lock.path}" by tid ${lock.tid} (${lock.comment})`); + lock.state = exports.LOCK_STATE.LOCKED; + if (typeof lock.granted === 'number') { + //debug.warn(`lock :: ALLOWING ${lock.forWriting ? "write" : "read" } lock on path "/${lock.path}" by tid ${lock.tid}; ${lock.comment}`); + } + else { + lock.granted = Date.now(); + if (options.noTimeout !== true) { + lock.expires = Date.now() + this.timeout; + //debug.warn(`lock :: GRANTED ${lock.forWriting ? "write" : "read" } lock on path "/${lock.path}" by tid ${lock.tid}; ${lock.comment}`); + let timeoutCount = 0; + const timeoutHandler = () => { + // Autorelease timeouts must only fire when there is something wrong in the + // executing (AceBase) code, eg an unhandled promise rejection causing a lock not + // to be released. To guard against programming errors, we will issue 3 warning + // messages before releasing the lock. + if (lock.state !== exports.LOCK_STATE.LOCKED) { + return; + } + timeoutCount++; + if (timeoutCount <= 3) { + // Warn first. + this.logger.warn(`${lock.forWriting ? 'write' : 'read'} lock on "/${lock.path}" is taking long [${timeoutCount}]; tid=${lock.tid} comment=${lock.comment}`); + lock.warned = true; + lock.timeout = setTimeout(timeoutHandler, this.timeout / 4); + return; + } + this.logger.error(`${lock.forWriting ? 'write' : 'read'} lock on "/${lock.path}" expired! tid=${lock.tid} comment=${lock.comment}`); + lock.state = exports.LOCK_STATE.EXPIRED; + // let allTransactionLocks = _locks.filter(l => l.tid === lock.tid).sort((a,b) => a.requested < b.requested ? -1 : 1); + // let transactionsDebug = allTransactionLocks.map(l => `${l.state} ${l.forWriting ? "WRITE" : "read"} ${l.comment}`).join("\n"); + // debug.error(transactionsDebug); + this._processLockQueue(); }; - } - if (prop === 'getTarget') { - // Get unproxied readonly (but still live) version of data. - return function (warn = true) { - warn && console.warn('Use getTarget with caution - any changes will not be synchronized!'); - return target; - }; - } - if (prop === 'getRef') { - // Gets the DataReference to this data target - return function getRef() { - const ref = getTargetRef(context.root.ref, context.target); - return ref; - }; - } - if (prop === 'forEach') { - return function forEach(callback) { - const keys = Object.keys(target); - // Fix: callback with unproxied value - let stop = false; - for (let i = 0; !stop && i < keys.length; i++) { - const key = keys[i]; - const value = proxifyChildValue(key); //, target[key] - stop = callback(value, key, i) === false; - } - }; - } - if (['values', 'entries', 'keys'].includes(prop)) { - return function* generator() { - const keys = Object.keys(target); - for (const key of keys) { - if (prop === 'keys') { - yield key; - } - else { - const value = proxifyChildValue(key); //, target[key] - if (prop === 'entries') { - yield [key, value]; - } - else { - yield value; - } - } - } - }; - } - if (prop === 'toArray') { - return function toArray(sortFn) { - const arr = Object.keys(target).map(key => proxifyChildValue(key)); //, target[key] - if (sortFn) { - arr.sort(sortFn); - } - return arr; - }; - } - if (prop === 'onChanged') { - // Starts monitoring the value - return function onChanged(callback) { - return context.flag('onChange', context.target, { callback }); - }; - } - if (prop === 'subscribe') { - // Gets subscriber function to use with Observables, or custom handling - return function subscribe() { - return context.flag('subscribe', context.target); - }; - } - if (prop === 'getObservable') { - // Creates an observable for monitoring the value - return function getObservable() { - return context.flag('observe', context.target); - }; - } - if (prop === 'getOrderedCollection') { - return function getOrderedCollection(orderProperty, orderIncrement) { - return new OrderedCollectionProxy(this, orderProperty, orderIncrement); - }; - } - if (prop === 'startTransaction') { - return function startTransaction() { - return context.flag('transaction', context.target); - }; - } - if (prop === 'remove' && !isArray) { - // Removes target from object collection - return function remove() { - if (context.target.length === 0) { - throw new Error('Can\'t remove proxy root value'); - } - const parent = getTargetValue(context.root.cache, context.target.slice(0, -1)); - const key = context.target.slice(-1)[0]; - context.flag('write', context.target); - delete parent[key]; - }; - } - return; // undefined - } - else if (typeof value === 'function') { - if (isArray) { - // Handle array methods - const writeArray = (action) => { - context.flag('write', context.target); - return action(); - }; - const cleanArrayValues = (values) => values.map((value) => { - value = unproxyValue(value); - removeVoidProperties(value); - return value; - }); - // Methods that directly change the array: - if (prop === 'push') { - return function push(...items) { - items = cleanArrayValues(items); - return writeArray(() => target.push(...items)); // push the items to the cache array - }; - } - if (prop === 'pop') { - return function pop() { - return writeArray(() => target.pop()); - }; - } - if (prop === 'splice') { - return function splice(start, deleteCount, ...items) { - items = cleanArrayValues(items); - return writeArray(() => target.splice(start, deleteCount, ...items)); - }; - } - if (prop === 'shift') { - return function shift() { - return writeArray(() => target.shift()); - }; - } - if (prop === 'unshift') { - return function unshift(...items) { - items = cleanArrayValues(items); - return writeArray(() => target.unshift(...items)); - }; - } - if (prop === 'sort') { - return function sort(compareFn) { - return writeArray(() => target.sort(compareFn)); - }; - } - if (prop === 'reverse') { - return function reverse() { - return writeArray(() => target.reverse()); - }; - } - // Methods that do not change the array themselves, but - // have callbacks that might, or return child values: - if (['indexOf', 'lastIndexOf'].includes(prop)) { - return function indexOf(item, start) { - if (item !== null && typeof item === 'object' && item[isProxy]) { - // Use unproxied value, or array.indexOf will return -1 (fixes issue #1) - item = item.getTarget(false); - } - return target[prop](item, start); - }; - } - if (['forEach', 'every', 'some', 'filter', 'map'].includes(prop)) { - return function iterate(callback) { - return target[prop]((value, i) => { - return callback(proxifyChildValue(i), i, proxy); //, value - }); - }; - } - if (['reduce', 'reduceRight'].includes(prop)) { - return function reduce(callback, initialValue) { - return target[prop]((prev, value, i) => { - return callback(prev, proxifyChildValue(i), i, proxy); //, value - }, initialValue); - }; - } - if (['find', 'findIndex'].includes(prop)) { - return function find(callback) { - let value = target[prop]((value, i) => { - return callback(proxifyChildValue(i), i, proxy); // , value - }); - if (prop === 'find' && value) { - const index = target.indexOf(value); - value = proxifyChildValue(index); //, value - } - return value; - }; - } - if (['values', 'entries', 'keys'].includes(prop)) { - return function* generator() { - for (let i = 0; i < target.length; i++) { - if (prop === 'keys') { - yield i; - } - else { - const value = proxifyChildValue(i); //, target[i] - if (prop === 'entries') { - yield [i, value]; - } - else { - yield value; - } - } - } - }; - } - } - // Other function (or not an array), should not alter its value - // return function fn(...args) { - // return target[prop](...args); - // } - return value; - } - // Proxify any other value - return proxifyChildValue(prop); //, value - }, - set(target, prop, value, receiver) { - // Eg: chats.chat1.title = 'New chat title'; - // target === chats.chat1, prop === 'title' - target = getTargetValue(context.root.cache, context.target); - if (typeof prop === 'symbol') { - return Reflect.set(target, prop, value, receiver); - } - if (target === null || typeof target !== 'object') { - throw new Error(`Cannot set property "${prop}" of ${target}. Value of path "/${targetRef.path}" is not an object`); - } - if (target instanceof Array && typeof prop === 'string') { - if (!/^[0-9]+$/.test(prop)) { - throw new Error(`Cannot set property "${prop}" on array value of path "/${targetRef.path}"`); - } - prop = parseInt(prop); - } - if (value !== null) { - if (typeof value === 'object') { - if (value[isProxy]) { - // Assigning one proxied value to another - value = value.valueOf(); - } - // else if (Object.isFrozen(value)) { - // // Create a copy to unfreeze it - // value = cloneObject(value); - // } - value = (0, utils_1.cloneObject)(value); // Fix #10, always clone objects so changes made through the proxy won't change the original object (and vice versa) - } - if ((0, utils_1.valuesAreEqual)(value, target[prop])) { //if (compareValues(value, target[prop]) === 'identical') { // (typeof value !== 'object' && target[prop] === value) { - // not changing the actual value, ignore - return true; - } - } - if (context.target.some(key => typeof key === 'number')) { - // Updating an object property inside an array. Flag the first array in target to be written. - // Eg: when chat.members === [{ name: 'Ewout', id: 'someid' }] - // --> chat.members[0].name = 'Ewout' --> Rewrite members array instead of chat/members[0]/name - context.flag('write', context.target.slice(0, context.target.findIndex(key => typeof key === 'number'))); - } - else if (target instanceof Array) { - // Flag the entire array to be overwritten - context.flag('write', context.target); - } - else { - // Flag child property - context.flag('write', context.target.concat(prop)); - } - // Set cached value: - if (value === null) { - delete target[prop]; - } - else { - removeVoidProperties(value); - target[prop] = value; - } - return true; - }, - deleteProperty(target, prop) { - target = getTargetValue(context.root.cache, context.target); - if (target === null) { - throw new Error(`Cannot delete property ${prop.toString()} of null`); - } - if (typeof prop === 'symbol') { - return Reflect.deleteProperty(target, prop); - } - if (!(prop in target)) { - return true; // Nothing to delete - } - context.flag('write', context.target.concat(prop)); - delete target[prop]; - return true; - }, - ownKeys(target) { - target = getTargetValue(context.root.cache, context.target); - return Reflect.ownKeys(target); - }, - has(target, prop) { - target = getTargetValue(context.root.cache, context.target); - return Reflect.has(target, prop); - }, - getOwnPropertyDescriptor(target, prop) { - target = getTargetValue(context.root.cache, context.target); - const descriptor = Reflect.getOwnPropertyDescriptor(target, prop); - if (descriptor) { - descriptor.configurable = true; // prevent "TypeError: 'getOwnPropertyDescriptor' on proxy: trap reported non-configurability for property '...' which is either non-existant or configurable in the proxy target" - } - return descriptor; - }, - getPrototypeOf(target) { - target = getTargetValue(context.root.cache, context.target); - return Reflect.getPrototypeOf(target); - }, - }; - const proxy = new Proxy({}, handler); - return proxy; -} -function removeVoidProperties(obj) { - if (typeof obj !== 'object') { - return; - } - Object.keys(obj).forEach(key => { - const val = obj[key]; - if (val === null || typeof val === 'undefined') { - delete obj[key]; - } - else if (typeof val === 'object') { - removeVoidProperties(val); - } - }); -} -/** - * Convenience function to access ILiveDataProxyValue methods on a proxied value - * @param proxiedValue The proxied value to get access to - * @returns Returns the same object typecasted to an ILiveDataProxyValue - * @example - * // IChatMessages is an ObjectCollection - * let observable: Observable; - * - * // Allows you to do this: - * observable = proxyAccess(chat.messages).getObservable(); - * - * // Instead of: - * observable = (chat.messages.msg1 as any as ILiveDataProxyValue).getObservable(); - * - * // Both do the exact same, but the first is less obscure - */ -function proxyAccess(proxiedValue) { - if (typeof proxiedValue !== 'object' || !proxiedValue[isProxy]) { - throw new Error('Given value is not proxied. Make sure you are referencing the value through the live data proxy.'); - } - return proxiedValue; -} -exports.proxyAccess = proxyAccess; -/** - * Provides functionality to work with ordered collections through a live data proxy. Eliminates - * the need for arrays to handle ordered data by adding a 'sort' properties to child objects in a - * collection, and provides functionality to sort and reorder items with a minimal amount of database - * updates. - */ -class OrderedCollectionProxy { - constructor(collection, orderProperty = 'order', orderIncrement = 10) { - this.collection = collection; - this.orderProperty = orderProperty; - this.orderIncrement = orderIncrement; - if (typeof collection !== 'object' || !collection[isProxy]) { - throw new Error('Collection is not proxied'); - } - if (collection.valueOf() instanceof Array) { - throw new Error('Collection is an array, not an object collection'); - } - if (!Object.keys(collection).every(key => typeof collection[key] === 'object')) { - throw new Error('Collection has non-object children'); - } - // Check if the collection has order properties. If not, assign them now - const ok = Object.keys(collection).every(key => typeof collection[key][orderProperty] === 'number'); - if (!ok) { - // Assign order properties now. Database will be updated automatically - const keys = Object.keys(collection); - for (let i = 0; i < keys.length; i++) { - const item = collection[keys[i]]; - item[orderProperty] = i * orderIncrement; // 0, 10, 20, 30 etc - } - } - } - /** - * Gets an observable for the target object collection. Same as calling `collection.getObservable()` - * @returns - */ - getObservable() { - return proxyAccess(this.collection).getObservable(); - } - /** - * Gets an observable that emits a new ordered array representation of the object collection each time - * the unlaying data is changed. Same as calling `getArray()` in a `getObservable().subscribe` callback - * @returns - */ - getArrayObservable() { - const Observable = (0, optional_observable_1.getObservable)(); - return new Observable((subscriber => { - const subscription = this.getObservable().subscribe(( /*value*/) => { - const newArray = this.getArray(); - subscriber.next(newArray); - }); - return function unsubscribe() { - subscription.unsubscribe(); - }; - })); - } - /** - * Gets an ordered array representation of the items in your object collection. The items in the array - * are proxied values, changes will be in sync with the database. Note that the array itself - * is not mutable: adding or removing items to it will NOT update the collection in the - * the database and vice versa. Use `add`, `delete`, `sort` and `move` methods to make changes - * that impact the collection's sorting order - * @returns order array - */ - getArray() { - const arr = proxyAccess(this.collection).toArray((a, b) => a[this.orderProperty] - b[this.orderProperty]); - // arr.push = (...items: T[]) => { - // items.forEach(item => this.add(item)); - // return arr.length; - // }; - return arr; - } - /** - * Adds or moves an item to/within the object collection and takes care of the proper sorting order. - * @param item Item to add or move - * @param index Optional target index in the sorted representation, appends if not specified. - * @param from If the item is being moved - * @returns - */ - add(item, index, from) { - const arr = this.getArray(); - let minOrder = Number.POSITIVE_INFINITY, maxOrder = Number.NEGATIVE_INFINITY; - for (let i = 0; i < arr.length; i++) { - const order = arr[i][this.orderProperty]; - minOrder = Math.min(order, minOrder); - maxOrder = Math.max(order, maxOrder); - } - let fromKey; - if (typeof from === 'number') { - // Moving existing item - fromKey = Object.keys(this.collection).find(key => this.collection[key] === item); - if (!fromKey) { - throw new Error('item not found in collection'); - } - if (from === index) { - return { key: fromKey, index }; - } - if (Math.abs(from - index) === 1) { - // Position being swapped, swap their order property values - const otherItem = arr[index]; - const otherOrder = otherItem[this.orderProperty]; - otherItem[this.orderProperty] = item[this.orderProperty]; - item[this.orderProperty] = otherOrder; - return { key: fromKey, index }; - } - else { - // Remove from array, code below will add again - arr.splice(from, 1); - } - } - if (typeof index !== 'number' || index >= arr.length) { - // append at the end - index = arr.length; - item[this.orderProperty] = (arr.length == 0 ? 0 : maxOrder + this.orderIncrement); - } - else if (index === 0) { - // insert before all others - item[this.orderProperty] = (arr.length == 0 ? 0 : minOrder - this.orderIncrement); - } - else { - // insert between 2 others - const orders = arr.map(item => item[this.orderProperty]); - const gap = orders[index] - orders[index - 1]; - if (gap > 1) { - item[this.orderProperty] = (orders[index] - Math.floor(gap / 2)); - } - else { - // TODO: Can this gap be enlarged by moving one of both orders? - // For now, change all other orders - arr.splice(index, 0, item); - for (let i = 0; i < arr.length; i++) { - arr[i][this.orderProperty] = (i * this.orderIncrement); - } - } - } - const key = typeof fromKey === 'string' - ? fromKey // Moved item, don't add it - : proxyAccess(this.collection).push(item); - return { key, index }; - } - /** - * Deletes an item from the object collection using the their index in the sorted array representation - * @param index - * @returns the key of the collection's child that was deleted - */ - delete(index) { - const arr = this.getArray(); - const item = arr[index]; - if (!item) { - throw new Error(`Item at index ${index} not found`); - } - const key = Object.keys(this.collection).find(key => this.collection[key] === item); - if (!key) { - throw new Error('Cannot find target object to delete'); - } - this.collection[key] = null; // Deletes it from db - return { key, index }; - } - /** - * Moves an item in the object collection by reordering it - * @param fromIndex Current index in the array (the ordered representation of the object collection) - * @param toIndex Target index in the array - * @returns - */ - move(fromIndex, toIndex) { - const arr = this.getArray(); - return this.add(arr[fromIndex], toIndex, fromIndex); - } - /** - * Reorders the object collection using given sort function. Allows quick reordering of the collection which is persisted in the database - * @param sortFn - */ - sort(sortFn) { - const arr = this.getArray(); - arr.sort(sortFn); - for (let i = 0; i < arr.length; i++) { - arr[i][this.orderProperty] = i * this.orderIncrement; - } - } -} -exports.OrderedCollectionProxy = OrderedCollectionProxy; - -},{"./data-reference":42,"./data-snapshot":43,"./id":45,"./optional-observable":48,"./path-info":50,"./path-reference":51,"./process":52,"./simple-event-emitter":56,"./utils":61}],42:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.DataReferencesArray = exports.DataSnapshotsArray = exports.DataReferenceQuery = exports.DataReference = exports.QueryDataRetrievalOptions = exports.DataRetrievalOptions = void 0; -const data_snapshot_1 = require("./data-snapshot"); -const subscription_1 = require("./subscription"); -const id_1 = require("./id"); -const path_info_1 = require("./path-info"); -const data_proxy_1 = require("./data-proxy"); -const optional_observable_1 = require("./optional-observable"); -class DataRetrievalOptions { - /** - * Options for data retrieval, allows selective loading of object properties - */ - 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.cache_mode === 'string' && !['allow', 'bypass', 'force'].includes(options.cache_mode)) { - throw new TypeError('invalid value for options.cache_mode'); - } - this.include = options.include || undefined; - this.exclude = options.exclude || undefined; - this.child_objects = typeof options.child_objects === 'boolean' ? options.child_objects : undefined; - this.cache_mode = typeof options.cache_mode === 'string' - ? options.cache_mode - : typeof options.allow_cache === 'boolean' - ? options.allow_cache ? 'allow' : 'bypass' - : 'allow'; - this.cache_cursor = typeof options.cache_cursor === 'string' ? options.cache_cursor : undefined; - } -} -exports.DataRetrievalOptions = DataRetrievalOptions; -class QueryDataRetrievalOptions extends DataRetrievalOptions { - /** - * @param options Options for data retrieval, allows selective loading of object properties - */ - constructor(options) { - super(options); - if (!['undefined', 'boolean'].includes(typeof options.snapshots)) { - throw new TypeError('options.snapshots must be a boolean'); - } - this.snapshots = typeof options.snapshots === 'boolean' ? options.snapshots : true; - } -} -exports.QueryDataRetrievalOptions = QueryDataRetrievalOptions; -const _private = Symbol('private'); -class DataReference { - /** - * Creates a reference to a node - */ - constructor(db, path, vars) { - this.db = db; - if (!path) { - path = ''; - } - path = path.replace(/^\/|\/$/g, ''); // Trim slashes - const pathInfo = path_info_1.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 || {}, - context: {}, - pushed: false, - cursor: null, - }; - } - context(context, merge = false) { - const currentContext = this[_private].context; - if (typeof context === 'object') { - const newContext = context ? merge ? currentContext || {} : context : {}; - if (context) { - // Merge new with current context - Object.keys(context).forEach(key => { - newContext[key] = context[key]; - }); - } - this[_private].context = newContext; - return this; - } - else if (typeof context === 'undefined') { - console.warn('Use snap.context() instead of snap.ref.context() to get updating context in event callbacks'); - return currentContext; - } - else { - throw new Error('Invalid context argument'); - } - } - /** - * Contains the last received cursor for this referenced path (if the connected database has transaction logging enabled). - * If you want to be notified if this value changes, add a handler with `ref.onCursor(callback)` - */ - get cursor() { - return this[_private].cursor; - } - set cursor(value) { - var _a; - this[_private].cursor = value; - (_a = this.onCursor) === null || _a === void 0 ? void 0 : _a.call(this, value); - } - /** - * The path this instance was created with - */ - get path() { return this[_private].path; } - /** - * The key or index of this node - */ - get key() { - const key = this[_private].key; - return typeof key === 'number' ? `[${key}]` : key; - } - /** - * If the "key" is a number, it is an index! - */ - get index() { - const key = this[_private].key; - if (typeof key !== 'number') { - throw new Error(`"${key}" is not a number`); - } - return key; - } - /** - * Returns a new reference to this node's parent - */ - get parent() { - const currentPath = path_info_1.PathInfo.fillVariables2(this.path, this.vars); - const info = path_info_1.PathInfo.get(currentPath); - if (info.parentPath === null) { - return null; - } - return new DataReference(this.db, info.parentPath).context(this[_private].context); - } - /** - * Contains values of the variables/wildcards used in a subscription path if this reference was - * created by an event ("value", "child_added" etc), or in a type mapping path when serializing / instantiating typed objects - */ - get vars() { - return this[_private].vars; - } - /** - * Returns a new reference to a child node - * @param childPath Child key, index or path - * @returns reference to the child - */ - child(childPath) { - childPath = typeof childPath === 'number' ? childPath : childPath.replace(/^\/|\/$/g, ''); - const currentPath = path_info_1.PathInfo.fillVariables2(this.path, this.vars); - const targetPath = path_info_1.PathInfo.getChildPath(currentPath, childPath); - return new DataReference(this.db, targetPath).context(this[_private].context); // `${this.path}/${childPath}` - } - /** - * Sets or overwrites the stored value - * @param value value to store in database - * @param onComplete optional completion callback to use instead of returning promise - * @returns promise that resolves with this reference when completed - */ - async set(value, onComplete) { - try { - if (this.isWildcardPath) { - throw new Error(`Cannot set the value of wildcard path "/${this.path}"`); - } - 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 undefined value in "/${this.path}"`); - } - if (!this.db.isReady) { - await this.db.ready(); - } - value = this.db.types.serialize(this.path, value); - const { cursor } = await this.db.api.set(this.path, value, { context: this[_private].context }); - this.cursor = cursor; - 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, this); - } - catch (err) { - console.error('Error in onComplete callback:', err); - } - } - else { - // throw again - throw err; - } - } - return this; - } - /** - * Updates properties of the referenced node - * @param updates containing the properties to update - * @param onComplete optional completion callback to use instead of returning promise - * @return returns promise that resolves with this reference once completed - */ - async update(updates, onComplete) { - try { - if (this.isWildcardPath) { - throw new Error(`Cannot update the value of wildcard path "/${this.path}"`); - } - if (!this.db.isReady) { - await this.db.ready(); - } - if (typeof updates !== 'object' || updates instanceof Array || updates instanceof ArrayBuffer || updates instanceof Date) { - await this.set(updates); - } - else if (Object.keys(updates).length === 0) { - console.warn(`update called on path "/${this.path}", but there is nothing to update`); - } - else { - updates = this.db.types.serialize(this.path, updates); - const { cursor } = await this.db.api.update(this.path, updates, { context: this[_private].context }); - this.cursor = cursor; - } - 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, this); - } - catch (err) { - console.error('Error in onComplete callback:', err); - } - } - else { - // throw again - throw err; - } - } - return this; - } - /** - * Sets the value a node using a transaction: it runs your callback function with the current value, uses its return value as the new value to store. - * The transaction is canceled if your callback returns undefined, or throws an error. If your callback returns null, the target node will be removed. - * @param callback - callback function that performs the transaction on the node's current value. It must return the new value to store (or promise with new value), undefined to cancel the transaction, or null to remove the node. - * @returns returns a promise that resolves with the DataReference once the transaction has been processed - */ - async transaction(callback) { - if (this.isWildcardPath) { - throw new Error(`Cannot start a transaction on wildcard path "/${this.path}"`); - } - if (!this.db.isReady) { - await this.db.ready(); - } - let throwError; - const cb = (currentValue) => { - currentValue = this.db.types.deserialize(this.path, currentValue); - const snap = new data_snapshot_1.DataSnapshot(this, currentValue); - let newValue; - try { - newValue = callback(snap); - } - catch (err) { - // callback code threw an error - throwError = err; // Remember error - return; // cancel transaction by returning undefined - } - if (newValue instanceof Promise) { - return newValue - .then((val) => { - return this.db.types.serialize(this.path, val); - }) - .catch(err => { - throwError = err; // Remember error - return; // cancel transaction by returning undefined - }); - } - else { - return this.db.types.serialize(this.path, newValue); - } - }; - const { cursor } = await this.db.api.transaction(this.path, cb, { context: this[_private].context }); - this.cursor = cursor; - if (throwError) { - // Rethrow error from callback code - throw throwError; - } - return this; - } - on(event, callback, cancelCallback) { - if (this.path === '' && ['value', 'child_changed'].includes(event)) { - // Removed 'notify_value' and 'notify_child_changed' events from the list, they do not require additional data loading anymore. - console.warn('WARNING: Listening for value and child_changed events on the root node is a bad practice. These events require loading of all data (value event), or potentially lots of data (child_changed event) each time they are fired'); - } - let eventPublisher = null; - const eventStream = new subscription_1.EventStream(publisher => { eventPublisher = publisher; }); - // Map OUR callback to original callback, so .off can remove the right callback(s) - const cb = { - event, - stream: eventStream, - userCallback: typeof callback === 'function' && callback, - ourCallback: (err, path, newValue, oldValue, eventContext) => { - if (err) { - // TODO: Investigate if this ever happens? - this.db.logger.error(`Error getting data for event ${event} on path "${path}"`, err); - return; - } - const ref = this.db.ref(path); - ref[_private].vars = path_info_1.PathInfo.extractVariables(this.path, path); - let callbackObject; - if (event.startsWith('notify_')) { - // No data event, callback with reference - callbackObject = ref.context(eventContext || {}); - } - else { - const values = { - previous: this.db.types.deserialize(path, oldValue), - current: this.db.types.deserialize(path, newValue), - }; - if (event === 'child_removed') { - callbackObject = new data_snapshot_1.DataSnapshot(ref, values.previous, true, values.previous, eventContext); - } - else if (event === 'mutations') { - callbackObject = new data_snapshot_1.MutationsDataSnapshot(ref, values.current, eventContext); - } - else { - const isRemoved = event === 'mutated' && values.current === null; - callbackObject = new data_snapshot_1.DataSnapshot(ref, values.current, isRemoved, values.previous, eventContext); - } - } - eventPublisher.publish(callbackObject); - if (eventContext === null || eventContext === void 0 ? void 0 : eventContext.acebase_cursor) { - this.cursor = eventContext.acebase_cursor; - } - }, - }; - this[_private].callbacks.push(cb); - const subscribe = () => { - // (NEW) Add callback to event stream - // ref.on('value', callback) is now exactly the same as ref.on('value').subscribe(callback) - if (typeof callback === 'function') { - eventStream.subscribe(callback, (activated, cancelReason) => { - if (!activated) { - cancelCallback && cancelCallback(cancelReason); - } - }); - } - const advancedOptions = typeof callback === 'object' - ? callback - : { newOnly: !callback }; // newOnly: if callback is not 'truthy', could change this to (typeof callback !== 'function' && callback !== true) but that would break client code that uses a truthy argument. - if (typeof advancedOptions.newOnly !== 'boolean') { - advancedOptions.newOnly = false; - } - if (this.isWildcardPath) { - advancedOptions.newOnly = true; - } - const cancelSubscription = (err) => { - // Access denied? - // Cancel subscription - const callbacks = this[_private].callbacks; - callbacks.splice(callbacks.indexOf(cb), 1); - this.db.api.unsubscribe(this.path, event, cb.ourCallback); - // Call cancelCallbacks - this.db.logger.error(`Subscription "${event}" on path "/${this.path}" canceled because of an error: ${err.message}`); - eventPublisher.cancel(err.message); - }; - const authorized = this.db.api.subscribe(this.path, event, cb.ourCallback, { newOnly: advancedOptions.newOnly, cancelCallback: cancelSubscription, syncFallback: advancedOptions.syncFallback }); - const allSubscriptionsStoppedCallback = () => { - const callbacks = this[_private].callbacks; - callbacks.splice(callbacks.indexOf(cb), 1); - return this.db.api.unsubscribe(this.path, event, cb.ourCallback); - }; - if (authorized instanceof Promise) { - // Web API now returns a promise that resolves if the request is allowed - // and rejects when access is denied by the set security rules - authorized.then(() => { - // Access granted - eventPublisher.start(allSubscriptionsStoppedCallback); - }).catch(cancelSubscription); - } - else { - // Local API, always authorized - eventPublisher.start(allSubscriptionsStoppedCallback); - } - if (!advancedOptions.newOnly) { - // If callback param is supplied (either a callback function or true or something else truthy), - // it will fire events for current values right now. - // Otherwise, it expects the .subscribe methode to be used, which will then - // only be called for future events - if (event === 'value') { - this.get(snap => { - eventPublisher.publish(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 => { - const childSnap = new data_snapshot_1.DataSnapshot(this.child(key), val[key]); - eventPublisher.publish(childSnap); - }); - }); - } - else if (event === 'notify_child_added') { - // 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, limit = step; - let skip = 0; - const more = async () => { - const children = await this.db.api.reflect(this.path, 'children', { limit, skip }); - children.list.forEach(child => { - const childRef = this.child(child.key); - eventPublisher.publish(childRef); - // typeof callback === 'function' && callback(childRef); - }); - if (children.more) { - skip += step; - more(); - } - }; - more(); + lock.timeout = setTimeout(timeoutHandler, this.timeout / 4); } } - }; - if (this.db.isReady) { - subscribe(); + return lock; } else { - this.db.ready(subscribe); - } - return eventStream; - } - off(event, callback) { - const subscriptions = this[_private].callbacks; - const stopSubs = subscriptions.filter(sub => (!event || sub.event === event) && (!callback || sub.userCallback === callback)); - if (stopSubs.length === 0) { - this.db.logger.warn(`Can't find event subscriptions to stop (path: "${this.path}", event: ${event || '(any)'}, callback: ${callback})`); + // Keep pending until clashing lock(s) is/are removed + //debug.warn(`lock :: QUEUED ${lock.forWriting ? "write" : "read" } lock on path "/${lock.path}" by tid ${lock.tid}; ${lock.comment}`); + (0, assert_js_1.assert)(lock.state === exports.LOCK_STATE.PENDING); + return new Promise((resolve, reject) => { + lock.resolve = resolve; + lock.reject = reject; + }); } - stopSubs.forEach(sub => { - sub.stream.stop(); - }); - return this; } - get(optionsOrCallback, callback) { - if (!this.db.isReady) { - const promise = this.db.ready().then(() => this.get(optionsOrCallback, callback)); - return typeof optionsOrCallback !== 'function' && typeof callback !== 'function' ? promise : undefined; // only return promise if no callback is used + unlock(lockOrId, comment, processQueue = true) { + var _a, _b; + let lock, i; + if (lockOrId instanceof NodeLock) { + lock = lockOrId; + i = this._locks.indexOf(lock); } - callback = - typeof optionsOrCallback === 'function' - ? optionsOrCallback - : typeof callback === 'function' - ? callback - : undefined; - if (this.isWildcardPath) { - const error = new Error(`Cannot get value of wildcard path "/${this.path}". Use .query() instead`); - if (typeof callback === 'function') { - throw error; - } - return Promise.reject(error); + else { + const id = lockOrId; + i = this._locks.findIndex(l => l.id === id); + lock = this._locks[i]; } - const options = new DataRetrievalOptions(typeof optionsOrCallback === 'object' ? optionsOrCallback : { cache_mode: 'allow' }); - const promise = this.db.api.get(this.path, options).then(result => { - var _a; - const isNewApiResult = ('context' in result && 'value' in result); - if (!isNewApiResult) { - // acebase-core version package was updated but acebase or acebase-client package was not? Warn, but don't throw an error. - console.warn('AceBase api.get method returned an old response value. Update your acebase or acebase-client package'); - result = { value: result, context: {} }; - } - const value = this.db.types.deserialize(this.path, result.value); - const snapshot = new data_snapshot_1.DataSnapshot(this, value, undefined, undefined, result.context); - if ((_a = result.context) === null || _a === void 0 ? void 0 : _a.acebase_cursor) { - this.cursor = result.context.acebase_cursor; - } - return snapshot; - }); - if (callback) { - promise.then(callback).catch(err => { - console.error('Uncaught error:', err); - }); - return; + if (i < 0) { + const msg = `lock on "/${(_a = lock === null || lock === void 0 ? void 0 : lock.path) !== null && _a !== void 0 ? _a : '?'}" for tid ${(_b = lock === null || lock === void 0 ? void 0 : lock.tid) !== null && _b !== void 0 ? _b : '?'} wasn't found; ${comment}`; + // debug.error(`unlock :: ${msg}`); + throw new NodeLockError(msg, lock !== null && lock !== void 0 ? lock : null); } - else { - return promise; + lock.state = exports.LOCK_STATE.DONE; + clearTimeout(lock.timeout); + if (lock.warned) { + this.logger.info(`long running ${lock.forWriting ? 'write' : 'read'} lock on "${lock.path}" by tid ${lock.tid} has been released`); } + this._locks.splice(i, 1); + DEBUG_MODE && console.error(`${lock.forWriting ? 'write' : 'read'} lock RELEASED on "${lock.path}" by tid ${lock.tid}`); + //debug.warn(`unlock :: RELEASED ${lock.forWriting ? "write" : "read" } lock on "/${lock.path}" for tid ${lock.tid}; ${lock.comment}; ${comment}`); + processQueue && this._processLockQueue(); + return lock; } - /** - * Waits for an event to occur - * @param event Name of the event, eg "value", "child_added", "child_changed", "child_removed" - * @param options data retrieval options, to include or exclude specific child keys - * @returns returns promise that resolves with a snapshot of the data - */ - once(event, options) { - if (event === 'value' && !this.isWildcardPath) { - // Shortcut, do not start listening for future events - return this.get(options); - } - return new Promise((resolve) => { - const callback = (snap) => { - this.off(event, callback); // unsubscribe directly - resolve(snap); - }; - this.on(event, callback); - }); + list() { + return this._locks || []; } + isAllowed(path, tid, forWriting) { + return this._allowLock(path, tid, forWriting).allow; + } +} +exports.NodeLocker = NodeLocker; +let lastid = 0; +class NodeLock { + static get LOCK_STATE() { return exports.LOCK_STATE; } /** - * @param value optional value to store into the database right away - * @param onComplete optional callback function to run once value has been stored - * @returns returns promise that resolves with the reference after the passed value has been stored + * Constructor for a record lock + * @param {NodeLocker} locker + * @param {string} path + * @param {string} tid + * @param {boolean} forWriting + * @param {boolean} priority */ - push(value, onComplete) { - if (this.isWildcardPath) { - const error = new Error(`Cannot push to wildcard path "/${this.path}"`); - if (typeof value === 'undefined' || typeof onComplete === 'function') { - throw error; - } - return Promise.reject(error); - } - const id = id_1.ID.generate(); - const ref = this.child(id); - ref[_private].pushed = true; - if (typeof value !== 'undefined') { - return ref.set(value, onComplete).then(() => ref); + constructor(locker, path, tid, forWriting, priority = false) { + this.locker = locker; + this.path = path; + this.tid = tid; + this.forWriting = forWriting; + this.priority = priority; + this.state = exports.LOCK_STATE.PENDING; + this.requested = Date.now(); + this.comment = ''; + this.waitingFor = null; + this.id = ++lastid; + this.history = []; + this.warned = false; + } + async release(comment) { + //return this.storage.unlock(this.path, this.tid, comment); + this.history.push({ action: 'release', path: this.path, forWriting: this.forWriting, comment }); + return this.locker.unlock(this, comment || this.comment); + } + async moveToParent() { + const parentPath = acebase_core_1.PathInfo.get(this.path).parentPath; //getPathInfo(this.path).parent; + const allowed = this.locker.isAllowed(parentPath, this.tid, this.forWriting); //_allowLock(parentPath, this.tid, this.forWriting); + if (allowed) { + DEBUG_MODE && console.error(`moveToParent ALLOWED for ${this.forWriting ? 'write' : 'read'} lock on "${this.path}" by tid ${this.tid} (${this.comment})`); + this.history.push({ path: this.path, forWriting: this.forWriting, action: 'moving to parent' }); + this.waitingFor = null; + this.path = parentPath; + // this.comment = `moved to parent: ${this.comment}`; + return this; } else { - return ref; + // Unlock without processing the queue + DEBUG_MODE && console.error(`moveToParent QUEUED for ${this.forWriting ? 'write' : 'read'} lock on "${this.path}" by tid ${this.tid} (${this.comment})`); + this.locker.unlock(this, `moveLockToParent: ${this.comment}`, false); + // Lock parent node with priority to jump the queue + const newLock = await this.locker.lock(parentPath, this.tid, this.forWriting, this.comment, { withPriority: true }); + DEBUG_MODE && console.error(`QUEUED moveToParent ALLOWED for ${this.forWriting ? 'write' : 'read'} lock on "${this.path}" by tid ${this.tid} (${this.comment})`); + newLock.history = this.history; + newLock.history.push({ path: this.path, forWriting: this.forWriting, action: 'moving to parent through queue (priority)' }); + return newLock; } } - /** - * Removes this node and all children - */ - async remove() { - if (this.isWildcardPath) { - throw new Error(`Cannot remove wildcard path "/${this.path}". Use query().remove instead`); - } - if (this.parent === null) { - throw new Error('Cannot remove the root node'); - } - return this.set(null); +} +exports.NodeLock = NodeLock; + +},{"./assert.js":31,"acebase-core":12}],41:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getValueType = exports.getNodeValueType = exports.getValueTypeName = exports.VALUE_TYPES = void 0; +const acebase_core_1 = require("acebase-core"); +const nodeValueTypes = { + // Native types: + OBJECT: 1, + ARRAY: 2, + NUMBER: 3, + BOOLEAN: 4, + STRING: 5, + BIGINT: 7, + // Custom types: + DATETIME: 6, + BINARY: 8, + REFERENCE: 9, // Absolute or relative path to other node + // Future: + // DOCUMENT: 10, // JSON/XML documents that are contained entirely within the stored node +}; +exports.VALUE_TYPES = nodeValueTypes; +function getValueTypeName(valueType) { + switch (valueType) { + case exports.VALUE_TYPES.ARRAY: return 'array'; + case exports.VALUE_TYPES.BINARY: return 'binary'; + case exports.VALUE_TYPES.BOOLEAN: return 'boolean'; + case exports.VALUE_TYPES.DATETIME: return 'date'; + case exports.VALUE_TYPES.NUMBER: return 'number'; + case exports.VALUE_TYPES.OBJECT: return 'object'; + case exports.VALUE_TYPES.REFERENCE: return 'reference'; + case exports.VALUE_TYPES.STRING: return 'string'; + case exports.VALUE_TYPES.BIGINT: return 'bigint'; + // case VALUE_TYPES.DOCUMENT: return 'document'; + default: 'unknown'; + } +} +exports.getValueTypeName = getValueTypeName; +function getNodeValueType(value) { + if (value instanceof Array) { + return exports.VALUE_TYPES.ARRAY; + } + else if (value instanceof acebase_core_1.PathReference) { + return exports.VALUE_TYPES.REFERENCE; + } + else if (value instanceof ArrayBuffer) { + return exports.VALUE_TYPES.BINARY; + } + // TODO else if (value instanceof DataDocument) { return VALUE_TYPES.DOCUMENT; } + else if (typeof value === 'string') { + return exports.VALUE_TYPES.STRING; + } + else if (typeof value === 'object') { + return exports.VALUE_TYPES.OBJECT; + } + else if (typeof value === 'bigint') { + return exports.VALUE_TYPES.BIGINT; + } + throw new Error(`Invalid value for standalone node: ${value}`); +} +exports.getNodeValueType = getNodeValueType; +function getValueType(value) { + if (value instanceof Array) { + return exports.VALUE_TYPES.ARRAY; + } + else if (value instanceof acebase_core_1.PathReference) { + return exports.VALUE_TYPES.REFERENCE; + } + else if (value instanceof ArrayBuffer) { + return exports.VALUE_TYPES.BINARY; } - /** - * Quickly checks if this reference has a value in the database, without returning its data - * @returns returns a promise that resolves with a boolean value - */ - async exists() { - if (this.isWildcardPath) { - throw new Error(`Cannot check wildcard path "/${this.path}" existence`); - } - if (!this.db.isReady) { - await this.db.ready(); - } - return this.db.api.exists(this.path); + else if (value instanceof Date) { + return exports.VALUE_TYPES.DATETIME; } - get isWildcardPath() { - return this.path.indexOf('*') >= 0 || this.path.indexOf('$') >= 0; + // TODO else if (value instanceof DataDocument) { return VALUE_TYPES.DOCUMENT; } + else if (typeof value === 'string') { + return exports.VALUE_TYPES.STRING; } - /** - * Creates a query object for current node - */ - query() { - return new DataReferenceQuery(this); + else if (typeof value === 'object') { + return exports.VALUE_TYPES.OBJECT; } - /** - * Gets the number of children this node has, uses reflection - */ - async count() { - const info = await this.reflect('info', { child_count: true }); - return info.children.count; + else if (typeof value === 'number') { + return exports.VALUE_TYPES.NUMBER; } - async reflect(type, args) { - if (this.isWildcardPath) { - throw new Error(`Cannot reflect on wildcard path "/${this.path}"`); - } - if (!this.db.isReady) { - await this.db.ready(); - } - return this.db.api.reflect(this.path, type, args); + else if (typeof value === 'boolean') { + return exports.VALUE_TYPES.BOOLEAN; } - async export(write, options = { format: 'json', type_safe: true }) { - if (this.isWildcardPath) { - throw new Error(`Cannot export wildcard path "/${this.path}"`); - } - if (!this.db.isReady) { - await this.db.ready(); - } - const writeFn = typeof write === 'function' ? write : write.write.bind(write); - return this.db.api.export(this.path, writeFn, options); + else if (typeof value === 'bigint') { + return exports.VALUE_TYPES.BIGINT; } - /** - * Imports the value of this node and all children - * @param read Function that reads data from your stream - * @param options Only supported format currently is json - * @returns returns a promise that resolves once all data is imported - */ - async import(read, options = { format: 'json', suppress_events: false }) { - if (this.isWildcardPath) { - throw new Error(`Cannot import to wildcard path "/${this.path}"`); + throw new Error(`Unknown value type: ${value}`); +} +exports.getValueType = getValueType; + +},{"acebase-core":12}],42:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.NotSupported = void 0; +class NotSupported { + constructor(context = 'browser') { throw new Error(`This feature is not supported in ${context} context`); } +} +exports.NotSupported = NotSupported; + +},{}],43:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.pfs = void 0; +class pfs { + static get hasFileSystem() { return false; } + static get fs() { return null; } +} +exports.pfs = pfs; + +},{}],44:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.executeQuery = void 0; +const acebase_core_1 = require("acebase-core"); +const node_value_types_js_1 = require("./node-value-types.js"); +const node_errors_js_1 = require("./node-errors.js"); +const index_js_1 = require("./data-index/index.js"); +const async_task_batch_js_1 = require("./async-task-batch.js"); +// eslint-disable-next-line @typescript-eslint/no-empty-function +const noop = () => { }; +/** + * + * @param storage Target storage instance + * @param path Path of the object collection to perform query on + * @param query Query to execute + * @param options Additional options + * @returns Returns a promise that resolves with matching data or paths in `results` + */ +async function executeQuery(api, path, query, options = { snapshots: false, include: undefined, exclude: undefined, child_objects: undefined, eventHandler: noop }) { + var _a, _b, _c, _d, _e, _f; + // TODO: Refactor to async + if (typeof options !== 'object') { + options = {}; + } + if (typeof options.snapshots === 'undefined') { + options.snapshots = false; + } + const context = {}; + if ((_a = api.storage.settings.transactions) === null || _a === void 0 ? void 0 : _a.log) { + context.acebase_cursor = acebase_core_1.ID.generate(); + } + const queryFilters = query.filters.map(f => (Object.assign({}, f))); + const querySort = query.order.map(s => (Object.assign({}, s))); + const sortMatches = (matches) => { + matches.sort((a, b) => { + const compare = (i) => { + const o = querySort[i]; + const trailKeys = acebase_core_1.PathInfo.getPathKeys(typeof o.key === 'number' ? `[${o.key}]` : o.key); + const left = trailKeys.reduce((val, key) => val !== null && typeof val === 'object' && key in val ? val[key] : null, a.val); + const right = trailKeys.reduce((val, key) => val !== null && typeof val === 'object' && key in val ? val[key] : null, b.val); + if (left === null) { + return right === null ? 0 : o.ascending ? -1 : 1; + } + if (right === null) { + return o.ascending ? 1 : -1; + } + // TODO: add collation options using Intl.Collator. Note this also has to be implemented in the matching engines (inclusing indexes) + // See discussion https://github.com/appy-one/acebase/discussions/27 + if (left == right) { + if (i < querySort.length - 1) { + return compare(i + 1); + } + else { + return a.path < b.path ? -1 : 1; + } // Sort by path if property values are equal + } + else if (left < right) { + return o.ascending ? -1 : 1; + } + // else if (left > right) { + return o.ascending ? 1 : -1; + // } + }; + return compare(0); + }); + }; + const loadResultsData = async (preResults, options) => { + // Limit the amount of concurrent getValue calls by batching them + if (preResults.length === 0) { + return []; + } + const maxBatchSize = 50; + const batch = new async_task_batch_js_1.AsyncTaskBatch(maxBatchSize); + const results = []; + preResults.forEach(({ path }, index) => batch.add(async () => { + const node = await api.storage.getNode(path, options); + const val = node.value; + if (val === null) { + // Record was deleted, but index isn't updated yet? + api.logger.warn(`Indexed result "/${path}" does not have a record!`); + // TODO: let index rebuild + return; + } + const result = { path, val }; + if (stepsExecuted.sorted) { + // Put the result in the same index as the preResult was + results[index] = result; + } + else { + results.push(result); + if (!stepsExecuted.skipped && results.length > query.skip + Math.abs(query.take)) { + // we can toss a value! sort, toss last one + sortMatches(results); + results.pop(); // Always toss last value, results have been sorted already + } + } + })); + await batch.finish(); + return results; + }; + const pathInfo = acebase_core_1.PathInfo.get(path); + const isWildcardPath = pathInfo.keys.some(key => key === '*' || key.toString().startsWith('$')); // path.includes('*'); + const availableIndexes = api.storage.indexes.get(path); + const usingIndexes = []; + // eslint-disable-next-line @typescript-eslint/no-empty-function + let stop = async () => { }; + if (isWildcardPath) { + // Check if path contains $vars with explicit filter values. If so, execute multiple queries and merge results + const vars = pathInfo.keys.filter(key => typeof key === 'string' && key.startsWith('$')); + const hasExplicitFilterValues = vars.length > 0 && vars.every(v => query.filters.some(f => f.key === v && ['==', 'in'].includes(f.op))); + const isRealtime = typeof options.monitor === 'object' && [(_b = options.monitor) === null || _b === void 0 ? void 0 : _b.add, (_c = options.monitor) === null || _c === void 0 ? void 0 : _c.change, (_d = options.monitor) === null || _d === void 0 ? void 0 : _d.remove].some(val => val === true); + if (hasExplicitFilterValues && !isRealtime) { + // create path combinations + const combinations = []; + for (const v of vars) { + const filters = query.filters.filter(f => f.key === v); + const filterValues = filters.reduce((values, f) => { + if (f.op === '==') { + values.push(f.compare); + } + if (f.op === 'in') { + if (!(f.compare instanceof Array)) { + throw new Error(`compare argument for 'in' operator must be an Array`); + } + values.push(...f.compare); + } + return values; + }, []); + // Expand all current combinations with these filter values + const prevCombinations = combinations.splice(0); + filterValues.forEach(fv => { + if (prevCombinations.length === 0) { + combinations.push({ [v]: fv }); + } + else { + combinations.push(...prevCombinations.map(c => (Object.assign(Object.assign({}, c), { [v]: fv })))); + } + }); + } + // create queries + const filters = query.filters.filter(f => !vars.includes(f.key)); + const paths = combinations.map(vars => acebase_core_1.PathInfo.get(acebase_core_1.PathInfo.getPathKeys(path).map(key => { var _a; return (_a = vars[key]) !== null && _a !== void 0 ? _a : key; })).path); + const loadData = query.order.length > 0; + const promises = paths.map(path => { + var _a; + return executeQuery(api, path, { filters, take: 0, skip: 0, order: [] }, { + snapshots: loadData, + cache_mode: options.cache_mode, + include: [...((_a = options.include) !== null && _a !== void 0 ? _a : []), ...query.order.map(o => o.key)], + exclude: options.exclude, + }); + }); + const resultSets = await Promise.all(promises); + let results = resultSets.reduce((results, set) => (results.push(...set.results), results), []); + if (loadData) { + sortMatches(results); + } + if (query.skip > 0) { + results.splice(0, query.skip); + } + if (query.take > 0) { + results.splice(query.take); + } + if (options.snapshots && (!loadData || ((_e = options.include) === null || _e === void 0 ? void 0 : _e.length) > 0 || ((_f = options.exclude) === null || _f === void 0 ? void 0 : _f.length) > 0 || !options.child_objects)) { + const { include, exclude, child_objects } = options; + results = await loadResultsData(results, { include, exclude, child_objects }); + } + return { results, context: null, stop }; + // const results = options.snapshots ? results } - if (!this.db.isReady) { - await this.db.ready(); + else if (availableIndexes.length === 0) { + // Wildcard paths require data to be indexed + const err = new Error(`Query on wildcard path "/${path}" requires an index`); + return Promise.reject(err); } - return this.db.api.import(this.path, read, options); - } - proxy(options) { - const isOptionsArg = typeof options === 'object' && (typeof options.cursor !== 'undefined' || typeof options.defaultValue !== 'undefined'); - if (typeof options !== 'undefined' && !isOptionsArg) { - this.db.logger.warn('Warning: live data proxy is being initialized with a deprecated method signature. Use ref.proxy(options) instead of ref.proxy(defaultValue)'); - options = { defaultValue: options }; + if (queryFilters.length === 0) { + // Filterless query on wildcard path. Use first available index with filter on non-null key value (all results) + const index = availableIndexes.filter((index) => index.type === 'normal')[0]; + queryFilters.push({ key: index.key, op: '!=', compare: null }); } - return data_proxy_1.LiveDataProxy.create(this, options); } - /** - * @param options optional initial data retrieval options. - * Not recommended to use yet - given includes/excludes are not applied to received mutations, - * or sync actions when using an AceBaseClient with cache db. - */ - observe(options) { - // options should not be used yet - we can't prevent/filter mutation events on excluded paths atm - if (options) { - throw new Error('observe does not support data retrieval options yet'); - } - if (this.isWildcardPath) { - throw new Error(`Cannot observe wildcard path "/${this.path}"`); + // Check if there are path specific indexes + // eg: index on "users/$uid/posts", key "$uid", including "title" (or key "title", including "$uid") + // Which are very useful for queries on "users/98sdfkb37/posts" with filter or sort on "title" + // const indexesOnPath = availableIndexes + // .map(index => { + // if (!index.path.includes('$')) { return null; } + // const pattern = '^' + index.path.replace(/(\$[a-z0-9_]+)/gi, (match, name) => `(?<${name}>[a-z0-9_]+|\\*)`) + '$'; + // const re = new RegExp(pattern, 'i'); + // const match = path.match(re); + // const canBeUsed = index.key[0] === '$' + // ? match.groups[index.key] !== '*' // Index key value MUST be present in the path + // : null !== ourFilters.find(filter => filter.key === index.key); // Index key MUST be in a filter + // if (!canBeUsed) { return null; } + // return { + // index, + // wildcards: match.groups, // eg: { "$uid": "98sdfkb37" } + // filters: Object.keys(match.groups).filter(name => match.groups[name] !== '*').length + // } + // }) + // .filter(info => info !== null) + // .sort((a, b) => { + // a.filters > b.filters ? -1 : 1 + // }); + // TODO: + // if (ourFilters.length === 0 && indexesOnPath.length > 0) { + // ourFilters = ourFilters.concat({ key: }) + // usingIndexes.push({ index: filter.index, description: filter.index.description}); + // } + queryFilters.forEach(filter => { + if (filter.index) { + // Index has been assigned already + return; } - const Observable = (0, optional_observable_1.getObservable)(); - return new Observable((observer => { - let cache, resolved = false; - let promise = this.get(options).then(snap => { - resolved = true; - cache = snap.val(); - observer.next(cache); + // // Check if there are path indexes we can use + // const pathIndexesWithKey = DataIndex.validOperators.includes(filter.op) + // ? indexesOnPath.filter(info => info.index.key === filter.key || info.index.includeKeys.includes(filter.key)) + // : []; + // Check if there are indexes on this filter key + const indexesOnKey = availableIndexes + .filter(index => index.key === filter.key) + .filter(index => { + return index.validOperators.includes(filter.op); + }); + if (indexesOnKey.length >= 1) { + // If there are multiple indexes on 1 key (happens when index includes other keys), + // we should check other .filters and .order to determine the best one to use + // TODO: Create a good strategy here... + const otherFilterKeys = queryFilters.filter(f => f !== filter).map(f => f.key); + const sortKeys = querySort.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.includes(key)); + const forSorting = availableKeys.filter(key => sortKeys.includes(key)); + const forBoth = forOtherFilters.concat(forSorting.filter(index => !forOtherFilters.includes(index))); + 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, points: points.total, filterKeys: forOtherFilters, sortKeys: forSorting }; }); - const updateCache = (snap) => { - if (!resolved) { - promise = promise.then(() => updateCache(snap)); - return; - } - const mutatedPath = snap.ref.path; - if (mutatedPath === this.path) { - cache = snap.val(); - return observer.next(cache); - } - const trailKeys = path_info_1.PathInfo.getPathKeys(mutatedPath).slice(path_info_1.PathInfo.getPathKeys(this.path).length); - let target = cache; - while (trailKeys.length > 1) { - const key = trailKeys.shift(); - if (!(key in target)) { - // Happens if initial loaded data did not include / excluded this data, - // or we missed out on an event - target[key] = typeof trailKeys[0] === 'number' ? [] : {}; + // Use index with the most points + beneficialIndexes.sort((a, b) => a.points > b.points ? -1 : 1); + const bestBenificialIndex = beneficialIndexes[0]; + // Assign to this filter + filter.index = bestBenificialIndex.index; + // Assign to other filters and sorts + bestBenificialIndex.filterKeys.forEach(key => { + queryFilters.filter(f => f !== filter && f.key === key).forEach(f => { + if (!index_js_1.DataIndex.validOperators.includes(f.op)) { + // The used operator for this filter is invalid for use on metadata + // Probably because it is an Array/Fulltext/Geo query operator + return; } - target = target[key]; - } - const prop = trailKeys.shift(); - const newValue = snap.val(); - if (newValue === null) { - // Remove it - target instanceof Array && typeof prop === 'number' ? target.splice(prop, 1) : delete target[prop]; - } - else { - // Set or update it - target[prop] = newValue; - } - observer.next(cache); - }; - this.on('mutated', updateCache); // TODO: Refactor to 'mutations' event instead - // Return unsubscribe function - return () => { - this.off('mutated', updateCache); - }; - })); - } - async forEach(callbackOrOptions, callback) { - let options; - if (typeof callbackOrOptions === 'function') { - callback = callbackOrOptions; - } - else { - options = callbackOrOptions; - } - if (typeof callback !== 'function') { - throw new TypeError('No callback function given'); - } - // Get all children through reflection. This could be tweaked further using paging - const info = await this.reflect('children', { limit: 0, skip: 0 }); // Gets ALL child keys - const summary = { - canceled: false, - total: info.list.length, - processed: 0, - }; - // Iterate through all children until callback returns false - for (let i = 0; i < info.list.length; i++) { - const key = info.list[i].key; - // Get child data - const snapshot = await this.child(key).get(options); - summary.processed++; - if (!snapshot.exists()) { - // Was removed in the meantime, skip - continue; - } - // Run callback - const result = await callback(snapshot); - if (result === false) { - summary.canceled = true; - break; // Stop looping - } - } - return summary; - } - async getMutations(cursorOrDate) { - const cursor = typeof cursorOrDate === 'string' ? cursorOrDate : undefined; - const timestamp = cursorOrDate === null || typeof cursorOrDate === 'undefined' ? 0 : cursorOrDate instanceof Date ? cursorOrDate.getTime() : undefined; - return this.db.api.getMutations({ path: this.path, cursor, timestamp }); - } - async getChanges(cursorOrDate) { - const cursor = typeof cursorOrDate === 'string' ? cursorOrDate : undefined; - const timestamp = cursorOrDate === null || typeof cursorOrDate === 'undefined' ? 0 : cursorOrDate instanceof Date ? cursorOrDate.getTime() : undefined; - return this.db.api.getChanges({ path: this.path, cursor, timestamp }); - } -} -exports.DataReference = DataReference; -class DataReferenceQuery { - /** - * Creates a query on a reference - */ - constructor(ref) { - this.ref = ref; - this[_private] = { - filters: [], - skip: 0, - take: 0, - order: [], - events: {}, - }; - } - /** - * Applies a filter to the children of the refence being queried. - * If there is an index on the property key being queried, it will be used - * to speed up the query - * @param key property to test value of - * @param op operator to use - * @param compare value to compare with - */ - 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`); + f.indexUsage = 'filter'; + f.index = bestBenificialIndex.index; + }); + }); + bestBenificialIndex.sortKeys.forEach(key => { + querySort.filter(s => s.key === key).forEach(s => { + s.index = bestBenificialIndex.index; + }); + }); } - // DISABLED 2019/10/23 because it is not fully implemented only works locally - // if (op === "custom" && typeof compare !== "function") { - // throw `${op} filter for ${key} must supply a Function compare argument`; - // } - // DISABLED 2022/08/15, implemented by query.ts in acebase - // 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, op, compare }); - return this; + if (filter.index) { + usingIndexes.push({ index: filter.index, description: filter.index.description }); + } + }); + if (querySort.length > 0 && query.take !== 0 && queryFilters.length === 0) { + // Check if we can use assign an index to sorts in a filterless take & sort query + querySort.forEach(sort => { + if (sort.index) { + // Index has been assigned already + return; + } + sort.index = availableIndexes + .filter(index => index.key === sort.key) + .find(index => index.type === 'normal'); + // if (sort.index) { + // usingIndexes.push({ index: sort.index, description: `${sort.index.description} (for sorting)`}); + // } + }); } - /** - * @deprecated use `.filter` instead - */ - where(key, op, compare) { - return this.filter(key, op, compare); + // const usingIndexes = ourFilters.map(filter => filter.index).filter(index => index); + const indexDescriptions = usingIndexes.map(index => index.description).join(', '); + usingIndexes.length > 0 && api.logger.info(`Using indexes for query: ${indexDescriptions}`); + // Filters that should run on all nodes after indexed results: + const tableScanFilters = queryFilters.filter(filter => !filter.index); + // Check if there are filters that require an index to run (such as "fulltext:contains", and "geo:nearby" etc) + 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); } - /** - * Limits the number of query results - */ - take(n) { - this[_private].take = n; - return this; + // Check if the filters are using valid operators + const allowedTableScanOperators = ['<', '<=', '==', '!=', '>=', '>', 'like', '!like', 'in', '!in', 'matches', '!matches', 'between', '!between', 'has', '!has', 'contains', '!contains', 'exists', '!exists']; // DISABLED "custom" because it is not fully implemented and only works locally + for (let i = 0; i < tableScanFilters.length; i++) { + const f = tableScanFilters[i]; + if (!allowedTableScanOperators.includes(f.op)) { + return Promise.reject(new Error(`query contains unknown filter operator "${f.op}" on path "${path}", key "${f.key}"`)); + } } - /** - * Skips the first n query results - */ - skip(n) { - this[_private].skip = n; - return this; + // Check if the available indexes are sufficient for this wildcard query + if (isWildcardPath && tableScanFilters.length > 0) { + // There are unprocessed filters, which means the fields aren't indexed. + // We're not going to get all data of a wildcard path to query manually. + // Indexes must be created + 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); } - sort(key, ascending = true) { - if (!['string', 'number'].includes(typeof key)) { - throw 'key must be a string or number'; + // Run queries on available indexes + const indexScanPromises = []; + queryFilters.forEach(filter => { + if (filter.index && filter.indexUsage !== 'filter') { + let promise = filter.index.query(filter.op, filter.compare) + .then(results => { + var _a, _b; + (_a = options.eventHandler) === null || _a === void 0 ? void 0 : _a.call(options, { name: 'stats', type: 'index_query', source: filter.index.description, stats: results.stats }); + if (results.hints.length > 0) { + (_b = options.eventHandler) === null || _b === void 0 ? void 0 : _b.call(options, { name: 'hints', type: 'index_query', source: filter.index.description, hints: results.hints }); + } + return results; + }); + // Get other filters that can be executed on these indexed results (eg filters on included keys of the index) + const resultFilters = queryFilters.filter(f => f.index === filter.index && f.indexUsage === 'filter'); + if (resultFilters.length > 0) { + // Hook into the promise + promise = promise.then(results => { + resultFilters.forEach(filter => { + const { key, op, index } = filter; + let { compare } = filter; + if (typeof compare === 'string' && !index.caseSensitive) { + compare = compare.toLocaleLowerCase(index.textLocale); + } + results = results.filterMetadata(key, op, compare); + }); + return results; + }); + } + indexScanPromises.push(promise); } - this[_private].order.push({ key, ascending }); - return this; + }); + const stepsExecuted = { + filtered: queryFilters.length === 0, + skipped: query.skip === 0, + taken: query.take === 0, + sorted: querySort.length === 0, + preDataLoaded: false, + dataLoaded: false, + }; + if (queryFilters.length === 0 && query.take === 0) { + api.logger.warn(`Filterless queries must use .take to limit the results. Defaulting to 100 for query on path "${path}"`); + query.take = 100; } - /** - * @deprecated use `.sort` instead - */ - order(key, ascending = true) { - return this.sort(key, ascending); + if (querySort.length > 0 && querySort[0].index) { + const sortIndex = querySort[0].index; + const ascending = query.take < 0 ? !querySort[0].ascending : querySort[0].ascending; + if (queryFilters.length === 0 && querySort.slice(1).every(s => sortIndex.allMetadataKeys.includes(s.key))) { + api.logger.info(`Using index for sorting: ${sortIndex.description}`); + const metadataSort = querySort.slice(1).map(s => { + s.index = sortIndex; // Assign index to skip later processing of this sort operation + return { key: s.key, ascending: s.ascending }; + }); + const promise = sortIndex.take(query.skip, Math.abs(query.take), { ascending, metadataSort }) + .then(results => { + var _a, _b; + (_a = options.eventHandler) === null || _a === void 0 ? void 0 : _a.call(options, { name: 'stats', type: 'sort_index_take', source: sortIndex.description, stats: results.stats }); + if (results.hints.length > 0) { + (_b = options.eventHandler) === null || _b === void 0 ? void 0 : _b.call(options, { name: 'hints', type: 'sort_index_take', source: sortIndex.description, hints: results.hints }); + } + return results; + }); + indexScanPromises.push(promise); + stepsExecuted.skipped = true; + stepsExecuted.taken = true; + stepsExecuted.sorted = true; + } + // else if (queryFilters.every(f => [sortIndex.key, ...sortIndex.includeKeys].includes(f.key))) { + // TODO: If an index can be used for sorting, and all filter keys are included in its metadata: query the index! + // Implement: + // sortIndex.query(ourFilters); + // etc + // } } - get(optionsOrCallback, callback) { - if (!this.ref.db.isReady) { - const promise = this.ref.db.ready().then(() => this.get(optionsOrCallback, callback)); - return typeof optionsOrCallback !== 'function' && typeof callback !== 'function' ? promise : undefined; // only return promise if no callback is used + return Promise.all(indexScanPromises) + .then(async (indexResultSets) => { + // Merge all results in indexResultSets, get distinct nodes + 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; } - callback = - typeof optionsOrCallback === 'function' - ? optionsOrCallback - : typeof callback === 'function' - ? callback - : undefined; - const options = new QueryDataRetrievalOptions(typeof optionsOrCallback === 'object' ? optionsOrCallback : { snapshots: true, cache_mode: 'allow' }); - options.allow_cache = options.cache_mode !== 'bypass'; // Backward compatibility when using older acebase-client - options.eventHandler = ev => { - // TODO: implement context for query events - if (!this[_private].events[ev.name]) { - return false; + else if (indexResultSets.length > 1) { + indexResultSets.sort((a, b) => a.length < b.length ? -1 : 1); // Sort results, shortest result set first + const shortestSet = indexResultSets[0]; + const otherSets = indexResultSets.slice(1); + indexedResults = shortestSet.reduce((results, match) => { + // Check if the key is present in the other result sets + const result = { key: match.key, path: match.path, val: { [shortestSet.filterKey]: match.value } }; + const matchedInAllSets = otherSets.every(set => set.findIndex(m => m.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 (querySort.length === 0 || querySort.every(o => o.index)) { + // No sorting, or all sorts are on indexed keys. We can use current index results + stepsExecuted.preDataLoaded = true; + if (!stepsExecuted.sorted && querySort.length > 0) { + sortMatches(indexedResults); + } + stepsExecuted.sorted = true; + if (!stepsExecuted.skipped && query.skip > 0) { + indexedResults = query.take < 0 + ? indexedResults.slice(0, -query.skip) + : indexedResults.slice(query.skip); + } + if (!stepsExecuted.taken && query.take !== 0) { + indexedResults = query.take < 0 + ? indexedResults.slice(query.take) + : indexedResults.slice(0, query.take); + } + stepsExecuted.skipped = true; + stepsExecuted.taken = true; + if (!options.snapshots) { + return indexedResults; + } + // TODO: exclude already known key values, merge loaded with known + 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 = querySort.length > 0; + const childOptions = loadPartialResults + ? { include: querySort.map(order => order.key) } + : { include: options.include, exclude: options.exclude, child_objects: options.child_objects }; + return loadResultsData(indexedResults, childOptions) + .then(results => { + if (querySort.length > 0) { + sortMatches(results); + } + stepsExecuted.sorted = true; + if (query.skip > 0) { + results = query.take < 0 + ? results.slice(0, -query.skip) + : results.slice(query.skip); + } + if (query.take !== 0) { + results = query.take < 0 + ? results.slice(query.take) + : results.slice(0, query.take); + } + stepsExecuted.skipped = true; + stepsExecuted.taken = true; + if (options.snapshots && loadPartialResults) { + // Get the rest + return loadResultsData(results, { include: options.include, exclude: options.exclude, child_objects: options.child_objects }); + } + return results; + }); } - const listeners = this[_private].events[ev.name]; - if (typeof listeners !== 'object' || listeners.length === 0) { - return false; + else { + // No need to take further actions, return what we have now + return indexedResults; } - if (['add', 'change', 'remove'].includes(ev.name)) { - const eventData = { - name: ev.name, - ref: new DataReference(this.ref.db, ev.path), - }; - if (options.snapshots && ev.name !== 'remove') { - const val = db.types.deserialize(ev.path, ev.value); - eventData.snapshot = new data_snapshot_1.DataSnapshot(eventData.ref, val, false); + } + // If we get here, this is a query on a regular path (no wildcards) with additional non-indexed filters left, + // we can get child records from a single parent. Merge index results by key + let indexKeyFilter; + if (indexedResults.length > 0) { + indexKeyFilter = indexedResults.map(result => result.key); + } + let matches = []; + let preliminaryStop = false; + const loadPartialData = querySort.length > 0; + const childOptions = loadPartialData + ? { include: querySort.map(order => order.key) } + : { include: options.include, exclude: options.exclude, child_objects: options.child_objects }; + const batch = { + promises: [], + async add(promise) { + this.promises.push(promise); + if (this.promises.length >= 1000) { + await Promise.all(this.promises.splice(0)); } - ev = eventData; - } - listeners.forEach(callback => { - var _a, _b; - try { - callback(ev); + }, + }; + try { + await api.storage.getChildren(path, { keyFilter: indexKeyFilter, async: true }).next(child => { + if (child.type !== node_value_types_js_1.VALUE_TYPES.OBJECT) { + return; } - catch (err) { - this.ref.db.logger.error(`Error executing "${ev.name}" event handler of realtime query on path "${this.ref.path}": ${(_b = (_a = err === null || err === void 0 ? void 0 : err.stack) !== null && _a !== void 0 ? _a : err === null || err === void 0 ? void 0 : err.message) !== null && _b !== void 0 ? _b : err}`); + if (!child.address) { + // Currently only happens if object has no properties + // ({}, stored as a tiny_value in parent record). In that case, + // should it be matched in any query? -- That answer could be YES, when testing a property for !exists. Ignoring for now + return; + } + if (preliminaryStop) { + return false; + } + const matchNode = async () => { + const isMatch = await api.storage.matchNode(child.address.path, tableScanFilters); + if (!isMatch) { + return; + } + const childPath = child.address.path; + let result; + if (options.snapshots || querySort.length > 0) { + const node = await api.storage.getNode(childPath, childOptions); + result = { path: childPath, val: node.value }; + } + else { + result = { path: childPath }; + } + // If a maximumum number of results is requested, we can check if we can preliminary toss this result + // This keeps the memory space used limited to skip + take + // TODO: see if we can limit it to the max number of results returned (.take) + matches.push(result); + if (query.take !== 0 && matches.length > Math.abs(query.take) + query.skip) { + if (querySort.length > 0) { + // A query order has been set. If this value falls in between it can replace some other value + // matched before. + sortMatches(matches); + } + else if (query.take > 0) { + // No query order set, we can stop after 'take' + 'skip' results + preliminaryStop = true; // Flags the loop that no more nodes have to be checked + } + // const ascending = querySort.length === 0 || (query.take >= 0 ? querySort[0].ascending : !querySort[0].ascending); + // if (ascending) { + // matches.pop(); // ascending sort order, toss last value + // } + // else { + // matches.shift(); // descending, toss first value + // } + matches.pop(); // Always toss last value, results have been sorted already + } + }; + const p = batch.add(matchNode()); + if (p instanceof Promise) { + // If this returns a promise, child iteration should pause automatically + return p; } }); - }; - // Check if there are event listeners set for realtime changes - options.monitor = { add: false, change: false, remove: false }; - if (this[_private].events) { - if (this[_private].events['add'] && this[_private].events['add'].length > 0) { - options.monitor.add = true; - } - if (this[_private].events['change'] && this[_private].events['change'].length > 0) { - options.monitor.change = true; - } - if (this[_private].events['remove'] && this[_private].events['remove'].length > 0) { - options.monitor.remove = true; - } } - // Stop realtime results if they are still enabled on a previous .get on this instance - this.stop(); - // NOTE: returning promise here, regardless of callback argument. Good argument to refactor method to async/await soon - const db = this.ref.db; - return db.api.query(this.ref.path, this[_private], options) - .catch(err => { - throw new Error(err); - }) - .then(res => { - const { stop } = res; - let { results, context } = res; - this.stop = async () => { - await stop(); - }; - if (!('results' in res && 'context' in res)) { - console.warn('Query results missing context. Update your acebase and/or acebase-client packages'); - results = res, context = {}; - } - if (options.snapshots) { - const snaps = results.map(result => { - const val = db.types.deserialize(result.path, result.val); - return new data_snapshot_1.DataSnapshot(db.ref(result.path), val, false, undefined, context); - }); - return DataSnapshotsArray.from(snaps); - } - else { - const refs = results.map(path => db.ref(path)); - return DataReferencesArray.from(refs); - } - }) - .then(results => { - callback && callback(results); - return results; - }); - } - /** - * Stops a realtime query, no more notifications will be received. - */ - async stop() { - // Overridden by .get - } - /** - * Executes the query and returns references. Short for `.get({ snapshots: false })` - * @param callback callback to use instead of returning a promise - * @returns returns an Promise that resolves with an array of DataReferences, or void when using a callback - * @deprecated Use `find` instead - */ - getRefs(callback) { - return this.get({ snapshots: false }, callback); - } - /** - * Executes the query and returns an array of references. Short for `.get({ snapshots: false })` - */ - find() { - return this.get({ snapshots: false }); - } - /** - * Executes the query and returns the number of results - */ - async count() { - const refs = await this.find(); - return refs.length; - } - /** - * Executes the query and returns if there are any results - */ - async exists() { - const originalTake = this[_private].take; - const p = this.take(1).find(); - this.take(originalTake); - const refs = await p; - return refs.length !== 0; - } - /** - * Executes the query, removes all matches from the database - * @returns returns a Promise that resolves once all matches have been removed - */ - async remove(callback) { - const refs = await this.find(); - // Perform updates on each distinct parent collection (only 1 parent if this is not a wildcard path) - const parentUpdates = refs.reduce((parents, ref) => { - const parent = parents[ref.parent.path]; - if (!parent) { - parents[ref.parent.path] = [ref]; - } - else { - parent.push(ref); - } - return parents; - }, {}); - const db = this.ref.db; - const promises = Object.keys(parentUpdates).map(async (parentPath) => { - const updates = refs.reduce((updates, ref) => { - updates[ref.key] = null; - return updates; - }, {}); - const ref = db.ref(parentPath); - try { - await ref.update(updates); - return { ref, success: true }; - } - catch (error) { - return { ref, success: false, error }; + catch (reason) { + // No record? + if (!(reason instanceof node_errors_js_1.NodeNotFoundError)) { + api.logger.warn(`Error getting child stream: ${reason}`); } - }); - const results = await Promise.all(promises); - callback && callback(results); - return results; - } - on(event, callback) { - if (!this[_private].events[event]) { - this[_private].events[event] = []; + return []; } - this[_private].events[event].push(callback); - return this; - } - /** - * Unsubscribes from (a) previously added event(s) - * @param event Name of the event - * @param callback callback function to remove - * @returns returns reference to this query - */ - off(event, callback) { - if (typeof event === 'undefined') { - this[_private].events = {}; - return this; + // Done iterating all children, wait for all match promises to resolve + await Promise.all(batch.promises); + stepsExecuted.preDataLoaded = loadPartialData; + stepsExecuted.dataLoaded = !loadPartialData; + if (querySort.length > 0) { + sortMatches(matches); + } + stepsExecuted.sorted = true; + if (query.skip > 0) { + matches = query.take < 0 + ? matches.slice(0, -query.skip) + : matches.slice(query.skip); + } + stepsExecuted.skipped = true; + if (query.take !== 0) { + // (should not be necessary, basically it has already been done in the loop?) + matches = query.take < 0 + ? matches.slice(query.take) + : matches.slice(0, query.take); } - if (!this[_private].events[event]) { - return this; + stepsExecuted.taken = true; + if (!stepsExecuted.dataLoaded) { + matches = await loadResultsData(matches, { include: options.include, exclude: options.exclude, child_objects: options.child_objects }); + stepsExecuted.dataLoaded = true; } - if (typeof callback === 'undefined') { - delete this[_private].events[event]; - return this; + return matches; + }) + .then(matches => { + // Order the results + if (!stepsExecuted.sorted && querySort.length > 0) { + sortMatches(matches); } - const index = this[_private].events[event].indexOf(callback); - if (!~index) { - return this; + if (!options.snapshots) { + // Remove the loaded values from the results, because they were not requested (and aren't complete, we only have data of the sorted keys) + matches = matches.map(match => match.path); } - this[_private].events[event].splice(index, 1); - return this; - } - async forEach(callbackOrOptions, callback) { - let options; - if (typeof callbackOrOptions === 'function') { - callback = callbackOrOptions; + // Limit result set + if (!stepsExecuted.skipped && query.skip > 0) { + matches = query.take < 0 + ? matches.slice(0, -query.skip) + : matches.slice(query.skip); } - else { - options = callbackOrOptions; + if (!stepsExecuted.taken && query.take !== 0) { + matches = query.take < 0 + ? matches.slice(query.take) + : matches.slice(0, query.take); } - if (typeof callback !== 'function') { - throw new TypeError('No callback function given'); + // NEW: Check if this is a realtime query - future updates must send query result updates + if (options.monitor === true) { + options.monitor = { add: true, change: true, remove: true }; } - // Get all query results. This could be tweaked further using paging - const refs = await this.find(); - const summary = { - canceled: false, - total: refs.length, - processed: 0, - }; - // Iterate through all children until callback returns false - for (let i = 0; i < refs.length; i++) { - const ref = refs[i]; - // Get child data - const snapshot = await ref.get(options); - summary.processed++; - if (!snapshot.exists()) { - // Was removed in the meantime, skip - continue; + if (typeof options.monitor === 'object' && (options.monitor.add || options.monitor.change || options.monitor.remove)) { + // TODO: Refactor this to use 'mutations' event instead of 'notify_child_*' + const monitor = options.monitor; + const matchedPaths = options.snapshots ? matches.map(match => match.path) : matches.slice(); + const ref = api.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 = () => { + api.unsubscribe(ref.path, 'child_changed', childChangedCallback); + api.unsubscribe(ref.path, 'child_added', childAddedCallback); + api.unsubscribe(ref.path, 'notify_child_removed', childRemovedCallback); + }; + stop = async () => { stopMonitoring(); }; + const childChangedCallback = async (err, path, newValue, oldValue) => { + const wasMatch = matchedPaths.includes(path); + let keepMonitoring = true; + // check if the properties we already have match filters, + // and if we have to check additional properties + const checkKeys = []; + queryFilters.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 = queryFilters.filter(filter => filter.key === key); + return filters.every(filter => { + var _a; + if (((_a = filter.index) === null || _a === void 0 ? void 0 : _a.textLocaleKey) && !seenKeys.includes(filter.index.textLocaleKey)) { + // Can't check because localeKey is missing + missingKeys.push(filter.index.textLocaleKey); + return true; // so we'll know if all others did match + } + else if (allowedTableScanOperators.includes(filter.op)) { + return api.storage.test(newValue[key], filter.op, filter.compare); + } + else { + // specific index filter + return filter.index.test(newValue, filter.op, filter.compare); + } + }); + }); + if (isMatch) { + // Matches all checked (updated) keys. BUT. Did we have all data needed? + // If it was a match before, other properties don't matter because they didn't change and won't + // change the current outcome + missingKeys.push(...checkKeys.filter(key => !seenKeys.includes(key))); + // let promise = Promise.resolve(true); + if (!wasMatch && missingKeys.length > 0) { + // We have to check if this node becomes a match + const filterQueue = queryFilters.filter(f => missingKeys.includes(f.key)); + const simpleFilters = filterQueue.filter(f => allowedTableScanOperators.includes(f.op)); + const indexFilters = filterQueue.filter(f => !allowedTableScanOperators.includes(f.op)); + if (simpleFilters.length > 0) { + isMatch = await api.storage.matchNode(path, simpleFilters); + } + if (isMatch && indexFilters.length > 0) { + // TODO: ask index what keys to load (eg: FullTextIndex might need key specified by localeKey) + const keysToLoad = indexFilters.reduce((keys, filter) => { + if (!keys.includes(filter.key)) { + keys.push(filter.key); + } + if (filter.index instanceof index_js_1.FullTextIndex && filter.index.config.localeKey && !keys.includes(filter.index.config.localeKey)) { + keys.push(filter.index.config.localeKey); + } + return keys; + }, []); + const node = await api.storage.getNode(path, { include: keysToLoad }); + if (node.value === null) { + return false; + } + isMatch = indexFilters.every(filter => filter.index.test(node.value, filter.op, filter.compare)); + } + } + } + if (isMatch) { + if (!wasMatch) { + addMatch(path); + } + // load missing data if snapshots are requested + if (options.snapshots) { + const loadOptions = { include: options.include, exclude: options.exclude, child_objects: options.child_objects }; + const node = await api.storage.getNode(path, loadOptions); + newValue = node.value; + } + if (wasMatch && monitor.change) { + keepMonitoring = options.eventHandler({ name: 'change', path, value: newValue }) !== false; + } + else if (!wasMatch && monitor.add) { + keepMonitoring = options.eventHandler({ name: 'add', path, value: newValue }) !== false; + } + } + else if (wasMatch) { + removeMatch(path); + if (monitor.remove) { + keepMonitoring = options.eventHandler({ name: 'remove', path: path, value: oldValue }) !== false; + } + } + if (keepMonitoring === false) { + stopMonitoring(); + } + }; + const childAddedCallback = (err, path, newValue) => { + const isMatch = queryFilters.every(filter => { + if (allowedTableScanOperators.includes(filter.op)) { + return api.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 (monitor.add) { + keepMonitoring = options.eventHandler({ name: 'add', path: path, value: options.snapshots ? newValue : null }) !== false; + } + } + if (keepMonitoring === false) { + stopMonitoring(); + } + }; + const childRemovedCallback = (err, path, newValue, oldValue) => { + let keepMonitoring = true; + removeMatch(path); + if (monitor.remove) { + keepMonitoring = options.eventHandler({ name: 'remove', path: path, value: options.snapshots ? oldValue : null }) !== false; + } + if (keepMonitoring === false) { + stopMonitoring(); + } + }; + if (options.monitor.add || options.monitor.change || options.monitor.remove) { + // Listen for child_changed events + api.subscribe(ref.path, 'child_changed', childChangedCallback); } - // Run callback - const result = await callback(snapshot); - if (result === false) { - summary.canceled = true; - break; // Stop looping + if (options.monitor.remove) { + api.subscribe(ref.path, 'notify_child_removed', childRemovedCallback); + } + if (options.monitor.add) { + api.subscribe(ref.path, 'child_added', childAddedCallback); } } - return summary; - } + return { results: matches, context, stop }; + }); } -exports.DataReferenceQuery = DataReferenceQuery; -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()); - } +exports.executeQuery = executeQuery; + +},{"./async-task-batch.js":32,"./data-index/index.js":34,"./node-errors.js":38,"./node-value-types.js":41,"acebase-core":12}],45:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.AceBaseStorage = exports.AceBaseStorageSettings = void 0; +const not_supported_js_1 = require("../../not-supported.js"); +/** + * Not supported in browser context + */ +class AceBaseStorageSettings extends not_supported_js_1.NotSupported { } -exports.DataSnapshotsArray = DataSnapshotsArray; -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); - } +exports.AceBaseStorageSettings = AceBaseStorageSettings; +/** + * Not supported in browser context + */ +class AceBaseStorage extends not_supported_js_1.NotSupported { } -exports.DataReferencesArray = DataReferencesArray; +exports.AceBaseStorage = AceBaseStorage; -},{"./data-proxy":41,"./data-snapshot":43,"./id":45,"./optional-observable":48,"./path-info":50,"./subscription":58}],43:[function(require,module,exports){ +},{"../../not-supported.js":42}],46:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.MutationsDataSnapshot = exports.DataSnapshot = void 0; -const path_info_1 = require("./path-info"); -function getChild(snapshot, path, previous = false) { - if (!snapshot.exists()) { - return null; +exports.createIndex = void 0; +const acebase_core_1 = require("acebase-core"); +const index_js_1 = require("../data-index/index.js"); +const index_js_2 = require("../promise-fs/index.js"); +/** +* Creates an index on specified path and key(s) +* @param path location of objects to be indexed. Eg: "users" to index all children of the "users" node; or "chats/*\/members" to index all members of all chats +* @param key for now - one key to index. Once our B+tree implementation supports nested trees, we can allow multiple fields +*/ +async function createIndex(context, path, key, options) { + if (!context.storage.indexes.supported) { + throw new Error('Indexes are not supported in current environment because it requires Node.js fs'); } - let child = previous ? snapshot.previous() : snapshot.val(); - if (typeof path === 'number') { - return child[path]; + // path = path.replace(/\/\*$/, ""); // Remove optional trailing "/*" + const { ipc, logger, indexes, storage } = context; + const rebuild = options && options.rebuild === true; + const indexType = (options && options.type) || 'normal'; + let includeKeys = (options && options.include) || []; + if (typeof includeKeys === 'string') { + includeKeys = [includeKeys]; } - path_info_1.PathInfo.getPathKeys(path).every(key => { - child = child[key]; - return typeof child !== 'undefined'; + 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 && options.config) { + // Additional index config params are not saved to index files, apply them to the in-memory index now + existingIndex.config = options.config; + } + if (existingIndex && rebuild !== true) { + logger.info(`Index on "/${path}/*/${key}" already exists`.colorize(acebase_core_1.ColorStyle.inverse)); + return existingIndex; + } + if (!ipc.isMaster) { + // Pass create request to master + const result = await ipc.sendRequest({ type: 'index.create', path, key, options }); + if (result.ok) { + return storage.indexes.add(result.fileName); + } + throw new Error(result.reason); + } + await index_js_2.pfs.mkdir(`${storage.settings.path}/${storage.name}.acebase`).catch(err => { + if (err.code !== 'EEXIST') { + throw err; + } }); - return child || null; -} -function getChildren(snapshot) { - if (!snapshot.exists()) { - return []; + const index = existingIndex || (() => { + const { include, caseSensitive, textLocale, textLocaleKey } = options; + const indexOptions = { include, caseSensitive, textLocale, textLocaleKey }; + switch (indexType) { + case 'array': return new index_js_1.ArrayIndex(storage, path, key, Object.assign({}, indexOptions)); + case 'fulltext': return new index_js_1.FullTextIndex(storage, path, key, Object.assign(Object.assign({}, indexOptions), { config: options.config })); + case 'geo': return new index_js_1.GeoIndex(storage, path, key, Object.assign({}, indexOptions)); + default: return new index_js_1.DataIndex(storage, path, key, Object.assign({}, indexOptions)); + } + })(); + if (!existingIndex) { + indexes.push(index); } - const value = snapshot.val(); - if (value instanceof Array) { - return new Array(value.length).map((v, i) => i); + try { + await index.build(); } - if (typeof value === 'object') { - return Object.keys(value); + catch (err) { + context.logger.error(`Index build on "/${path}/*/${key}" failed: ${err.message} (code: ${err.code})`.colorize(acebase_core_1.ColorStyle.red)); + if (!existingIndex) { + // Only remove index if we added it. Build may have failed because someone tried creating the index more than once, or rebuilding it while it was building... + indexes.splice(indexes.indexOf(index), 1); + } + throw err; } - return []; + ipc.sendNotification({ type: 'index.created', fileName: index.fileName, path, key, options }); + return index; } -class DataSnapshot { +exports.createIndex = createIndex; + +},{"../data-index/index.js":34,"../promise-fs/index.js":43,"acebase-core":12}],47:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.CustomStorageHelpers = void 0; +const acebase_core_1 = require("acebase-core"); +/** + * Helper functions to build custom storage classes with + */ +class CustomStorageHelpers { /** - * Indicates whether the node exists in the database + * Helper function that returns a SQL where clause for all children of given path + * @param path Path to get children of + * @param columnName Name of the Path column in your SQL db, default is 'path' + * @returns Returns the SQL where clause */ - exists() { return false; } + 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; + } /** - * Creates a new DataSnapshot instance + * Helper function that returns a regular expression to test if paths are children of the given path + * @param path Path to test children of + * @returns Returns regular expression to test paths with */ - constructor(ref, value, isRemoved = false, prevValue, context) { - this.ref = ref; - this.val = () => { return value; }; - this.previous = () => { return prevValue; }; - this.exists = () => { - if (isRemoved) { - return false; - } - return value !== null && typeof value !== 'undefined'; - }; - this.context = () => { return context || {}; }; + static ChildPathsRegex(path) { + return new RegExp(`^${path}(?:/[^/[]+|\\[[0-9]+\\])$`); } /** - * Creates a `DataSnapshot` instance - * @internal (for internal use) + * Helper function that returns a SQL where clause for all descendants of given path + * @param path Path to get descendants of + * @param columnName Name of the Path column in your SQL db, default is 'path' + * @returns Returns the SQL where clause */ - static for(ref, value) { - return new DataSnapshot(ref, value); - } - child(path) { - // Create new snapshot for child data - const val = getChild(this, path, false); - const prev = getChild(this, path, true); - return new DataSnapshot(this.ref.child(path), val, false, prev); + static DescendantPathsSql(path, columnName = 'path') { + const where = path === '' + ? `${columnName} <> ''` + : `${columnName} LIKE '${path}/%' OR ${columnName} LIKE '${path}[%'`; + return where; } /** - * Checks if the snapshot's value has a child with the given key or path - * @param path child key or path + * Helper function that returns a regular expression to test if paths are descendants of the given path + * @param path Path to test descendants of + * @returns Returns regular expression to test paths with */ - hasChild(path) { - return getChild(this, path) !== null; + static DescendantPathsRegex(path) { + return new RegExp(`^${path}(?:/[^/[]+|\\[[0-9]+\\])`); } /** - * Indicates whether the the snapshot's value has any child nodes + * PathInfo helper class. Can be used to extract keys from a given path, get parent paths, check if a path is a child or descendant of other path etc + * @example + * var pathInfo = CustomStorage.PathInfo.get('my/path/to/data'); + * pathInfo.key === 'data'; + * pathInfo.parentPath === 'my/path/to'; + * pathInfo.pathKeys; // ['my','path','to','data']; + * pathInfo.isChildOf('my/path/to') === true; + * pathInfo.isDescendantOf('my/path') === true; + * pathInfo.isParentOf('my/path/to/data/child') === true; + * pathInfo.isAncestorOf('my/path/to/data/child/grandchild') === true; + * pathInfo.childPath('child') === 'my/path/to/data/child'; + * pathInfo.childPath(0) === 'my/path/to/data[0]'; */ - hasChildren() { - return getChildren(this).length > 0; + static get PathInfo() { + return acebase_core_1.PathInfo; + } +} +exports.CustomStorageHelpers = CustomStorageHelpers; + +},{"acebase-core":12}],48:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.CustomStorage = exports.CustomStorageNodeInfo = exports.CustomStorageNodeAddress = exports.CustomStorageSettings = exports.CustomStorageTransaction = exports.ICustomStorageNode = exports.ICustomStorageNodeMetaData = exports.CustomStorageHelpers = void 0; +const acebase_core_1 = require("acebase-core"); +const { compareValues } = acebase_core_1.Utils; +const node_info_js_1 = require("../../node-info.js"); +const node_lock_js_1 = require("../../node-lock.js"); +const node_value_types_js_1 = require("../../node-value-types.js"); +const node_errors_js_1 = require("../../node-errors.js"); +const index_js_1 = require("../index.js"); +const helpers_js_1 = require("./helpers.js"); +const node_address_js_1 = require("../../node-address.js"); +const assert_js_1 = require("../../assert.js"); +var helpers_js_2 = require("./helpers.js"); +Object.defineProperty(exports, "CustomStorageHelpers", { enumerable: true, get: function () { return helpers_js_2.CustomStorageHelpers; } }); +/** Interface for metadata being stored for nodes */ +class ICustomStorageNodeMetaData { + constructor() { + /** cuid (time sortable revision id). Nodes stored in the same operation share this id */ + this.revision = ''; + /** Number of revisions, starting with 1. Resets to 1 after deletion and recreation */ + this.revision_nr = 0; + /** Creation date/time in ms since epoch UTC */ + this.created = 0; + /** Last modification date/time in ms since epoch UTC */ + this.modified = 0; + /** Type of the node's value. 1=object, 2=array, 3=number, 4=boolean, 5=string, 6=date, 7=reserved, 8=binary, 9=reference */ + this.type = 0; + } +} +exports.ICustomStorageNodeMetaData = ICustomStorageNodeMetaData; +/** Interface for metadata combined with a stored value */ +class ICustomStorageNode extends ICustomStorageNodeMetaData { + constructor() { + super(); + /** only Object, Array, large string and binary values. */ + this.value = null; } +} +exports.ICustomStorageNode = ICustomStorageNode; +/** Enables get/set/remove operations to be wrapped in transactions to improve performance and reliability. */ +class CustomStorageTransaction { /** - * The number of child nodes in this snapshot + * @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 */ - numChildren() { - return getChildren(this).length; + constructor(target) { + this.production = false; // dev mode by default + this.target = { + get originalPath() { return target.path; }, + path: target.path, + get write() { return target.write; }, + }; + this.id = acebase_core_1.ID.generate(); } /** - * Runs a callback function for each child node in this snapshot until the callback returns false - * @param callback function that is called with a snapshot of each child node in this snapshot. - * Must return a boolean value that indicates whether to continue iterating or not. + * Returns the number of children stored in their own records. This implementation uses `childrenOf` to count, override if storage supports a quicker way. + * Eg: For SQL databases, you can implement this with a single query like `SELECT count(*) FROM nodes WHERE ${CustomStorageHelpers.ChildPathsSql(path)}` + * @param path + * @returns Returns a promise that resolves with the number of children */ - forEach(callback) { - const value = this.val(); - const prev = this.previous(); - return getChildren(this).every((key) => { - const snap = new DataSnapshot(this.ref.child(key), value[key], false, prev[key]); - return callback(snap); - }); + async getChildCount(path) { + let childCount = 0; + await this.childrenOf(path, { metadata: false, value: false }, () => { childCount++; return false; }); + return childCount; } /** - * The key of the node's path + * NOT USED YET + * Default implementation of getMultiple that executes .get for each given path. Override for custom logic + * @param paths + * @returns Returns promise with a Map of paths to nodes */ - get key() { return this.ref.key; } -} -exports.DataSnapshot = DataSnapshot; -class MutationsDataSnapshot extends DataSnapshot { - constructor(ref, mutations, context) { - super(ref, mutations, false, undefined, context); - /** - * Don't use this to get previous values of mutated nodes. - * Use `.previous` properties on the individual child snapshots instead. - * @throws Throws an error if you do use it. - */ - this.previous = () => { throw new Error('Iterate values to get previous values for each mutation'); }; - this.val = (warn = true) => { - if (warn) { - console.warn('Unless you know what you are doing, it is best not to use the value of a mutations snapshot directly. Use child methods and forEach to iterate the mutations instead'); - } - return mutations; - }; + async getMultiple(paths) { + const map = new Map(); + await Promise.all(paths.map(path => this.get(path).then(val => map.set(path, val)))); + return map; } /** - * Runs a callback function for each mutation in this snapshot until the callback returns false - * @param callback function that is called with a snapshot of each mutation in this snapshot. Must return a boolean value that indicates whether to continue iterating or not. - * @returns Returns whether every child was interated + * NOT USED YET + * Default implementation of setMultiple that executes .set for each given path. Override for custom logic + * @param nodes */ - forEach(callback) { - const mutations = this.val(false); - return mutations.every(mutation => { - const ref = mutation.target.reduce((ref, key) => ref.child(key), this.ref); - const snap = new DataSnapshot(ref, mutation.val, false, mutation.prev); - return callback(snap); - }); - } - child(index) { - if (typeof index !== 'number') { - throw new Error('child index must be a number'); - } - const mutation = this.val(false)[index]; - const ref = mutation.target.reduce((ref, key) => ref.child(key), this.ref); - return new DataSnapshot(ref, mutation.val, false, mutation.prev); - } -} -exports.MutationsDataSnapshot = MutationsDataSnapshot; - -},{"./path-info":50}],44:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.DebugLogger = void 0; -const process_1 = require("./process"); -// eslint-disable-next-line @typescript-eslint/no-empty-function -const noop = () => { }; -class DebugLogger { - constructor(level = 'log', prefix = '') { - this.level = level; - this.prefix = prefix; - this.setLevel(level); + async setMultiple(nodes) { + await Promise.all(nodes.map(({ path, node }) => this.set(path, node))); } - setLevel(level) { - const prefix = this.prefix ? this.prefix + ' %s' : ''; - this.verbose = ['verbose'].includes(level) ? prefix ? console.log.bind(console, prefix) : console.log.bind(console) : noop; - this.trace = ['verbose'].includes(level) ? prefix ? console.log.bind(console, prefix) : console.log.bind(console) : noop; - this.debug = ['verbose'].includes(level) ? prefix ? console.log.bind(console, prefix) : console.log.bind(console) : noop; - this.log = ['verbose', 'log'].includes(level) ? prefix ? console.log.bind(console, prefix) : console.log.bind(console) : noop; - this.info = ['verbose', 'log'].includes(level) ? prefix ? console.log.bind(console, prefix) : console.log.bind(console) : noop; - this.warn = ['verbose', 'log', 'warn'].includes(level) ? prefix ? console.warn.bind(console, prefix) : console.warn.bind(console) : noop; - this.error = ['verbose', 'log', 'warn', 'error'].includes(level) ? prefix ? console.error.bind(console, prefix) : console.error.bind(console) : noop; - this.fatal = ['verbose', 'log', 'warn', 'error'].includes(level) ? prefix ? console.error.bind(console, prefix) : console.error.bind(console) : noop; - this.write = (text) => { - const isRunKit = typeof process_1.default !== 'undefined' && process_1.default.env && typeof process_1.default.env.RUNKIT_ENDPOINT_PATH === 'string'; - if (text && isRunKit) { - text.split('\n').forEach(line => console.log(line)); // Logs each line separately - } - else { - console.log(text); - } - }; + /** + * Default implementation of removeMultiple that executes .remove for each given path. Override for custom logic + * @param paths + */ + async removeMultiple(paths) { + await Promise.all(paths.map(path => this.remove(path))); } -} -exports.DebugLogger = DebugLogger; - -},{"./process":52}],45:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.ID = void 0; -const cuid_1 = require("./cuid"); -// const uuid62 = require('uuid62'); -let timeBias = 0; -class ID { /** - * (for internal use) - * bias in milliseconds to adjust generated cuid timestamps with + * @returns {Promise} */ - static set timeBias(bias) { - if (typeof bias !== 'number') { - return; + async commit() { throw new Error(`CustomStorageTransaction.rollback must be overridden by subclass`); } + /** + * 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 targetPath + */ + async moveToParentPath(targetPath) { + const currentPath = (this._lock && this._lock.path) || this.target.path; + if (currentPath === targetPath) { + return targetPath; // Already on the right path } - timeBias = bias; - } - static generate() { - // Could also use https://www.npmjs.com/package/pushid for Firebase style 20 char id's - return (0, cuid_1.default)(timeBias).slice(1); // Cuts off the always leading 'c' - // return uuid62.v1(); + const pathInfo = helpers_js_1.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; } } -exports.ID = ID; - -},{"./cuid":39}],46:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.ObjectCollection = exports.PartialArray = exports.SimpleObservable = exports.SchemaDefinition = exports.Colorize = exports.ColorStyle = exports.SimpleEventEmitter = exports.SimpleCache = exports.ascii85 = exports.PathInfo = exports.Utils = exports.TypeMappings = exports.Transport = exports.EventSubscription = exports.EventPublisher = exports.EventStream = exports.PathReference = exports.ID = exports.DebugLogger = exports.OrderedCollectionProxy = exports.proxyAccess = exports.MutationsDataSnapshot = exports.DataSnapshot = exports.DataReferencesArray = exports.DataSnapshotsArray = exports.QueryDataRetrievalOptions = exports.DataRetrievalOptions = exports.DataReferenceQuery = exports.DataReference = exports.Api = exports.AceBaseBaseSettings = exports.AceBaseBase = void 0; -var acebase_base_1 = require("./acebase-base"); -Object.defineProperty(exports, "AceBaseBase", { enumerable: true, get: function () { return acebase_base_1.AceBaseBase; } }); -Object.defineProperty(exports, "AceBaseBaseSettings", { enumerable: true, get: function () { return acebase_base_1.AceBaseBaseSettings; } }); -var api_1 = require("./api"); -Object.defineProperty(exports, "Api", { enumerable: true, get: function () { return api_1.Api; } }); -var data_reference_1 = require("./data-reference"); -Object.defineProperty(exports, "DataReference", { enumerable: true, get: function () { return data_reference_1.DataReference; } }); -Object.defineProperty(exports, "DataReferenceQuery", { enumerable: true, get: function () { return data_reference_1.DataReferenceQuery; } }); -Object.defineProperty(exports, "DataRetrievalOptions", { enumerable: true, get: function () { return data_reference_1.DataRetrievalOptions; } }); -Object.defineProperty(exports, "QueryDataRetrievalOptions", { enumerable: true, get: function () { return data_reference_1.QueryDataRetrievalOptions; } }); -Object.defineProperty(exports, "DataSnapshotsArray", { enumerable: true, get: function () { return data_reference_1.DataSnapshotsArray; } }); -Object.defineProperty(exports, "DataReferencesArray", { enumerable: true, get: function () { return data_reference_1.DataReferencesArray; } }); -var data_snapshot_1 = require("./data-snapshot"); -Object.defineProperty(exports, "DataSnapshot", { enumerable: true, get: function () { return data_snapshot_1.DataSnapshot; } }); -Object.defineProperty(exports, "MutationsDataSnapshot", { enumerable: true, get: function () { return data_snapshot_1.MutationsDataSnapshot; } }); -var data_proxy_1 = require("./data-proxy"); -Object.defineProperty(exports, "proxyAccess", { enumerable: true, get: function () { return data_proxy_1.proxyAccess; } }); -Object.defineProperty(exports, "OrderedCollectionProxy", { enumerable: true, get: function () { return data_proxy_1.OrderedCollectionProxy; } }); -var debug_1 = require("./debug"); -Object.defineProperty(exports, "DebugLogger", { enumerable: true, get: function () { return debug_1.DebugLogger; } }); -var id_1 = require("./id"); -Object.defineProperty(exports, "ID", { enumerable: true, get: function () { return id_1.ID; } }); -var path_reference_1 = require("./path-reference"); -Object.defineProperty(exports, "PathReference", { enumerable: true, get: function () { return path_reference_1.PathReference; } }); -var subscription_1 = require("./subscription"); -Object.defineProperty(exports, "EventStream", { enumerable: true, get: function () { return subscription_1.EventStream; } }); -Object.defineProperty(exports, "EventPublisher", { enumerable: true, get: function () { return subscription_1.EventPublisher; } }); -Object.defineProperty(exports, "EventSubscription", { enumerable: true, get: function () { return subscription_1.EventSubscription; } }); -exports.Transport = require("./transport"); -var type_mappings_1 = require("./type-mappings"); -Object.defineProperty(exports, "TypeMappings", { enumerable: true, get: function () { return type_mappings_1.TypeMappings; } }); -exports.Utils = require("./utils"); -var path_info_1 = require("./path-info"); -Object.defineProperty(exports, "PathInfo", { enumerable: true, get: function () { return path_info_1.PathInfo; } }); -var ascii85_1 = require("./ascii85"); -Object.defineProperty(exports, "ascii85", { enumerable: true, get: function () { return ascii85_1.ascii85; } }); -var simple_cache_1 = require("./simple-cache"); -Object.defineProperty(exports, "SimpleCache", { enumerable: true, get: function () { return simple_cache_1.SimpleCache; } }); -var simple_event_emitter_1 = require("./simple-event-emitter"); -Object.defineProperty(exports, "SimpleEventEmitter", { enumerable: true, get: function () { return simple_event_emitter_1.SimpleEventEmitter; } }); -var simple_colors_1 = require("./simple-colors"); -Object.defineProperty(exports, "ColorStyle", { enumerable: true, get: function () { return simple_colors_1.ColorStyle; } }); -Object.defineProperty(exports, "Colorize", { enumerable: true, get: function () { return simple_colors_1.Colorize; } }); -var schema_1 = require("./schema"); -Object.defineProperty(exports, "SchemaDefinition", { enumerable: true, get: function () { return schema_1.SchemaDefinition; } }); -var simple_observable_1 = require("./simple-observable"); -Object.defineProperty(exports, "SimpleObservable", { enumerable: true, get: function () { return simple_observable_1.SimpleObservable; } }); -var partial_array_1 = require("./partial-array"); -Object.defineProperty(exports, "PartialArray", { enumerable: true, get: function () { return partial_array_1.PartialArray; } }); -const object_collection_1 = require("./object-collection"); -Object.defineProperty(exports, "ObjectCollection", { enumerable: true, get: function () { return object_collection_1.ObjectCollection; } }); - -},{"./acebase-base":35,"./api":36,"./ascii85":37,"./data-proxy":41,"./data-reference":42,"./data-snapshot":43,"./debug":44,"./id":45,"./object-collection":47,"./partial-array":49,"./path-info":50,"./path-reference":51,"./schema":53,"./simple-cache":54,"./simple-colors":55,"./simple-event-emitter":56,"./simple-observable":57,"./subscription":58,"./transport":59,"./type-mappings":60,"./utils":61}],47:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.ObjectCollection = void 0; -const id_1 = require("./id"); +exports.CustomStorageTransaction = CustomStorageTransaction; /** - * Convenience interface for defining an object collection - * @example - * type ChatMessage = { - * text: string, uid: string, sent: Date - * } - * type Chat = { - * title: text - * messages: ObjectCollection - * } + * 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. */ -class ObjectCollection { - /** - * Converts and array of values into an object collection, generating a unique key for each item in the array - * @param array - * @example - * const array = [ - * { title: "Don't make me think!", author: "Steve Krug" }, - * { title: "The tipping point", author: "Malcolm Gladwell" } - * ]; - * - * // Convert: - * const collection = ObjectCollection.from(array); - * // --> { - * // kh1x3ygb000120r7ipw6biln: { - * // title: "Don't make me think!", - * // author: "Steve Krug" - * // }, - * // kh1x3ygb000220r757ybpyec: { - * // title: "The tipping point", - * // author: "Malcolm Gladwell" - * // } - * // } - * - * // Now it's easy to add them to the db: - * db.ref('books').update(collection); - */ - static from(array) { - const collection = {}; - array.forEach(child => { - collection[id_1.ID.generate()] = child; - }); - return collection; +class CustomStorageSettings extends index_js_1.StorageSettings { + constructor(settings) { + super(settings); + /** + * Whether default node locking should be used. + * Set to false if your storage backend disallows multiple simultanious write transactions. + * Set to true if your storage backend does not support transactions (eg LocalStorage) or allows + * multiple simultanious write transactions (eg AceBase binary). + * @default true + */ + this.locking = true; + if (typeof settings !== 'object') { + throw new Error('settings missing'); + } + 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; + if (this.locking) { + this.lockTimeout = typeof settings.lockTimeout === 'number' ? settings.lockTimeout : 120; + } + this.ready = settings.ready; + // Hijack getTransaction to add locking + const useLocking = this.locking; + const nodeLocker = useLocking ? new node_lock_js_1.NodeLocker(console, this.lockTimeout) : null; + this.getTransaction = async ({ path, write }) => { + // console.log(`${write ? 'WRITE' : 'READ'} transaction requested for path "${path}"`) + const transaction = await settings.getTransaction({ path, write }); + (0, assert_js_1.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; + }; } } -exports.ObjectCollection = ObjectCollection; - -},{"./id":45}],48:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.setObservable = exports.getObservable = void 0; -const simple_observable_1 = require("./simple-observable"); -const utils_1 = require("./utils"); -let _shimRequested = false; -let _observable; -(async () => { - // Try pre-loading rxjs Observable - // Test availability in global scope first - const global = (0, utils_1.getGlobalObject)(); - if (typeof global.Observable !== 'undefined') { - _observable = global.Observable; - return; +exports.CustomStorageSettings = CustomStorageSettings; +class CustomStorageNodeAddress { + constructor(containerPath) { + this.path = containerPath; } - // Try importing it from dependencies - try { - const { Observable } = await Promise.resolve().then(() => require('rxjs')); - _observable = Observable; +} +exports.CustomStorageNodeAddress = CustomStorageNodeAddress; +class CustomStorageNodeInfo extends node_info_js_1.NodeInfo { + constructor(info) { + super(info); + this.revision = info.revision; + this.revision_nr = info.revision_nr; + this.created = info.created; + this.modified = info.modified; } - catch (_a) { - // rxjs Observable not available, setObservable must be used if usage of SimpleObservable is not desired - _observable = simple_observable_1.SimpleObservable; +} +exports.CustomStorageNodeInfo = CustomStorageNodeInfo; +class CustomStorage extends index_js_1.Storage { + constructor(dbname, settings, env) { + super(dbname, settings, env); + this._customImplementation = settings; + this._init(); } -})(); -function getObservable() { - if (_observable === simple_observable_1.SimpleObservable && !_shimRequested) { - console.warn('Using AceBase\'s simple Observable implementation because rxjs is not available. ' + - 'Add it to your project with "npm install rxjs", add it to AceBase using db.setObservable(Observable), ' + - 'or call db.setObservable("shim") to suppress this warning'); + async _init() { + this.logger.info(`Database "${this.name}" details:`.colorize(acebase_core_1.ColorStyle.dim)); + this.logger.info(`- Type: CustomStorage`.colorize(acebase_core_1.ColorStyle.dim)); + this.logger.info(`- Path: ${this.settings.path}`.colorize(acebase_core_1.ColorStyle.dim)); + this.logger.info(`- Max inline value size: ${this.settings.maxInlineValueSize}`.colorize(acebase_core_1.ColorStyle.dim)); + this.logger.info(`- Autoremove undefined props: ${this.settings.removeVoidProperties}`.colorize(acebase_core_1.ColorStyle.dim)); + // Create root node if it's not there yet + 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'); } - if (_observable) { - return _observable; + throwImplementationError(message) { + throw new Error(`CustomStorage "${this._customImplementation.name}" ${message}`); } - throw new Error('RxJS Observable could not be loaded. '); -} -exports.getObservable = getObservable; -function setObservable(Observable) { - if (Observable === 'shim') { - _observable = simple_observable_1.SimpleObservable; - _shimRequested = true; + _storeNode(path, node, options) { + // serialize the value to store + 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: node_value_types_js_1.VALUE_TYPES.DATETIME, value: val.getTime() }; + } + else if (val instanceof acebase_core_1.PathReference) { + return { type: node_value_types_js_1.VALUE_TYPES.REFERENCE, value: val.path }; + } + else if (val instanceof ArrayBuffer) { + return { type: node_value_types_js_1.VALUE_TYPES.BINARY, value: acebase_core_1.ascii85.encode(val) }; + } + else if (typeof val === 'object') { + (0, assert_js_1.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 === node_value_types_js_1.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 < node.value.length; i++) { + obj[i] = node.value[i]; + } + node.value = obj; + } + if (node.type === node_value_types_js_1.VALUE_TYPES.BINARY && typeof node.value !== 'string') { + console.warn(`Unprocessed binary value. ${unprocessed}`); + node.value = acebase_core_1.ascii85.encode(node.value); + } + if (node.type === node_value_types_js_1.VALUE_TYPES.REFERENCE && node.value instanceof acebase_core_1.PathReference) { + console.warn(`Unprocessed path reference. ${unprocessed}`); + node.value = node.value.path; + } + if ([node_value_types_js_1.VALUE_TYPES.OBJECT, node_value_types_js_1.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 => { + node.value[key] = getTypedChildValue(original[key]); + }); + } + return options.transaction.set(path, node); } - else { - _observable = Observable; + _processReadNodeValue(node) { + const getTypedChildValue = (val) => { + // Typed value stored in parent record + if (val.type === node_value_types_js_1.VALUE_TYPES.BINARY) { + // binary stored in a parent record as a string + return acebase_core_1.ascii85.decode(val.value); + } + else if (val.type === node_value_types_js_1.VALUE_TYPES.DATETIME) { + // Date value stored as number + return new Date(val.value); + } + else if (val.type === node_value_types_js_1.VALUE_TYPES.REFERENCE) { + // Path reference stored as string + return new acebase_core_1.PathReference(val.value); + } + else { + throw new Error(`Unhandled child value type ${val.type}`); + } + }; + switch (node.type) { + case node_value_types_js_1.VALUE_TYPES.ARRAY: + case node_value_types_js_1.VALUE_TYPES.OBJECT: { + // check if any value needs to be converted + // NOTE: Arrays are stored with numeric properties + const obj = node.value; + Object.keys(obj).forEach(key => { + const item = obj[key]; + if (typeof item === 'object' && 'type' in item) { + obj[key] = getTypedChildValue(item); + } + }); + node.value = obj; + break; + } + case node_value_types_js_1.VALUE_TYPES.BINARY: { + node.value = acebase_core_1.ascii85.decode(node.value); + break; + } + case node_value_types_js_1.VALUE_TYPES.REFERENCE: { + node.value = new acebase_core_1.PathReference(node.value); + break; + } + case node_value_types_js_1.VALUE_TYPES.STRING: { + // No action needed + // node.value = node.value; + break; + } + default: + throw new Error(`Invalid standalone record value type`); // should never happen + } } -} -exports.setObservable = setObservable; - -},{"./simple-observable":57,"./utils":61,"rxjs":62}],49:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.PartialArray = void 0; -/** - * Sparse/partial array converted to a serializable object. Use `Object.keys(sparseArray)` and `Object.values(sparseArray)` to iterate its indice and/or values - */ -class PartialArray { - constructor(sparseArray) { - if (sparseArray instanceof Array) { - for (let i = 0; i < sparseArray.length; i++) { - if (typeof sparseArray[i] !== 'undefined') { - this[i] = sparseArray[i]; + async _readNode(path, options) { + // deserialize a stored value (always an object with "type", "value", "revision", "revision_nr", "created", "modified") + const node = await options.transaction.get(path); + if (node === null) { + return null; + } + if (typeof node !== 'object') { + this.throwImplementationError(`transaction.get 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 = node_value_types_js_1.VALUE_TYPES.STRING; + } + else if (typeof val === 'number') { + type = node_value_types_js_1.VALUE_TYPES.NUMBER; + } + else if (typeof val === 'boolean') { + type = node_value_types_js_1.VALUE_TYPES.BOOLEAN; + } + else if (val instanceof Array) { + type = node_value_types_js_1.VALUE_TYPES.ARRAY; + } + else if (typeof val === 'object') { + if ('type' in val) { + const serialized = val; + type = serialized.type; + val = serialized.value; + if (type === node_value_types_js_1.VALUE_TYPES.DATETIME) { + val = new Date(val); + } + else if (type === node_value_types_js_1.VALUE_TYPES.REFERENCE) { + val = new acebase_core_1.PathReference(val); } } + else { + type = node_value_types_js_1.VALUE_TYPES.OBJECT; + } + } + else { + throw new Error(`Unknown value type`); + } + return { type, value: val }; + } + /** + * 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 + */ + async _writeNode(path, value, options) { + if (!options.merge && 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`); + } + // Check if the value for this node changed, to prevent recursive calls to + // perform unnecessary writes that do not change any data + if (typeof options.diff === 'undefined' && typeof options.currentValue !== 'undefined') { + const diff = compareValues(options.currentValue, value); + if (options.merge && typeof diff === 'object') { + diff.removed = diff.removed.filter(key => value[key] === null); // Only keep "removed" items that are really being removed by setting to null + } + options.diff = diff; + } + if (options.diff === 'identical') { + return; // Done! + } + const transaction = options.transaction; + // Get info about current node at path + const currentRow = options.currentValue === null + ? null // No need to load info if currentValue is null (we already know it doesn't exist) + : await this._readNode(path, { transaction }); + if (options.merge && currentRow) { + if (currentRow.type === node_value_types_js_1.VALUE_TYPES.ARRAY && !(value instanceof Array) && typeof value === 'object' && Object.keys(value).some(key => isNaN(parseInt(key)))) { + throw new Error(`Cannot merge existing array of path "${path}" with an object`); + } + if (value instanceof Array && currentRow.type !== node_value_types_js_1.VALUE_TYPES.ARRAY) { + throw new Error(`Cannot merge existing object of path "${path}" with an array`); + } } - else if (sparseArray) { - Object.assign(this, sparseArray); + const revision = options.revision || acebase_core_1.ID.generate(); + const mainNode = { + type: currentRow && currentRow.type === node_value_types_js_1.VALUE_TYPES.ARRAY ? node_value_types_js_1.VALUE_TYPES.ARRAY : node_value_types_js_1.VALUE_TYPES.OBJECT, + value: {}, + }; + const childNodeValues = {}; + if (value instanceof Array) { + mainNode.type = node_value_types_js_1.VALUE_TYPES.ARRAY; + // Convert array to object with numeric properties + const obj = {}; + for (let i = 0; i < value.length; i++) { + obj[i] = value[i]; + } + value = obj; } - } -} -exports.PartialArray = PartialArray; - -},{}],50:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.PathInfo = void 0; -function getPathKeys(path) { - path = path.replace(/\[/g, '/[').replace(/^\/+/, '').replace(/\/+$/, ''); // Replace [ with /[, remove leading slashes, remove trailing slashes - if (path.length === 0) { - return []; - } - const keys = path.split('/'); - return keys.map(key => { - return key.startsWith('[') ? parseInt(key.slice(1, -1)) : key; - }); -} -class PathInfo { - static get(path) { - return new PathInfo(path); - } - static getChildPath(path, childKey) { - // return getChildPath(path, childKey); - return PathInfo.get(path).child(childKey).path; - } - static getPathKeys(path) { - return getPathKeys(path); - } - constructor(path) { - if (typeof path === 'string') { - this.keys = getPathKeys(path); + else if (value instanceof acebase_core_1.PathReference) { + mainNode.type = node_value_types_js_1.VALUE_TYPES.REFERENCE; + mainNode.value = value.path; } - else if (path instanceof Array) { - this.keys = path; + else if (value instanceof ArrayBuffer) { + mainNode.type = node_value_types_js_1.VALUE_TYPES.BINARY; + mainNode.value = acebase_core_1.ascii85.encode(value); } - this.path = this.keys.reduce((path, key, i) => i === 0 ? `${key}` : typeof key === 'string' ? `${path}/${key}` : `${path}[${key}]`, ''); - } - get key() { - return this.keys.length === 0 ? null : this.keys.slice(-1)[0]; - } - get parent() { - if (this.keys.length == 0) { - return null; + else if (typeof value === 'string') { + mainNode.type = node_value_types_js_1.VALUE_TYPES.STRING; + mainNode.value = value; + } + const currentIsObjectOrArray = currentRow ? [node_value_types_js_1.VALUE_TYPES.OBJECT, node_value_types_js_1.VALUE_TYPES.ARRAY].includes(currentRow.type) : false; + const newIsObjectOrArray = [node_value_types_js_1.VALUE_TYPES.OBJECT, node_value_types_js_1.VALUE_TYPES.ARRAY].includes(mainNode.type); + const children = { + current: [], + new: [], + }; + let currentObject = null; + if (currentIsObjectOrArray) { + currentObject = currentRow.value; + children.current = Object.keys(currentObject); + // if (currentObject instanceof Array) { // ALWAYS FALSE BECAUSE THEY ARE STORED AS OBJECTS WITH NUMERIC PROPERTIES + // // Convert array to object with numeric properties + // const obj = {}; + // for (let i = 0; i < value.length; i++) { + // obj[i] = value[i]; + // } + // currentObject = obj; + // } + if (newIsObjectOrArray) { + mainNode.value = currentObject; + } + } + if (newIsObjectOrArray) { + // Object or array. Determine which properties can be stored in the main node, + // and which should be stored in their own nodes + if (!options.merge) { + // Check which keys are present in the old object, but not in newly given object + Object.keys(mainNode.value).forEach(key => { + if (!(key in value)) { + // Property that was in old object, is not in new value -> set to null to mark deletion! + value[key] = null; + } + }); + } + Object.keys(value).forEach(key => { + const val = value[key]; + delete mainNode.value[key]; // key is being overwritten, moved from inline to dedicated, or deleted. TODO: check if this needs to be done SQLite & MSSQL implementations too + if (val === null) { // || typeof val === 'undefined' + // This key is being removed + return; + } + else if (typeof val === 'undefined') { + if (this.settings.removeVoidProperties === true) { + delete value[key]; // Kill the property in the passed object as well, to prevent differences in stored and working values + return; + } + else { + throw new Error(`Property "${key}" has invalid value. Cannot store undefined values. Set removeVoidProperties option to true to automatically remove undefined properties`); + } + } + // Where to store this value? + if (this.valueFitsInline(val)) { + // Store in main node + mainNode.value[key] = val; + } + else { + // Store in child node + childNodeValues[key] = val; + } + }); + } + // Insert or update node + const isArray = mainNode.type === node_value_types_js_1.VALUE_TYPES.ARRAY; + if (currentRow) { + // update + this.logger.info(`Node "/${path}" is being ${options.merge ? 'updated' : 'overwritten'}`.colorize(acebase_core_1.ColorStyle.cyan)); + // If existing is an array or object, we have to find out which children are affected + if (currentIsObjectOrArray || newIsObjectOrArray) { + // Get current child nodes in dedicated child records + const pathInfo = acebase_core_1.PathInfo.get(path); + const keys = []; + let checkExecuted = false; + const includeChildCheck = (childPath) => { + checkExecuted = true; + if (!transaction.production && !pathInfo.isParentOf(childPath)) { + // Double check failed + this.throwImplementationError(`"${childPath}" is not a child of "${path}" - childrenOf must only check and return paths that are children`); + } + return true; + }; + const addChildPath = (childPath) => { + if (!checkExecuted) { + this.throwImplementationError(`childrenOf did not call checkCallback before addCallback`); + } + const key = acebase_core_1.PathInfo.get(childPath).key; + keys.push(key.toString()); // .toString to make sure all keys are compared as strings + return true; // Keep streaming + }; + 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: [], + delete: options && options.merge ? Object.keys(value).filter(key => value[key] === null) : children.current.filter(key => !children.new.includes(key)), + }; + changes.update = children.new.filter(key => children.current.includes(key) && !changes.delete.includes(key)); + if (isArray && options.merge && (changes.insert.length > 0 || changes.delete.length > 0)) { + // deletes or inserts of individual array entries are not allowed, unless it is the last entry: + // - deletes would cause the paths of following items to change, which is unwanted because the actual data does not change, + // eg: removing index 3 on array of size 10 causes entries with index 4 to 9 to 'move' to indexes 3 to 8 + // - inserts might introduce gaps in indexes, + // eg: adding to index 7 on an array of size 3 causes entries with indexes 3 to 6 to go 'missing' + const newArrayKeys = changes.update.concat(changes.insert); + const isExhaustive = newArrayKeys.every((k, index, arr) => arr.includes(index.toString())); + if (!isExhaustive) { + throw new Error(`Elements cannot be inserted beyond, or removed before the end of an array. Rewrite the whole array at path "${path}" or change your schema to use an object collection instead`); + } + } + // (over)write all child nodes that must be stored in their own record + const writePromises = Object.keys(childNodeValues).map(key => { + const keyOrIndex = isArray ? parseInt(key) : key; + const childDiff = typeof options.diff === 'object' ? options.diff.forChild(keyOrIndex) : undefined; + if (childDiff === 'identical') { + // console.warn(`Skipping _writeNode recursion for child "${keyOrIndex}"`); + return; // Skip + } + const childPath = pathInfo.childPath(keyOrIndex); // PathInfo.getChildPath(path, key); + const childValue = childNodeValues[keyOrIndex]; + // Pass current child value to _writeNode + const currentChildValue = typeof options.currentValue === 'undefined' // Fixing issue #20 + ? undefined + : options.currentValue !== null && typeof options.currentValue === 'object' && keyOrIndex in options.currentValue + ? options.currentValue[keyOrIndex] + : null; + return this._writeNode(childPath, childValue, { transaction, revision, merge: false, currentValue: currentChildValue, diff: childDiff }); + }); + // Delete all child nodes that were stored in their own record, but are being removed + // Also delete nodes that are being moved from a dedicated record to inline + const movingNodes = newIsObjectOrArray ? keys.filter(key => key in mainNode.value) : []; // moving from dedicated to inline value + const deleteDedicatedKeys = changes.delete.concat(movingNodes); + const deletePromises = deleteDedicatedKeys.map(key => { + const keyOrIndex = isArray ? parseInt(key) : key; + const childPath = pathInfo.childPath(keyOrIndex); + return this._deleteNode(childPath, { transaction }); + }); + const promises = writePromises.concat(deletePromises); + await Promise.all(promises); + } + // Update main node + // TODO: Check if revision should change? + const p = 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, + }); + if (p instanceof Promise) { + return await p; + } + } + else { + // Current node does not exist, create it and any child nodes + // write all child nodes that must be stored in their own record + this.logger.info(`Node "/${path}" is being created`.colorize(acebase_core_1.ColorStyle.cyan)); + if (isArray) { + // Check if the array is "intact" (all entries have an index from 0 to the end with no gaps) + const arrayKeys = Object.keys(mainNode.value).concat(Object.keys(childNodeValues)); + const isExhaustive = arrayKeys.every((k, index, arr) => arr.includes(index.toString())); + if (!isExhaustive) { + throw new Error(`Cannot store arrays with missing entries`); + } + } + const promises = Object.keys(childNodeValues).map(key => { + const keyOrIndex = isArray ? parseInt(key) : key; + const childPath = acebase_core_1.PathInfo.getChildPath(path, keyOrIndex); + const childValue = childNodeValues[keyOrIndex]; + return this._writeNode(childPath, childValue, { transaction, revision, merge: false, currentValue: null }); + }); + // Create current node + const p = this._storeNode(path, { + type: mainNode.type, + value: mainNode.value, + revision, + revision_nr: 1, + created: Date.now(), + modified: Date.now(), + }, { + transaction, + }); + if (p instanceof Promise) { + promises.push(p); + } + await Promise.all(promises); } - const parentKeys = this.keys.slice(0, -1); - return new PathInfo(parentKeys); } - get parentPath() { - return this.keys.length === 0 ? null : this.parent.path; + /** + * 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! + */ + async _deleteNode(path, options) { + const pathInfo = acebase_core_1.PathInfo.get(path); + this.logger.info(`Node "/${path}" is being deleted`.colorize(acebase_core_1.ColorStyle.cyan)); + const deletePaths = [path]; + let checkExecuted = false; + const includeDescendantCheck = (descPath) => { + checkExecuted = true; + if (!transaction.production && !pathInfo.isAncestorOf(descPath)) { + // Double check failed + this.throwImplementationError(`"${descPath}" is not a descendant of "${path}" - descendantsOf must only check and return paths that are descendants`); + } + return true; + }; + const addDescendant = (descPath) => { + if (!checkExecuted) { + this.throwImplementationError(`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.logger.info(`Nodes ${deletePaths.map(p => `"/${p}"`).join(',')} are being deleted`.colorize(acebase_core_1.ColorStyle.cyan)); + return transaction.removeMultiple(deletePaths); } - child(childKey) { - if (typeof childKey === 'string') { - if (childKey.length === 0) { - throw new Error(`child key for path "${this.path}" cannot be empty`); + /** + * Enumerates all children of a given Node for reflection purposes + */ + getChildren(path, options = {}) { + let callback; + const generator = { + /** + * + * @param valueCallback callback function to run for each child. Return false to stop iterating + * @returns returns a promise that resolves with a boolean indicating if all children have been enumerated, or was canceled by the valueCallback function + */ + next(valueCallback) { + callback = valueCallback; + return start(); + }, + }; + const start = async () => { + const transaction = options.transaction || await this._customImplementation.getTransaction({ path, write: false }); + try { + let canceled = false; + await (async () => { + const node = await this._readNode(path, { transaction }); + if (!node) { + throw new node_errors_js_1.NodeNotFoundError(`Node "/${path}" does not exist`); + } + if (![node_value_types_js_1.VALUE_TYPES.OBJECT, node_value_types_js_1.VALUE_TYPES.ARRAY].includes(node.type)) { + // No children + return; + } + const isArray = node.type === node_value_types_js_1.VALUE_TYPES.ARRAY; + const value = node.value; + let keys = Object.keys(value).map(key => isArray ? parseInt(key) : key); + if (options.keyFilter) { + keys = keys.filter(key => options.keyFilter.includes(key)); + } + const pathInfo = acebase_core_1.PathInfo.get(path); + keys.length > 0 && keys.every(key => { + const 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: new Date(node.created), + modified: new Date(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 (!transaction.production && !pathInfo.isParentOf(childPath)) { + // Double check failed + this.throwImplementationError(`"${childPath}" is not a child of "${path}" - childrenOf must only check and return paths that are children`); + } + if (options.keyFilter) { + const key = acebase_core_1.PathInfo.get(childPath).key; + return options.keyFilter.includes(key); + } + return true; + }; + const addChildNode = (childPath, node) => { + if (!checkExecuted) { + this.throwImplementationError(`childrenOf did not call checkCallback before addCallback`); + } + const key = acebase_core_1.PathInfo.get(childPath).key; + const info = new CustomStorageNodeInfo({ + path: childPath, + type: node.type, + key: isArray ? null : key, + index: isArray ? key : null, + address: new node_address_js_1.NodeAddress(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) { + // transaction was created by us, commit + await transaction.commit(); + } + return canceled; } - // Allow expansion of a child path (eg "user/name") into equivalent `child('user').child('name')` - const keys = getPathKeys(childKey); - keys.forEach(key => { - // Check AceBase key rules here so they will be enforced regardless of storage target. - // This prevents specific keys to be allowed in one environment (eg browser), but then - // refused upon syncing to a binary AceBase db. Fixes https://github.com/appy-one/acebase/issues/172 - if (typeof key !== 'string') { - return; + catch (err) { + if (!options.transaction) { + // transaction was created by us, rollback + await transaction.rollback(err); } - if (/[\x00-\x08\x0b\x0c\x0e-\x1f/[\]\\]/.test(key)) { - throw new Error(`Invalid child key "${key}" for path "${this.path}". Keys cannot contain control characters or any of the following characters: \\ / [ ]`); + throw err; + } + }; // start() + return generator; + } + async getNode(path, options) { + // path = path.replace(/'/g, ''); // prevent sql injection, remove single quotes + options = options || {}; + const transaction = options.transaction || await this._customImplementation.getTransaction({ path, write: false }); + try { + const node = await (async () => { + // Get path, path/* and path[* + const filtered = (options.include && options.include.length > 0) || (options.exclude && options.exclude.length > 0) || options.child_objects === false; + const pathInfo = acebase_core_1.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); + (0, assert_js_1.assert)(lockPath === pathInfo.parentPath, `transaction.moveToParentPath() did not move to the right parent path of "${path}"`); + const parentNode = await this._readNode(pathInfo.parentPath, { transaction }); + if (parentNode && [node_value_types_js_1.VALUE_TYPES.OBJECT, node_value_types_js_1.VALUE_TYPES.ARRAY].includes(parentNode.type) && pathInfo.key in parentNode.value) { + 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 isArray = targetNode.type === node_value_types_js_1.VALUE_TYPES.ARRAY; + /** + * Convert include & exclude filters to PathInfo instances for easier handling + */ + const convertFilterArray = (arr) => { + const isNumber = (key) => /^[0-9]+$/.test(key); + return arr.map(path => acebase_core_1.PathInfo.get(isArray && isNumber(path) ? `[${path}]` : path)); + }; + const includeFilter = options.include ? convertFilterArray(options.include) : []; + const excludeFilter = options.exclude ? convertFilterArray(options.exclude) : []; + /** + * Apply include filters to prevent unwanted properties stored inline to be added. + * + * Removes properties that are not on the trail of any include filter, but were loaded because they are + * stored inline in the parent node. + * + * Example: + * data of `"users/someuser/posts/post1"`: `{ title: 'My first post', posted: (date), history: {} }` + * code: `db.ref('users/someuser').get({ include: ['posts/*\/title'] })` + * descPath: `"users/someuser/posts/post1"`, + * trailKeys: `["posts", "post1"]`, + * includeFilter[0]: `["posts", "*", "title"]` + * properties `posted` and `history` must be removed from the object + */ + const applyFiltersOnInlineData = (descPath, node) => { + if ([node_value_types_js_1.VALUE_TYPES.OBJECT, node_value_types_js_1.VALUE_TYPES.ARRAY].includes(node.type) && includeFilter.length > 0) { + const trailKeys = acebase_core_1.PathInfo.getPathKeys(descPath).slice(pathInfo.keys.length); + const checkPathInfo = new acebase_core_1.PathInfo(trailKeys); + const remove = []; + const includes = includeFilter.filter(info => info.isDescendantOf(checkPathInfo)); + if (includes.length > 0) { + const isArray = node.type === node_value_types_js_1.VALUE_TYPES.ARRAY; + remove.push(...Object.keys(node.value).map(key => isArray ? +key : key)); // Mark all at first + for (const info of includes) { + const targetProp = info.keys[trailKeys.length]; + if (typeof targetProp === 'string' && (targetProp === '*' || targetProp.startsWith('$'))) { + remove.splice(0); + break; + } + const index = remove.indexOf(targetProp); + index >= 0 && remove.splice(index, 1); + } + } + const hasIncludeOnChild = includeFilter.some(info => info.isChildOf(checkPathInfo)); + const hasExcludeOnChild = excludeFilter.some(info => info.isChildOf(checkPathInfo)); + if (hasExcludeOnChild && !hasIncludeOnChild) { + // do not remove children that are NOT in direct exclude filters (which includes them again) + const excludes = excludeFilter.filter(info => info.isChildOf(checkPathInfo)); + for (let i = 0; i < remove.length; i++) { + if (!excludes.find(info => info.equals(remove[i]))) { + remove.splice(i, 1); + i--; + } + } + } + // remove.length > 0 && this.debug.log(`Remove properties:`, remove); + for (const key of remove) { + delete node.value[key]; + } + } + }; + applyFiltersOnInlineData(path, targetNode); + let checkExecuted = false; + const includeDescendantCheck = (descPath, metadata) => { + checkExecuted = true; + if (!transaction.production && !pathInfo.isAncestorOf(descPath)) { + // Double check failed + this.throwImplementationError(`"${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 + const descPathKeys = acebase_core_1.PathInfo.getPathKeys(descPath); + const trailKeys = descPathKeys.slice(pathInfo.keys.length); + const checkPathInfo = new acebase_core_1.PathInfo(trailKeys); + let include = (includeFilter.length > 0 + ? includeFilter.some(info => checkPathInfo.isOnTrailOf(info)) + : true) + && (excludeFilter.length > 0 + ? !excludeFilter.some(info => info.equals(checkPathInfo) || info.isAncestorOf(checkPathInfo)) + : true); + // Apply child_objects filter. If metadata is not loaded, we can only skip deeper descendants here - any child object that does get through will be ignored by addDescendant + if (include + && options.child_objects === false + && (pathInfo.isParentOf(descPath) && [node_value_types_js_1.VALUE_TYPES.OBJECT, node_value_types_js_1.VALUE_TYPES.ARRAY].includes(metadata ? metadata.type : -1) + || acebase_core_1.PathInfo.getPathKeys(descPath).length > pathInfo.pathKeys.length + 1)) { + include = false; + } + return include; + }; + const descRows = []; + const addDescendant = (descPath, node) => { + // console.warn(`Adding descendant "${descPath}"`); + if (!checkExecuted) { + this.throwImplementationError('descendantsOf did not call checkCallback before addCallback'); + } + if (options.child_objects === false && [node_value_types_js_1.VALUE_TYPES.OBJECT, node_value_types_js_1.VALUE_TYPES.ARRAY].includes(node.type)) { + // child objects are filtered out, but this one got through because includeDescendantCheck did not have access to its metadata, + // which is ok because doing that might drastically improve performance in client code. Skip it now. + return true; + } + // Apply include filters to prevent unwanted properties stored inline to be added + applyFiltersOnInlineData(descPath, node); + // Process the value + this._processReadNodeValue(node); + // Add node + const row = node; + row.path = descPath; + descRows.push(row); + return true; // Keep streaming + }; + await transaction.descendantsOf(path, { metadata: true, value: true }, includeDescendantCheck, addDescendant); + this.logger.info(`Read node "/${path}" and ${filtered ? '(filtered) ' : ''}descendants from ${descRows.length + 1} records`.colorize(acebase_core_1.ColorStyle.magenta)); + const result = targetNode; + const objectToArray = (obj) => { + // Convert object value to array + const arr = []; + Object.keys(obj).forEach(key => { + const index = parseInt(key); + arr[index] = obj[index]; + }); + return arr; + }; + if (targetNode.type === node_value_types_js_1.VALUE_TYPES.ARRAY) { + result.value = objectToArray(result.value); } - if (key.length > 128) { - throw new Error(`child key "${key}" for path "${this.path}" is too long. Max key length is 128`); + if (targetNode.type === node_value_types_js_1.VALUE_TYPES.OBJECT || targetNode.type === node_value_types_js_1.VALUE_TYPES.ARRAY) { + // target node is an object or array + // merge with other found (child) nodes + const targetPathKeys = acebase_core_1.PathInfo.getPathKeys(path); + const value = targetNode.value; + for (let i = 0; i < descRows.length; i++) { + const otherNode = descRows[i]; + const pathKeys = acebase_core_1.PathInfo.getPathKeys(otherNode.path); + const trailKeys = pathKeys.slice(targetPathKeys.length); + let parent = value; + for (let j = 0; j < trailKeys.length; j++) { + (0, assert_js_1.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' + ? node_value_types_js_1.VALUE_TYPES.ARRAY + : node_value_types_js_1.VALUE_TYPES.OBJECT; + let nodeValue; + if (!isLast) { + nodeValue = nodeType === node_value_types_js_1.VALUE_TYPES.OBJECT ? {} : []; + } + else { + nodeValue = otherNode.value; + if (nodeType === node_value_types_js_1.VALUE_TYPES.ARRAY) { + nodeValue = objectToArray(nodeValue); + } + } + if (key in parent) { + // Merge with parent + const mergePossible = typeof parent[key] === typeof nodeValue && [node_value_types_js_1.VALUE_TYPES.OBJECT, node_value_types_js_1.VALUE_TYPES.ARRAY].includes(nodeType); + if (!mergePossible) { + // Ignore the value in the child record, see issue #20: "Assertion failed: Merging child values can only be done if existing and current values are both an array or object" + this.logger.error(`The value stored in node "${otherNode.path}" cannot be merged with the parent node, value will be ignored. This error should disappear once the target node value is updated. See issue #20 for more information`, { path, parent, key, nodeType, nodeValue }); + } + else { + Object.keys(nodeValue).forEach(childKey => { + if (childKey in parent[key]) { + this.throwImplementationError(`Custom storage merge error: child key "${childKey}" is in parent value already! Make sure the get/childrenOf/descendantsOf methods of the custom storage class return values that can be modified by AceBase without affecting the stored source`); + } + parent[key][childKey] = nodeValue[childKey]; + }); + } + } + else { + parent[key] = nodeValue; + } + parent = parent[key]; + } + } } - if (key.length === 0) { - throw new Error(`child key for path "${this.path}" cannot be empty`); + else if (descRows.length > 0) { + this.throwImplementationError(`multiple records found for non-object value!`); } - }); - childKey = keys; - } - return new PathInfo(this.keys.concat(childKey)); - } - childPath(childKey) { - return this.child(childKey).path; - } - get pathKeys() { - return this.keys; - } - /** - * If varPath contains variables or wildcards, it will return them with the values found in fullPath - * @param {string} varPath path containing variables such as * and $name - * @param {string} fullPath real path to a node - * @returns {{ [index: number]: string|number, [variable: string]: string|number }} returns an array-like object with all variable values. All named variables are also set on the array by their name (eg vars.uid and vars.$uid) - * @example - * PathInfo.extractVariables('users/$uid/posts/$postid', 'users/ewout/posts/post1/title') === { - * 0: 'ewout', - * 1: 'post1', - * uid: 'ewout', // or $uid - * postid: 'post1' // or $postid - * }; - * - * PathInfo.extractVariables('users/*\/posts/*\/$property', 'users/ewout/posts/post1/title') === { - * 0: 'ewout', - * 1: 'post1', - * 2: 'title', - * property: 'title' // or $property - * }; - * - * PathInfo.extractVariables('users/$user/friends[*]/$friend', 'users/dora/friends[4]/diego') === { - * 0: 'dora', - * 1: 4, - * 2: 'diego', - * user: 'dora', // or $user - * friend: 'diego' // or $friend - * }; - */ - static extractVariables(varPath, fullPath) { - if (!varPath.includes('*') && !varPath.includes('$')) { - return []; - } - // if (!this.equals(fullPath)) { - // throw new Error(`path does not match with the path of this PathInfo instance: info.equals(path) === false!`) - // } - 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; - // Set the $variable property - variables[key] = pathKey; - // Set friendly property name (without $) - const varName = key.slice(1); - if (typeof variables[varName] === 'undefined') { - variables[varName] = pathKey; + // Post process filters to remove any data that got through 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 + (0, assert_js_1.assert)(Object.keys(result.value[key]).length === 0); + delete result.value[key]; + } + }); } - } - }); - return variables; - } - /** - * If varPath contains variables or wildcards, it will return a path with the variables replaced by the keys found in fullPath. - * @example - * PathInfo.fillVariables('users/$uid/posts/$postid', 'users/ewout/posts/post1/title') === 'users/ewout/posts/post1' - */ - static fillVariables(varPath, fullPath) { - if (varPath.indexOf('*') < 0 && varPath.indexOf('$') < 0) { - return varPath; - } - const keys = getPathKeys(varPath); - const pathKeys = getPathKeys(fullPath); - const 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 "${varPath}" because they do not match`); - } - }); - let mergedPath = ''; - merged.forEach(key => { - if (typeof key === 'number') { - mergedPath += `[${key}]`; - } - else { - if (mergedPath.length > 0) { - mergedPath += '/'; + if (options.include) { + // TODO: remove any unselected children that did get through } - mergedPath += key; - } - }); - return mergedPath; - } - /** - * Replaces all variables in a path with the values in the vars argument - * @param varPath path containing variables - * @param vars variables object such as one gotten from PathInfo.extractVariables - */ - static fillVariables2(varPath, vars) { - if (typeof vars !== 'object' || Object.keys(vars).length === 0) { - return varPath; // Nothing to fill - } - const pathKeys = getPathKeys(varPath); - let n = 0; - const targetPath = pathKeys.reduce((path, key) => { - if (typeof key === 'string' && (key === '*' || key.startsWith('$'))) { - return PathInfo.getChildPath(path, vars[n++]); - } - else { - return PathInfo.getChildPath(path, 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 = acebase_core_1.PathInfo.getPathKeys(path); + process(result.value, checkKeys); + }); + } + return result; + })(); + if (!options.transaction) { + // transaction was created by us, commit + await transaction.commit(); } - }, ''); - return targetPath; - } - /** - * Checks if a given path matches this path, eg "posts/*\/title" matches "posts/12344/title" and "users/123/name" matches "users/$uid/name" - */ - equals(otherPath) { - const other = otherPath instanceof PathInfo ? otherPath : new PathInfo(otherPath); - if (this.path === other.path) { - return true; - } // they are identical - if (this.keys.length !== other.keys.length) { - return false; - } - return this.keys.every((key, index) => { - const otherKey = other.keys[index]; - return otherKey === key - || (typeof otherKey === 'string' && (otherKey === '*' || otherKey[0] === '$')) - || (typeof key === 'string' && (key === '*' || key[0] === '$')); - }); - } - /** - * Checks if a given path is an ancestor, eg "posts" is an ancestor of "posts/12344/title" - */ - isAncestorOf(descendantPath) { - const descendant = descendantPath instanceof PathInfo ? descendantPath : new PathInfo(descendantPath); - if (descendant.path === '' || this.path === descendant.path) { - return false; - } - if (this.path === '') { - return true; - } - if (this.keys.length >= descendant.keys.length) { - return false; - } - return this.keys.every((key, index) => { - const otherKey = descendant.keys[index]; - return otherKey === key - || (typeof otherKey === 'string' && (otherKey === '*' || otherKey[0] === '$')) - || (typeof key === 'string' && (key === '*' || key[0] === '$')); - }); - } - /** - * Checks if a given path is a descendant, eg "posts/1234/title" is a descendant of "posts" - */ - isDescendantOf(ancestorPath) { - const ancestor = ancestorPath instanceof PathInfo ? ancestorPath : new PathInfo(ancestorPath); - if (this.path === '' || this.path === ancestor.path) { - return false; - } - if (ancestorPath === '') { - return true; - } - if (ancestor.keys.length >= this.keys.length) { - return false; - } - return ancestor.keys.every((key, index) => { - const otherKey = this.keys[index]; - return otherKey === key - || (typeof otherKey === 'string' && (otherKey === '*' || otherKey[0] === '$')) - || (typeof key === 'string' && (key === '*' || key[0] === '$')); - }); - } - /** - * Checks if the other path is on the same trail as this path. Paths on the same trail if they share a - * common ancestor. Eg: "posts" is on the trail of "posts/1234/title" and vice versa. - */ - isOnTrailOf(otherPath) { - const other = otherPath instanceof PathInfo ? otherPath : new PathInfo(otherPath); - if (this.path.length === 0 || other.path.length === 0) { - return true; - } - if (this.path === other.path) { - return true; + return node; } - return this.pathKeys.every((key, index) => { - if (index >= other.keys.length) { - return true; + catch (err) { + if (!options.transaction) { + // transaction was created by us, rollback + await transaction.rollback(err); } - const otherKey = other.keys[index]; - return otherKey === key - || (typeof otherKey === 'string' && (otherKey === '*' || otherKey[0] === '$')) - || (typeof key === 'string' && (key === '*' || key[0] === '$')); - }); - } - /** - * Checks if a given path is a direct child, eg "posts/1234/title" is a child of "posts/1234" - */ - isChildOf(otherPath) { - const other = otherPath instanceof PathInfo ? otherPath : new PathInfo(otherPath); - if (this.path === '') { - return false; - } // If our path is the root, it's nobody's child... - return this.parent.equals(other); - } - /** - * Checks if a given path is its parent, eg "posts/1234" is the parent of "posts/1234/title" - */ - isParentOf(otherPath) { - const other = otherPath instanceof PathInfo ? otherPath : new PathInfo(otherPath); - if (other.path === '') { - return false; - } // If the other path is the root, this path cannot be its parent - return this.equals(other.parent); - } -} -exports.PathInfo = PathInfo; - -},{}],51:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.PathReference = void 0; -class PathReference { - /** - * Creates a reference to a path that can be stored in the database. Use this to create cross-references to other data in your database - * @param path - */ - constructor(path) { - this.path = path; - } -} -exports.PathReference = PathReference; - -},{}],52:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.default = { - // eslint-disable-next-line @typescript-eslint/ban-types - nextTick(fn) { - setTimeout(fn, 0); - }, -}; - -},{}],53:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.SchemaDefinition = void 0; -// parses a typestring, creates checker functions -function parse(definition) { - // tokenize - let pos = 0; - function consumeSpaces() { - let c; - while (c = definition[pos], [' ', '\r', '\n', '\t'].includes(c)) { - pos++; - } - } - function consumeCharacter(c) { - if (definition[pos] !== c) { - throw new Error(`Unexpected character at position ${pos}. Expected: '${c}', found '${definition[pos]}'`); - } - pos++; - } - function readProperty() { - consumeSpaces(); - const prop = { name: '', optional: false, wildcard: false }; - let c; - while (c = definition[pos], c === '_' || c === '$' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (prop.name.length > 0 && c >= '0' && c <= '9') || (prop.name.length === 0 && c === '*')) { - prop.name += c; - pos++; - } - if (prop.name.length === 0) { - throw new Error(`Property name expected at position ${pos}, found: ${definition.slice(pos, pos + 10)}..`); - } - if (definition[pos] === '?') { - prop.optional = true; - pos++; - } - if (prop.name === '*' || prop.name[0] === '$') { - prop.optional = true; - prop.wildcard = true; + throw err; } - consumeSpaces(); - consumeCharacter(':'); - return prop; } - function readType() { - consumeSpaces(); - let type = { typeOf: 'any' }, c; - // try reading simple type first: (string,number,boolean,Date etc) - let name = ''; - while (c = definition[pos], (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) { - name += c; - pos++; - } - if (name.length === 0) { - if (definition[pos] === '*') { - // any value - consumeCharacter('*'); - type.typeOf = 'any'; - } - else if (['\'', '"', '`'].includes(definition[pos])) { - // Read string value - type.typeOf = 'string'; - type.value = ''; - const quote = definition[pos]; - consumeCharacter(quote); - while (c = definition[pos], c && c !== quote) { - type.value += c; - pos++; - } - consumeCharacter(quote); - } - else if (definition[pos] >= '0' && definition[pos] <= '9') { - // read numeric value - type.typeOf = 'number'; - let nr = ''; - while (c = definition[pos], c === '.' || c === 'n' || (c >= '0' && c <= '9')) { - nr += c; - pos++; - } - if (nr.endsWith('n')) { - type.value = BigInt(nr); + async getNodeInfo(path, options = {}) { + options = options || {}; + const pathInfo = acebase_core_1.PathInfo.get(path); + const transaction = options.transaction || await this._customImplementation.getTransaction({ path, write: false }); + try { + const node = await this._readNode(path, { transaction }); + const info = new CustomStorageNodeInfo({ + 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 node_address_js_1.NodeAddress(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 !== '') { + // Try parent node + const lockPath = await transaction.moveToParentPath(pathInfo.parentPath); + (0, assert_js_1.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 && [node_value_types_js_1.VALUE_TYPES.OBJECT, node_value_types_js_1.VALUE_TYPES.ARRAY].includes(parent.type) && pathInfo.key in parent.value) { + // Stored in parent node + 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 if (nr.includes('.')) { - type.value = parseFloat(nr); + else { + // Parent doesn't exist, so the node we're looking for cannot exist either + info.address = null; } - else { - type.value = parseInt(nr); + } + if (options.include_child_count) { + info.childCount = 0; + if ([node_value_types_js_1.VALUE_TYPES.OBJECT, node_value_types_js_1.VALUE_TYPES.ARRAY].includes(info.valueType) && info.address) { + // Get number of children + info.childCount = node.value ? Object.keys(node.value).length : 0; + info.childCount += await transaction.getChildCount(path); } } - else if (definition[pos] === '{') { - // Read object (interface) definition - consumeCharacter('{'); - type.typeOf = 'object'; - type.instanceOf = Object; - // Read children: - type.children = []; - while (true) { - const prop = readProperty(); - const types = readTypes(); - type.children.push({ name: prop.name, optional: prop.optional, wildcard: prop.wildcard, types }); - consumeSpaces(); - if (definition[pos] === ';' || definition[pos] === ',') { - consumeCharacter(definition[pos]); - consumeSpaces(); - } - if (definition[pos] === '}') { - break; - } + if (!options.transaction) { + // transaction was created by us, commit + await transaction.commit(); + } + return info; + } + catch (err) { + if (!options.transaction) { + // transaction was created by us, rollback + await transaction.rollback(err); + } + throw err; + } + } + // TODO: Move to Storage base class? + async setNode(path, value, options = { suppress_events: false, context: null }) { + if (this.settings.readOnly) { + throw new Error(`Database is opened in read-only mode`); + } + const pathInfo = acebase_core_1.PathInfo.get(path); + const transaction = options.transaction || await this._customImplementation.getTransaction({ 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}`); } - consumeCharacter('}'); + await this._writeNodeWithTracking('', value, { merge: false, transaction, suppress_events: options.suppress_events, context: options.context }); } - else if (definition[pos] === '/') { - // Read regular expression definition - consumeCharacter('/'); - let pattern = '', flags = ''; - while (c = definition[pos], c !== '/' || pattern.endsWith('\\')) { - pattern += c; - pos++; + else if (typeof options.assert_revision !== 'undefined') { + const info = await this.getNodeInfo(path, { transaction }); + if (info.revision !== options.assert_revision) { + throw new node_errors_js_1.NodeRevisionError(`revision '${info.revision}' does not match requested revision '${options.assert_revision}'`); } - consumeCharacter('/'); - while (c = definition[pos], ['g', 'i', 'm', 's', 'u', 'y', 'd'].includes(c)) { - flags += c; - pos++; + if (info.address && info.address.path === path && value !== null && !this.valueFitsInline(value)) { + // Overwrite node + await this._writeNodeWithTracking(path, value, { merge: false, transaction, suppress_events: options.suppress_events, context: options.context }); + } + else { + // Update parent node + const lockPath = await transaction.moveToParentPath(pathInfo.parentPath); + (0, assert_js_1.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, suppress_events: options.suppress_events, context: options.context }); } - type.typeOf = 'string'; - type.matches = new RegExp(pattern, flags); } else { - throw new Error(`Expected a type definition at position ${pos}, found character '${definition[pos]}'`); + // Delegate operation to update on parent node + const lockPath = await transaction.moveToParentPath(pathInfo.parentPath); + (0, assert_js_1.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, suppress_events: options.suppress_events, context: options.context }); + } + if (!options.transaction) { + // transaction was created by us, commit + await transaction.commit(); } } - else if (['string', 'number', 'boolean', 'bigint', 'undefined', 'String', 'Number', 'Boolean', 'BigInt'].includes(name)) { - type.typeOf = name.toLowerCase(); + catch (err) { + if (!options.transaction) { + // transaction was created by us, rollback + await transaction.rollback(err); + } + throw err; } - else if (name === 'Object' || name === 'object') { - type.typeOf = 'object'; - type.instanceOf = Object; + } + // TODO: Move to Storage base class? + async updateNode(path, updates, options = { suppress_events: false, context: null }) { + if (this.settings.readOnly) { + throw new Error(`Database is opened in read-only mode`); } - else if (name === 'Date') { - type.typeOf = 'object'; - type.instanceOf = Date; + if (typeof updates !== 'object') { + throw new Error(`invalid updates argument`); //. Must be a non-empty object or array } - else if (name === 'Binary' || name === 'binary') { - type.typeOf = 'object'; - type.instanceOf = ArrayBuffer; + else if (Object.keys(updates).length === 0) { + return; // Nothing to update. Done! } - else if (name === 'any') { - type.typeOf = 'any'; + const transaction = options.transaction || await this._customImplementation.getTransaction({ path, write: true }); + try { + // Get info about current node + const nodeInfo = await this.getNodeInfo(path, { transaction }); + const pathInfo = acebase_core_1.PathInfo.get(path); + if (nodeInfo.exists && nodeInfo.address && nodeInfo.address.path === path) { + // Node exists and is stored in its own record. + // Update it + await this._writeNodeWithTracking(path, updates, { transaction, merge: true, suppress_events: options.suppress_events, context: options.context }); + } + else if (nodeInfo.exists) { + // Node exists, but is stored in its parent node. + const pathInfo = acebase_core_1.PathInfo.get(path); + const lockPath = await transaction.moveToParentPath(pathInfo.parentPath); + (0, assert_js_1.assert)(lockPath === pathInfo.parentPath, `transaction.moveToParentPath() did not move to the right parent path of "${path}"`); + await this._writeNodeWithTracking(pathInfo.parentPath, { [pathInfo.key]: updates }, { transaction, merge: true, suppress_events: options.suppress_events, context: options.context }); + } + else { + // The node does not exist, it's parent doesn't have it either. Update the parent instead + const lockPath = await transaction.moveToParentPath(pathInfo.parentPath); + (0, assert_js_1.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, suppress_events: options.suppress_events, context: options.context }); + } + if (!options.transaction) { + // transaction was created by us, commit + await transaction.commit(); + } } - else if (name === 'null') { - // This is ignored, null values are not stored in the db (null indicates deletion) - type.typeOf = 'object'; - type.value = null; + catch (err) { + if (!options.transaction) { + // transaction was created by us, rollback + await transaction.rollback(err); + } + throw err; } - else if (name === 'Array') { - // Read generic Array defintion - consumeCharacter('<'); - type.typeOf = 'object'; - type.instanceOf = Array; //name; - type.genericTypes = readTypes(); - consumeCharacter('>'); + } +} +exports.CustomStorage = CustomStorage; + +},{"../../assert.js":31,"../../node-address.js":37,"../../node-errors.js":38,"../../node-info.js":39,"../../node-lock.js":40,"../../node-value-types.js":41,"../index.js":56,"./helpers.js":47,"acebase-core":12}],49:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createIndexedDBInstance = void 0; +const acebase_core_1 = require("acebase-core"); +const index_js_1 = require("../index.js"); +const index_js_2 = require("../../../index.js"); +const settings_js_1 = require("./settings.js"); +const transaction_js_1 = require("./transaction.js"); +function createIndexedDBInstance(dbname, init = {}) { + const settings = new settings_js_1.IndexedDBStorageSettings(init); + // We'll create an IndexedDB with name "dbname.acebase" + const request = indexedDB.open(`${dbname}.acebase`, 1); + request.onupgradeneeded = (e) => { + // create datastore + const db = request.result; + // Create "nodes" object store for metadata + db.createObjectStore('nodes', { keyPath: 'path' }); + // Create "content" object store with all data + db.createObjectStore('content'); + }; + let idb; + const readyPromise = new Promise((resolve, reject) => { + request.onsuccess = e => { + idb = request.result; + resolve(); + }; + request.onerror = e => { + reject(e); + }; + }); + const cache = new acebase_core_1.SimpleCache(typeof settings.cacheSeconds === 'number' ? settings.cacheSeconds : 60); // 60 second node cache by default + // cache.enabled = false; + const storageSettings = new index_js_1.CustomStorageSettings({ + name: 'IndexedDB', + locking: true, + removeVoidProperties: settings.removeVoidProperties, + maxInlineValueSize: settings.maxInlineValueSize, + lockTimeout: settings.lockTimeout, + ready() { + return readyPromise; + }, + async getTransaction(target) { + await readyPromise; + const context = { + debug: false, + db: idb, + cache, + ipc, + }; + return new transaction_js_1.IndexedDBStorageTransaction(context, target); + }, + }); + const db = new index_js_2.AceBase(dbname, { + logLevel: settings.logLevel, + storage: storageSettings, + sponsor: settings.sponsor, + // isolated: settings.isolated, + }); + const ipc = db.api.storage.ipc; + db.settings.ipcEvents = settings.multipleTabs === true; + ipc.on('notification', async (notification) => { + const message = notification.data; + if (typeof message !== 'object') { + return; } - else if (['true', 'false'].includes(name)) { - type.typeOf = 'boolean'; - type.value = name === 'true'; + if (message.action === 'cache.invalidate') { + // console.warn(`Invalidating cache for paths`, message.paths); + for (const path of message.paths) { + cache.remove(path); + } + } + }); + return db; +} +exports.createIndexedDBInstance = createIndexedDBInstance; + +},{"../../../index.js":33,"../index.js":48,"./settings.js":50,"./transaction.js":51,"acebase-core":12}],50:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.IndexedDBStorageSettings = void 0; +const index_js_1 = require("../../index.js"); +class IndexedDBStorageSettings extends index_js_1.StorageSettings { + constructor(settings) { + super(settings); + /** + * Whether to enable cross-tab synchronization + * @default false + */ + this.multipleTabs = false; + /** + * How many seconds to keep node info in memory, to speed up IndexedDB performance. + * @default 60 + */ + this.cacheSeconds = 60; + /** + * You can turn this on if you are a sponsor + * @default false + */ + this.sponsor = false; + if (typeof settings.logLevel === 'string') { + this.logLevel = settings.logLevel; + } + if (typeof settings.multipleTabs === 'boolean') { + this.multipleTabs = settings.multipleTabs; } - else { - throw new Error(`Unknown type at position ${pos}: "${type}"`); + if (typeof settings.cacheSeconds === 'number') { + this.cacheSeconds = settings.cacheSeconds; } - // Check if it's an Array of given type (eg: string[] or string[][]) - // Also converts to generics, string[] becomes Array, string[][] becomes Array> - consumeSpaces(); - while (definition[pos] === '[') { - consumeCharacter('['); - consumeCharacter(']'); - type = { typeOf: 'object', instanceOf: Array, genericTypes: [type] }; + if (typeof settings.sponsor === 'boolean') { + this.sponsor = settings.sponsor; } - return type; + ['type', 'ipc', 'path'].forEach((prop) => { + if (prop in settings) { + console.warn(`${prop} setting is not supported for AceBase IndexedDBStorage`); + } + }); } - function readTypes() { - consumeSpaces(); - const types = [readType()]; - while (definition[pos] === '|') { - consumeCharacter('|'); - types.push(readType()); - consumeSpaces(); +} +exports.IndexedDBStorageSettings = IndexedDBStorageSettings; + +},{"../../index.js":56}],51:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.IndexedDBStorageTransaction = void 0; +const index_js_1 = require("../index.js"); +function _requestToPromise(request) { + return new Promise((resolve, reject) => { + request.onsuccess = event => { + return resolve(request.result || null); + }; + request.onerror = reject; + }); +} +class IndexedDBStorageTransaction extends index_js_1.CustomStorageTransaction { + /** + * Creates a transaction object for IndexedDB usage. Because IndexedDB automatically commits + * transactions when they have not been touched for a number of microtasks (eg promises + * resolving whithout querying data), we will enqueue set and remove operations until commit + * or rollback. We'll create separate IndexedDB transactions for get operations, caching their + * values to speed up successive requests for the same data. + */ + constructor(context, target) { + super(target); + this.context = context; + this.production = true; // Improves performance, only set when all works well + this._pending = []; + } + _createTransaction(write = false) { + const tx = this.context.db.transaction(['nodes', 'content'], write ? 'readwrite' : 'readonly'); + return tx; + } + _splitMetadata(node) { + const value = node.value; + const copy = Object.assign({}, node); + delete copy.value; + const metadata = copy; + return { metadata, value }; + } + async commit() { + // console.log(`*** commit ${this._pending.length} operations ****`); + if (this._pending.length === 0) { + return; + } + const batch = this._pending.splice(0); + this.context.ipc.sendNotification({ action: 'cache.invalidate', paths: batch.map(op => op.path) }); + const tx = this._createTransaction(true); + try { + await new Promise((resolve, reject) => { + let stop = false, processed = 0; + const handleError = (err) => { + stop = true; + reject(err); + }; + const handleSuccess = () => { + if (++processed === batch.length) { + resolve(); + } + }; + batch.forEach((op, i) => { + if (stop) { + return; + } + let r1, r2; + const path = op.path; + if (op.action === 'set') { + const { metadata, value } = this._splitMetadata(op.node); + const nodeInfo = { path, metadata }; + r1 = tx.objectStore('nodes').put(nodeInfo); // Insert into "nodes" object store + r2 = tx.objectStore('content').put(value, path); // Add value to "content" object store + this.context.cache.set(path, op.node); + } + else if (op.action === 'remove') { + r1 = tx.objectStore('content').delete(path); // Remove from "content" object store + r2 = tx.objectStore('nodes').delete(path); // Remove from "nodes" data store + this.context.cache.set(path, null); + } + else { + handleError(new Error(`Unknown pending operation "${op.action}" on path "${path}" `)); + } + let succeeded = 0; + r1.onsuccess = r2.onsuccess = () => { + if (++succeeded === 2) { + handleSuccess(); + } + }; + r1.onerror = r2.onerror = handleError; + }); + }); + tx.commit && tx.commit(); + } + catch (err) { + console.error(err); + tx.abort && tx.abort(); + throw err; } - return types; } - return readType(); -} -function checkObject(path, properties, obj, partial) { - // Are there any properties that should not be in there? - const invalidProperties = properties.find(prop => prop.name === '*' || prop.name[0] === '$') // Only if no wildcard properties are allowed - ? [] - : Object.keys(obj).filter(key => ![null, undefined].includes(obj[key]) // Ignore null or undefined values - && !properties.find(prop => prop.name === key)); - if (invalidProperties.length > 0) { - return { ok: false, reason: `Object at path "${path}" cannot have propert${invalidProperties.length === 1 ? 'y' : 'ies'} ${invalidProperties.map(p => `"${p}"`).join(', ')}` }; + async rollback(err) { + // Nothing has committed yet, so we'll leave it like that + this._pending = []; } - // Loop through properties that should be present - function checkProperty(property) { - const hasValue = ![null, undefined].includes(obj[property.name]); - if (!property.optional && (partial ? obj[property.name] === null : !hasValue)) { - return { ok: false, reason: `Property at path "${path}/${property.name}" is not optional` }; + async get(path) { + // console.log(`*** get "${path}" ****`); + if (this.context.cache.has(path)) { + const cache = this.context.cache.get(path); + // console.log(`Using cached node for path "${path}": `, cache); + return cache; } - if (hasValue && property.types.length === 1) { - return checkType(`${path}/${property.name}`, property.types[0], obj[property.name], false); + const tx = this._createTransaction(false); + const r1 = _requestToPromise(tx.objectStore('nodes').get(path)); // Get metadata from "nodes" object store + const r2 = _requestToPromise(tx.objectStore('content').get(path)); // Get content from "content" object store + try { + const results = await Promise.all([r1, r2]); + tx.commit && tx.commit(); + const info = results[0]; + if (!info) { + // Node doesn't exist + this.context.cache.set(path, null); + return null; + } + const node = info.metadata; + node.value = results[1]; + this.context.cache.set(path, node); + return node; } - if (hasValue && !property.types.some(type => checkType(`${path}/${property.name}`, type, obj[property.name], false).ok)) { - return { ok: false, reason: `Property at path "${path}/${property.name}" does not match any of ${property.types.length} allowed types` }; + catch (err) { + console.error(`IndexedDB get error`, err); + tx.abort && tx.abort(); + throw err; } - return { ok: true }; } - const namedProperties = properties.filter(prop => !prop.wildcard); - const failedProperty = namedProperties.find(prop => !checkProperty(prop).ok); - if (failedProperty) { - const reason = checkProperty(failedProperty).reason; - return { ok: false, reason }; + set(path, node) { + // Queue the operation until commit + this._pending.push({ action: 'set', path, node }); } - const wildcardProperty = properties.find(prop => prop.wildcard); - if (!wildcardProperty) { - return { ok: true }; + remove(path) { + // Queue the operation until commit + this._pending.push({ action: 'remove', path }); } - const wildcardChildKeys = Object.keys(obj).filter(key => !namedProperties.find(prop => prop.name === key)); - let result = { ok: true }; - for (let i = 0; i < wildcardChildKeys.length && result.ok; i++) { - const childKey = wildcardChildKeys[i]; - result = checkProperty({ name: childKey, types: wildcardProperty.types, optional: true, wildcard: true }); + async removeMultiple(paths) { + // Queues multiple items at once, dramatically improves performance for large datasets + paths.forEach(path => { + this._pending.push({ action: 'remove', path }); + }); + } + childrenOf(path, include, checkCallback, addCallback) { + // console.log(`*** childrenOf "${path}" ****`); + return this._getChildrenOf(path, Object.assign(Object.assign({}, include), { descendants: false }), checkCallback, addCallback); + } + descendantsOf(path, include, checkCallback, addCallback) { + // console.log(`*** descendantsOf "${path}" ****`); + return this._getChildrenOf(path, Object.assign(Object.assign({}, include), { descendants: true }), checkCallback, addCallback); + } + _getChildrenOf(path, include, checkCallback, addCallback) { + // Use cursor to loop from path on + return new Promise((resolve, reject) => { + const pathInfo = index_js_1.CustomStorageHelpers.PathInfo.get(path); + const tx = this._createTransaction(false); + const store = tx.objectStore('nodes'); + const query = IDBKeyRange.lowerBound(path, true); + const cursor = include.metadata ? store.openCursor(query) : store.openKeyCursor(query); + cursor.onerror = e => { + var _a; + (_a = tx.abort) === null || _a === void 0 ? void 0 : _a.call(tx); + reject(e); + }; + cursor.onsuccess = async (e) => { + var _a, _b, _c; + const otherPath = (_b = (_a = cursor.result) === null || _a === void 0 ? void 0 : _a.key) !== null && _b !== void 0 ? _b : null; + let keepGoing = true; + if (otherPath === null) { + // No more results + keepGoing = false; + } + else if (!pathInfo.isAncestorOf(otherPath)) { + // Paths are sorted, no more children or ancestors to be expected! + keepGoing = false; + } + else if (include.descendants || pathInfo.isParentOf(otherPath)) { + let node; + if (include.metadata) { + const valueCursor = cursor; + const data = valueCursor.result.value; + node = data.metadata; + } + const shouldAdd = checkCallback(otherPath, node); + if (shouldAdd) { + if (include.value) { + // Load value! + if (this.context.cache.has(otherPath)) { + const cache = this.context.cache.get(otherPath); + node.value = cache.value; + } + else { + const req = tx.objectStore('content').get(otherPath); + node.value = await new Promise((resolve, reject) => { + req.onerror = e => { + resolve(null); // Value missing? + }; + req.onsuccess = e => { + resolve(req.result); + }; + }); + this.context.cache.set(otherPath, node.value === null ? null : node); + } + } + keepGoing = addCallback(otherPath, node); + } + } + if (keepGoing) { + try { + cursor.result.continue(); + } + catch (err) { + // We reached the end of the cursor? + keepGoing = false; + } + } + if (!keepGoing) { + (_c = tx.commit) === null || _c === void 0 ? void 0 : _c.call(tx); + resolve(); + } + }; + }); } - return result; } -function checkType(path, type, value, partial, trailKeys) { - const ok = { ok: true }; - if (type.typeOf === 'any') { - return ok; - } - if (trailKeys instanceof Array && trailKeys.length > 0) { - // The value to check resides in a descendant path of given type definition. - // Recursivly check child type definitions to find a match - if (type.typeOf !== 'object') { - return { ok: false, reason: `path "${path}" must be typeof ${type.typeOf}` }; // given value resides in a child path, but parent is not allowed be an object. +exports.IndexedDBStorageTransaction = IndexedDBStorageTransaction; + +},{"../index.js":48}],52:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createLocalStorageInstance = exports.LocalStorageTransaction = exports.LocalStorageSettings = void 0; +const index_js_1 = require("../index.js"); +const index_js_2 = require("../../../index.js"); +const settings_js_1 = require("./settings.js"); +Object.defineProperty(exports, "LocalStorageSettings", { enumerable: true, get: function () { return settings_js_1.LocalStorageSettings; } }); +const transaction_js_1 = require("./transaction.js"); +Object.defineProperty(exports, "LocalStorageTransaction", { enumerable: true, get: function () { return transaction_js_1.LocalStorageTransaction; } }); +function createLocalStorageInstance(dbname, init = {}) { + const settings = new settings_js_1.LocalStorageSettings(init); + // Determine whether to use localStorage or sessionStorage + const ls = settings.provider ? settings.provider : settings.temp ? localStorage : sessionStorage; + // Setup our CustomStorageSettings + const storageSettings = new index_js_1.CustomStorageSettings({ + name: 'LocalStorage', + locking: true, + removeVoidProperties: settings.removeVoidProperties, + maxInlineValueSize: settings.maxInlineValueSize, + async ready() { + // LocalStorage is always ready + }, + async getTransaction(target) { + // Create an instance of our transaction class + const context = { + debug: true, + dbname, + localStorage: ls, + }; + const transaction = new transaction_js_1.LocalStorageTransaction(context, target); + return transaction; + }, + }); + const db = new index_js_2.AceBase(dbname, { logLevel: settings.logLevel, storage: storageSettings, sponsor: settings.sponsor }); + db.settings.ipcEvents = settings.multipleTabs === true; + return db; +} +exports.createLocalStorageInstance = createLocalStorageInstance; + +},{"../../../index.js":33,"../index.js":48,"./settings.js":53,"./transaction.js":54}],53:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.LocalStorageSettings = void 0; +const index_js_1 = require("../../index.js"); +class LocalStorageSettings extends index_js_1.StorageSettings { + constructor(settings) { + super(settings); + /** + * whether to use sessionStorage instead of localStorage + * @default false + */ + this.temp = false; + /** + * Whether to enable cross-tab synchronization + * @default false + */ + this.multipleTabs = false; + if (typeof settings.temp === 'boolean') { + this.temp = settings.temp; } - if (!type.children) { - return ok; + if (typeof settings.provider === 'object') { + this.provider = settings.provider; } - const childKey = trailKeys[0]; - let property = type.children.find(prop => prop.name === childKey); - if (!property) { - property = type.children.find(prop => prop.name === '*' || prop.name[0] === '$'); + if (typeof settings.multipleTabs === 'boolean') { + this.multipleTabs = settings.multipleTabs; } - if (!property) { - return { ok: false, reason: `Object at path "${path}" cannot have property "${childKey}"` }; + if (typeof settings.logLevel === 'string') { + this.logLevel = settings.logLevel; } - if (property.optional && value === null && trailKeys.length === 1) { - return ok; + if (typeof settings.sponsor === 'boolean') { + this.sponsor = settings.sponsor; } - let result; - property.types.some(type => { - const childPath = typeof childKey === 'number' ? `${path}[${childKey}]` : `${path}/${childKey}`; - result = checkType(childPath, type, value, partial, trailKeys.slice(1)); - return result.ok; + ['type', 'ipc', 'path'].forEach((prop) => { + if (prop in settings) { + console.warn(`${prop} setting is not supported for AceBase LocalStorage`); + } }); - return result; } - if (value === null) { - return ok; +} +exports.LocalStorageSettings = LocalStorageSettings; + +},{"../../index.js":56}],54:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.LocalStorageTransaction = void 0; +const index_js_1 = require("../index.js"); +// Setup CustomStorageTransaction for browser's LocalStorage +class LocalStorageTransaction extends index_js_1.CustomStorageTransaction { + constructor(context, target) { + super(target); + this.context = context; + this._storageKeysPrefix = `${this.context.dbname}.acebase::`; } - if (type.instanceOf === Object && (typeof value !== 'object' || value instanceof Array || value instanceof Date)) { - return { ok: false, reason: `path "${path}" must be an object collection` }; + async commit() { + // All changes have already been committed. TODO: use same approach as IndexedDB } - if (type.instanceOf && (typeof value !== 'object' || value.constructor !== type.instanceOf)) { // !(value instanceof type.instanceOf) // value.constructor.name !== type.instanceOf - return { ok: false, reason: `path "${path}" must be an instance of ${type.instanceOf.name}` }; + async rollback(err) { + // Not able to rollback changes, because we did not keep track } - if ('value' in type && value !== type.value) { - return { ok: false, reason: `path "${path}" must be value: ${type.value}` }; + async get(path) { + // Gets value from localStorage, wrapped in Promise + const json = this.context.localStorage.getItem(this.getStorageKeyForPath(path)); + const val = JSON.parse(json); + return val; } - if (typeof value !== type.typeOf) { - return { ok: false, reason: `path "${path}" must be typeof ${type.typeOf}` }; + async set(path, val) { + // Sets value in localStorage, wrapped in Promise + const json = JSON.stringify(val); + this.context.localStorage.setItem(this.getStorageKeyForPath(path), json); } - if (type.instanceOf === Array && type.genericTypes && !value.every(v => type.genericTypes.some(t => checkType(path, t, v, false).ok))) { - return { ok: false, reason: `every array value of path "${path}" must match one of the specified types` }; + async remove(path) { + // Removes a value from localStorage, wrapped in Promise + this.context.localStorage.removeItem(this.getStorageKeyForPath(path)); } - if (type.typeOf === 'object' && type.children) { - return checkObject(path, type.children, value, partial); + async childrenOf(path, include, checkCallback, addCallback) { + // Streams all child paths + // Cannot query localStorage, so loop through all stored keys to find children + const pathInfo = index_js_1.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; + } + const 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; + } + } + } } - if (type.matches && !type.matches.test(value)) { - return { ok: false, reason: `path "${path}" must match regular expression /${type.matches.source}/${type.matches.flags}` }; + async descendantsOf(path, include, checkCallback, addCallback) { + // Streams all descendant paths + // Cannot query localStorage, so loop through all stored keys to find descendants + const pathInfo = index_js_1.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; + } + const 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; + } + } + } } - return ok; -} -// eslint-disable-next-line @typescript-eslint/ban-types -function getConstructorType(val) { - switch (val) { - case String: return 'string'; - case Number: return 'number'; - case Boolean: return 'boolean'; - case Date: return 'Date'; - case BigInt: return 'bigint'; - case Array: throw new Error('Schema error: Array cannot be used without a type. Use string[] or Array instead'); - default: throw new Error(`Schema error: unknown type used: ${val.name}`); + /** + * Helper function to get the path from a localStorage key + */ + getPathFromStorageKey(key) { + return key.slice(this._storageKeysPrefix.length); } -} -class SchemaDefinition { - constructor(definition, handling = { warnOnly: false }) { - this.handling = handling; - this.source = definition; - if (typeof definition === 'object') { - // Turn object into typescript definitions - // eg: - // const example = { - // name: String, - // born: Date, - // instrument: "'guitar'|'piano'", - // "address?": { - // street: String - // } - // }; - // Resulting ts: "{name:string,born:Date,instrument:'guitar'|'piano',address?:{street:string}}" - const toTS = (obj) => { - return '{' + Object.keys(obj) - .map(key => { - let val = obj[key]; - if (val === undefined) { - val = 'undefined'; - } - else if (val instanceof RegExp) { - val = `/${val.source}/${val.flags}`; - } - else if (typeof val === 'object') { - val = toTS(val); - } - else if (typeof val === 'function') { - val = getConstructorType(val); - } - else if (!['string', 'number', 'boolean', 'bigint'].includes(typeof val)) { - throw new Error(`Type definition for key "${key}" must be a string, number, boolean, bigint, object, regular expression, or one of these classes: String, Number, Boolean, Date, BigInt`); - } - return `${key}:${val}`; - }) - .join(',') + '}'; - }; - this.text = toTS(definition); - } - else if (typeof definition === 'string') { - this.text = definition; - } - else { - throw new Error('Type definiton must be a string or an object'); - } - this.type = parse(this.text); + /** + * Helper function to get the localStorage key for a path + */ + getStorageKeyForPath(path) { + return `${this._storageKeysPrefix}${path}`; } - check(path, value, partial, trailKeys) { - const result = checkType(path, this.type, value, partial, trailKeys); - if (!result.ok && this.handling.warnOnly) { - // Only issue a warning, allows schema definitions to be added to a production db to monitor if they are accurate before enforcing them. - result.warning = `${partial ? 'Partial schema' : 'Schema'} check on path "${path}"${trailKeys ? ` for child "${trailKeys.join('/')}"` : ''} failed: ${result.reason}`; - result.ok = true; - this.handling.warnCallback(result.warning); - } - return result; +} +exports.LocalStorageTransaction = LocalStorageTransaction; + +},{"../index.js":48}],55:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.SchemaValidationError = void 0; +class SchemaValidationError extends Error { + constructor(reason) { + super(`Schema validation failed: ${reason}`); + this.reason = reason; } } -exports.SchemaDefinition = SchemaDefinition; +exports.SchemaValidationError = SchemaValidationError; -},{}],54:[function(require,module,exports){ +},{}],56:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.SimpleCache = void 0; -const utils_1 = require("./utils"); -const calculateExpiryTime = (expirySeconds) => expirySeconds > 0 ? Date.now() + (expirySeconds * 1000) : Infinity; +exports.Storage = exports.SchemaValidationError = exports.StorageSettings = void 0; +var storage_settings_js_1 = require("./storage-settings.js"); +Object.defineProperty(exports, "StorageSettings", { enumerable: true, get: function () { return storage_settings_js_1.StorageSettings; } }); +var errors_js_1 = require("./errors.js"); +Object.defineProperty(exports, "SchemaValidationError", { enumerable: true, get: function () { return errors_js_1.SchemaValidationError; } }); +var storage_js_1 = require("./storage.js"); +Object.defineProperty(exports, "Storage", { enumerable: true, get: function () { return storage_js_1.Storage; } }); + +},{"./errors.js":55,"./storage-settings.js":60,"./storage.js":61}],57:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createIndex = void 0; +var create_index_js_1 = require("./create-index.js"); +Object.defineProperty(exports, "createIndex", { enumerable: true, get: function () { return create_index_js_1.createIndex; } }); + +},{"./create-index.js":46}],58:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.MSSQLStorage = exports.MSSQLStorageSettings = void 0; +const not_supported_js_1 = require("../../not-supported.js"); /** - * Simple cache implementation that retains immutable values in memory for a limited time. - * Immutability is enforced by cloning the stored and retrieved values. To change a cached value, it will have to be `set` again with the new value. + * Not supported in browser context */ -class SimpleCache { - get size() { return this.cache.size; } - constructor(options) { - var _a; - this.enabled = true; - if (typeof options === 'number') { - // Old signature: only expirySeconds given - options = { expirySeconds: options }; +class MSSQLStorageSettings extends not_supported_js_1.NotSupported { +} +exports.MSSQLStorageSettings = MSSQLStorageSettings; +/** + * Not supported in browser context + */ +class MSSQLStorage extends not_supported_js_1.NotSupported { +} +exports.MSSQLStorage = MSSQLStorage; + +},{"../../not-supported.js":42}],59:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.SQLiteStorage = exports.SQLiteStorageSettings = void 0; +const not_supported_js_1 = require("../../not-supported.js"); +/** + * Not supported in browser context + */ +class SQLiteStorageSettings extends not_supported_js_1.NotSupported { +} +exports.SQLiteStorageSettings = SQLiteStorageSettings; +/** + * Not supported in browser context + */ +class SQLiteStorage extends not_supported_js_1.NotSupported { +} +exports.SQLiteStorage = SQLiteStorage; + +},{"../../not-supported.js":42}],60:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.StorageSettings = void 0; +/** + * Storage Settings + */ +class StorageSettings { + constructor(settings = {}) { + /** + * in bytes, max amount of child data to store within a parent record before moving to a dedicated record. Default is 50 + * @default 50 + */ + this.maxInlineValueSize = 50; + /** + * Instead of throwing errors on undefined values, remove the properties automatically. Default is false + * @default false + */ + this.removeVoidProperties = false; + /** + * Target path to store database files in, default is `'.'` + * @default '.' + */ + this.path = '.'; + /** + * timeout setting for read and write locks in seconds. Operations taking longer than this will be aborted. Default is 120 seconds. + * @default 120 + */ + this.lockTimeout = 120; + /** + * optional type of storage class - used by `AceBaseStorage` to create different specific db files (data, transaction, auth etc) + * @see AceBaseStorageSettings see `AceBaseStorageSettings.type` for more info + */ + this.type = 'data'; + /** + * Whether the database should be opened in readonly mode + * @default false + */ + this.readOnly = false; + if (typeof settings.maxInlineValueSize === 'number') { + this.maxInlineValueSize = settings.maxInlineValueSize; } - options.cloneValues = options.cloneValues !== false; - if (typeof options.expirySeconds !== 'number' && typeof options.maxEntries !== 'number') { - throw new Error('Either expirySeconds or maxEntries must be specified'); + if (typeof settings.removeVoidProperties === 'boolean') { + this.removeVoidProperties = settings.removeVoidProperties; } - this.options = options; - this.cache = new Map(); - // Cleanup every minute - const interval = setInterval(() => { this.cleanUp(); }, 60 * 1000); - (_a = interval.unref) === null || _a === void 0 ? void 0 : _a.call(interval); - } - has(key) { - if (!this.enabled) { - return false; + if (typeof settings.path === 'string') { + this.path = settings.path; } - return this.cache.has(key); - } - get(key) { - if (!this.enabled) { - return null; + if (this.path.endsWith('/')) { + this.path = this.path.slice(0, -1); + } + if (typeof settings.lockTimeout === 'number') { + this.lockTimeout = settings.lockTimeout; + } + if (typeof settings.type === 'string') { + this.type = settings.type; + } + if (typeof settings.readOnly === 'boolean') { + this.readOnly = settings.readOnly; + } + if (['object', 'string'].includes(typeof settings.ipc)) { + this.ipc = settings.ipc; } - const entry = this.cache.get(key); - if (!entry) { - return null; - } // if (!entry || entry.expires <= Date.now()) { return null; } - entry.expires = calculateExpiryTime(this.options.expirySeconds); - entry.accessed = Date.now(); - return this.options.cloneValues ? (0, utils_1.cloneObject)(entry.value) : entry.value; } - set(key, value) { - if (this.options.maxEntries > 0 && this.cache.size >= this.options.maxEntries && !this.cache.has(key)) { - // console.warn(`* cache limit ${this.options.maxEntries} reached: ${this.cache.size}`); - // Remove an expired item or the one that was accessed longest ago - let oldest = null; - const now = Date.now(); - for (const [key, entry] of this.cache.entries()) { - if (entry.expires <= now) { - // Found an expired item. Remove it now and stop - this.cache.delete(key); - oldest = null; - break; +} +exports.StorageSettings = StorageSettings; + +},{}],61:[function(require,module,exports){ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Storage = void 0; +const acebase_core_1 = require("acebase-core"); +const index_js_1 = require("../ipc/index.js"); +const assert_js_1 = require("../assert.js"); +const index_js_2 = require("../data-index/index.js"); +const indexes_js_1 = require("./indexes.js"); +const index_js_3 = require("../promise-fs/index.js"); +const errors_js_1 = require("./errors.js"); +const node_errors_js_1 = require("../node-errors.js"); +const node_info_js_1 = require("../node-info.js"); +const node_value_types_js_1 = require("../node-value-types.js"); +const { compareValues, getChildValues, encodeString, defer } = acebase_core_1.Utils; +const DEBUG_MODE = false; +const SUPPORTED_EVENTS = ['value', 'child_added', 'child_changed', 'child_removed', 'mutated', 'mutations']; +// Add 'notify_*' event types for each event to enable data-less notifications, so data retrieval becomes optional +SUPPORTED_EVENTS.push(...SUPPORTED_EVENTS.map(event => `notify_${event}`)); +// eslint-disable-next-line @typescript-eslint/no-empty-function +const NOOP = () => { }; +class Storage extends acebase_core_1.SimpleEventEmitter { + createTid() { + return DEBUG_MODE ? ++this._lastTid : acebase_core_1.ID.generate(); + } + /** + * Base class for database storage, must be extended by back-end specific methods. + * Currently implemented back-ends are AceBaseStorage, SQLiteStorage, MSSQLStorage, CustomStorage + * @param name name of the database + * @param settings instance of AceBaseStorageSettings or SQLiteStorageSettings + */ + constructor(name, settings, env) { + var _a; + super(); + this.name = name; + this.settings = settings; + // private _validation = new Map boolean, schema?: SchemaDefinition }>; + this._schemas = []; + this._indexes = []; + this._annoucedIndexes = new Map(); + this.indexes = { + /** + * Tests if (the default storage implementation of) indexes are supported in the environment. + * They are currently only supported when running in Node.js because they use the fs filesystem. + * TODO: Implement storage specific indexes (eg in SQLite, MySQL, MSSQL, in-memory) + */ + get supported() { + return index_js_3.pfs === null || index_js_3.pfs === void 0 ? void 0 : index_js_3.pfs.hasFileSystem; + }, + create: (path, key, options = { + rebuild: false, + }) => { + const context = { storage: this, logger: this.logger, indexes: this._indexes, ipc: this.ipc }; + return (0, indexes_js_1.createIndex)(context, path, key, options); + }, + /** + * Returns indexes at a path, or a specific index on a key in that path + */ + get: (path, key = null) => { + if (path.includes('$')) { + // Replace $variables in path with * wildcards + const pathKeys = acebase_core_1.PathInfo.getPathKeys(path).map(key => typeof key === 'string' && key.startsWith('$') ? '*' : key); + path = (new acebase_core_1.PathInfo(pathKeys)).path; + } + return this._indexes.filter(index => index.path === path && + (key === null || key === index.key)); + }, + /** + * Returns all indexes on a target path, optionally includes indexes on child and parent paths + */ + getAll: (targetPath, options = { parentPaths: true, childPaths: true }) => { + const pathKeys = acebase_core_1.PathInfo.getPathKeys(targetPath); + return this._indexes.filter(index => { + const indexKeys = acebase_core_1.PathInfo.getPathKeys(index.path + '/*'); + // check if index is on a parent node of given path: + if (options.parentPaths && indexKeys.every((key, i) => { return key === '*' || pathKeys[i] === key; }) && [index.key].concat(...index.includeKeys).includes(pathKeys[indexKeys.length])) { + // eg: path = 'restaurants/1/location/lat', index is on 'restaurants(/*)', key 'location' + return true; + } + else if (indexKeys.length < pathKeys.length) { + // the index is on a higher path, and did not match above parent paths check + return false; + } + else if (!options.childPaths && indexKeys.length !== pathKeys.length) { + // no checking for indexes on child paths and index path has more or less keys than path + // eg: path = 'restaurants/1', index is on child path 'restaurants/*/reviews(/*)', key 'rating' + return false; + } + // check if all path's keys match the index path + // eg: path = 'restaurants/1', index is on 'restaurants(/*)', key 'name' + // or: path = 'restaurants/1', index is on 'restaurants/*/reviews(/*)', key 'rating' (and options.childPaths === true) + return pathKeys.every((key, i) => { + return [key, '*'].includes(indexKeys[i]); //key === indexKeys[i] || indexKeys[i] === '*'; + }); + }); + }, + /** + * Returns all indexes + */ + list: () => { + return this._indexes.slice(); + }, + /** + * Discovers and populates all created indexes + */ + load: async () => { + this._indexes.splice(0); + if (!index_js_3.pfs.hasFileSystem) { + // If pfs (fs) is not available, don't try using it + return; } - if (!oldest || entry.accessed < oldest.accessed) { - oldest = { key, accessed: entry.accessed }; + let files = []; + try { + files = (await index_js_3.pfs.readdir(`${this.settings.path}/${this.name}.acebase`)); + } + catch (err) { + if (err.code !== 'ENOENT') { + // If the directory is not found, there are no file indexes. (probably not supported by used storage class) + // Only complain if error is something else + this.logger.error(err); + } + } + const promises = []; + files.forEach(fileName => { + if (!fileName.endsWith('.idx')) { + return; + } + const needsStoragePrefix = this.settings.type !== 'data'; // auth indexes need to start with "[auth]-" and have to be ignored by other storage types + const hasStoragePrefix = /^\[[a-z]+\]-/.test(fileName); + if ((!needsStoragePrefix && !hasStoragePrefix) || needsStoragePrefix && fileName.startsWith(`[${this.settings.type}]-`)) { + const p = this.indexes.add(fileName); + promises.push(p); + } + }); + await Promise.all(promises); + }, + add: async (fileName) => { + const existingIndex = this._indexes.find(index => index.fileName === fileName); + if (existingIndex) { + return existingIndex; + } + else if (this._annoucedIndexes.has(fileName)) { + // Index is already in the process of being added, wait until it becomes availabe + const index = await this._annoucedIndexes.get(fileName); + return index; + } + try { + // Announce the index to prevent race condition in between reading and receiving the IPC index.created notification + const indexPromise = index_js_2.DataIndex.readFromFile(this, fileName); + this._annoucedIndexes.set(fileName, indexPromise); + const index = await indexPromise; + this._indexes.push(index); + this._annoucedIndexes.delete(fileName); + return index; + } + catch (err) { + this.logger.error(err); + return null; + } + }, + /** + * Deletes an index from the database + */ + delete: async (fileName) => { + const index = await this.indexes.remove(fileName); + await index.delete(); + this.ipc.sendNotification({ type: 'index.deleted', fileName: index.fileName, path: index.path, keys: index.key }); + }, + /** + * Removes an index from the list. Does not delete the actual file, `delete` does that! + * @returns returns the removed index + */ + remove: async (fileName) => { + const index = this._indexes.find(index => index.fileName === fileName); + if (!index) { + throw new Error(`Index ${fileName} not found`); + } + this._indexes.splice(this._indexes.indexOf(index), 1); + return index; + }, + close: async () => { + // Close all indexes + const promises = this.indexes.list().map(index => index.close().catch(err => this.logger.error(err))); + await Promise.all(promises); + }, + }; + this._eventSubscriptions = {}; + this.subscriptions = { + /** + * Adds a subscription to a node + * @param path Path to the node to add subscription to + * @param type Type of the subscription + * @param callback Subscription callback function + */ + add: (path, type, callback) => { + if (SUPPORTED_EVENTS.indexOf(type) < 0) { + throw new TypeError(`Invalid event type "${type}"`); + } + let pathSubs = this._eventSubscriptions[path]; + if (!pathSubs) { + pathSubs = this._eventSubscriptions[path] = []; + } + // if (pathSubs.findIndex(ps => ps.type === type && ps.callback === callback)) { + // this.logger.warn(`Identical subscription of type ${type} on path "${path}" being added`); + // } + pathSubs.push({ created: Date.now(), type, callback }); + this.emit('subscribe', { path, event: type, callback }); // Enables IPC peers to be notified + }, + /** + * Removes 1 or more subscriptions from a node + * @param path Path to the node to remove the subscription from + * @param type Type of subscription(s) to remove (optional: if omitted all types will be removed) + * @param callback Callback to remove (optional: if omitted all of the same type will be removed) + */ + remove: (path, type, callback) => { + const pathSubs = this._eventSubscriptions[path]; + if (!pathSubs) { + return; + } + const next = () => pathSubs.findIndex(ps => (type ? ps.type === type : true) && (callback ? ps.callback === callback : true)); + let i; + while ((i = next()) >= 0) { + pathSubs.splice(i, 1); } + this.emit('unsubscribe', { path, event: type, callback }); // Enables IPC peers to be notified + }, + /** + * Checks if there are any subscribers at given path that need the node's previous value when a change is triggered + * @param path + */ + hasValueSubscribersForPath(path) { + const valueNeeded = this.getValueSubscribersForPath(path); + return !!valueNeeded; + }, + /** + * Gets all subscribers at given path that need the node's previous value when a change is triggered + * @param path + */ + getValueSubscribersForPath: (path) => { + // Subscribers that MUST have the entire previous value of a node before updating: + // - "value" events on the path itself, and any ancestor path + // - "child_added", "child_removed" events on the parent path + // - "child_changed" events on the parent path and its ancestors + // - ALL events on child/descendant paths + const pathInfo = new acebase_core_1.PathInfo(path); + const valueSubscribers = []; + Object.keys(this._eventSubscriptions).forEach(subscriptionPath => { + if (pathInfo.equals(subscriptionPath) || pathInfo.isDescendantOf(subscriptionPath)) { + // path being updated === subscriptionPath, or a child/descendant path of it + // eg path === "posts/123/title" + // and subscriptionPath is "posts/123/title", "posts/$postId/title", "posts/123", "posts/*", "posts" etc + const pathSubs = this._eventSubscriptions[subscriptionPath]; + const eventPath = acebase_core_1.PathInfo.fillVariables(subscriptionPath, path); + pathSubs + .filter(sub => !sub.type.startsWith('notify_')) // notify events don't need additional value loading + .forEach(sub => { + let dataPath = null; + if (sub.type === 'value') { // ["value", "notify_value"].includes(sub.type) + dataPath = eventPath; + } + else if (['mutated', 'mutations'].includes(sub.type) && pathInfo.isDescendantOf(eventPath)) { //["mutated", "notify_mutated"].includes(sub.type) + dataPath = path; // Only needed data is the properties being updated in the targeted path + } + else if (sub.type === 'child_changed' && path !== eventPath) { // ["child_changed", "notify_child_changed"].includes(sub.type) + const childKey = acebase_core_1.PathInfo.getPathKeys(path.slice(eventPath.length).replace(/^\//, ''))[0]; + dataPath = acebase_core_1.PathInfo.getChildPath(eventPath, childKey); + } + else if (['child_added', 'child_removed'].includes(sub.type) && pathInfo.isChildOf(eventPath)) { //["child_added", "child_removed", "notify_child_added", "notify_child_removed"] + const childKey = acebase_core_1.PathInfo.getPathKeys(path.slice(eventPath.length).replace(/^\//, ''))[0]; + dataPath = acebase_core_1.PathInfo.getChildPath(eventPath, childKey); + } + if (dataPath !== null && !valueSubscribers.some(s => s.type === sub.type && s.eventPath === eventPath)) { + valueSubscribers.push({ type: sub.type, eventPath, dataPath, subscriptionPath }); + } + }); + } + }); + return valueSubscribers; + }, + /** + * Gets all subscribers at given path that could possibly be invoked after a node is updated + */ + getAllSubscribersForPath: (path) => { + const pathInfo = acebase_core_1.PathInfo.get(path); + const subscribers = []; + Object.keys(this._eventSubscriptions).forEach(subscriptionPath => { + // if (pathInfo.equals(subscriptionPath) //path === subscriptionPath + // || pathInfo.isDescendantOf(subscriptionPath) + // || pathInfo.isAncestorOf(subscriptionPath) + // ) { + if (pathInfo.isOnTrailOf(subscriptionPath)) { + const pathSubs = this._eventSubscriptions[subscriptionPath]; + const eventPath = acebase_core_1.PathInfo.fillVariables(subscriptionPath, path); + pathSubs.forEach(sub => { + let dataPath = null; + if (sub.type === 'value' || sub.type === 'notify_value') { + dataPath = eventPath; + } + else if (['child_changed', 'notify_child_changed'].includes(sub.type)) { + const childKey = path === eventPath || pathInfo.isAncestorOf(eventPath) + ? '*' + : acebase_core_1.PathInfo.getPathKeys(path.slice(eventPath.length).replace(/^\//, ''))[0]; + dataPath = acebase_core_1.PathInfo.getChildPath(eventPath, childKey); + } + else if (['mutated', 'mutations', 'notify_mutated', 'notify_mutations'].includes(sub.type)) { + dataPath = path; + } + else if (['child_added', 'child_removed', 'notify_child_added', 'notify_child_removed'].includes(sub.type) + && (pathInfo.isChildOf(eventPath) + || path === eventPath + || pathInfo.isAncestorOf(eventPath))) { + const childKey = path === eventPath || pathInfo.isAncestorOf(eventPath) + ? '*' + : acebase_core_1.PathInfo.getPathKeys(path.slice(eventPath.length).replace(/^\//, ''))[0]; + dataPath = acebase_core_1.PathInfo.getChildPath(eventPath, childKey); //NodePath(subscriptionPath).childPath(childKey); + } + if (dataPath !== null && !subscribers.some(s => s.type === sub.type && s.eventPath === eventPath && s.subscriptionPath === subscriptionPath)) { // && subscribers.findIndex(s => s.type === sub.type && s.dataPath === dataPath) < 0 + subscribers.push({ type: sub.type, eventPath, dataPath, subscriptionPath }); + } + }); + } + }); + return subscribers; + }, + /** + * Triggers subscription events to run on relevant nodes + * @param event Event type: "value", "child_added", "child_changed", "child_removed" + * @param path Path to the node the subscription is on + * @param dataPath path to the node the value is stored + * @param oldValue old value + * @param newValue new value + * @param context context used by the client that updated this data + */ + trigger: (event, path, dataPath, oldValue, newValue, context) => { + //console.warn(`Event "${event}" triggered on node "/${path}" with data of "/${dataPath}": `, newValue); + const pathSubscriptions = this._eventSubscriptions[path] || []; + pathSubscriptions.filter(sub => sub.type === event) + .forEach(sub => { + sub.callback(null, dataPath, newValue, oldValue, context); + // if (event.startsWith('notify_')) { + // // Notify only event, run callback without data + // sub.callback(null, dataPath); + // } + // else { + // // Run callback with data + // sub.callback(null, dataPath, newValue, oldValue); + // } + }); + }, + }; + this.logger = (_a = env.logger) !== null && _a !== void 0 ? _a : new acebase_core_1.DebugLogger(env.logLevel, `[${name}${typeof settings.type === 'string' && settings.type !== 'data' ? `:${settings.type}` : ''}]`); // `├ ${name} ┤` // `[🧱${name}]` + // Setup IPC to allow vertical scaling (multiple threads sharing locks and data) + const ipcName = name + (typeof settings.type === 'string' ? `_${settings.type}` : ''); + const ipcSocketSettings = typeof settings.ipc === 'object' && settings.ipc !== null && 'role' in settings.ipc && settings.ipc.role === 'socket' + ? settings.ipc + : null; + if (ipcSocketSettings || settings.ipc === 'socket' || settings.ipc instanceof index_js_1.NetIPCServer) { + const ipcSettings = Object.assign({ ipcName, server: settings.ipc instanceof index_js_1.NetIPCServer ? settings.ipc : null }, (ipcSocketSettings && { maxIdleTime: ipcSocketSettings.maxIdleTime, loggerPluginPath: ipcSocketSettings.loggerPluginPath })); + this.ipc = new index_js_1.IPCSocketPeer(this, ipcSettings); + } + else if (settings.ipc) { + const ipcClientSettings = settings.ipc; + if (typeof ipcClientSettings.port !== 'number') { + throw new Error('IPC port number must be a number'); } - if (oldest !== null) { - this.cache.delete(oldest.key); + if (!['master', 'worker'].includes(ipcClientSettings.role)) { + throw new Error(`IPC client role must be either "master" or "worker", not "${ipcClientSettings.role}"`); } + const ipcSettings = Object.assign({ dbname: ipcName }, ipcClientSettings); + this.ipc = new index_js_1.RemoteIPCPeer(this, ipcSettings); } - this.cache.set(key, { value: this.options.cloneValues ? (0, utils_1.cloneObject)(value) : value, added: Date.now(), accessed: Date.now(), expires: calculateExpiryTime(this.options.expirySeconds) }); - } - remove(key) { - this.cache.delete(key); - } - cleanUp() { - const now = Date.now(); - this.cache.forEach((entry, key) => { - if (entry.expires <= now) { - this.cache.delete(key); + else { + this.ipc = new index_js_1.IPCPeer(this, ipcName); + } + this.ipc.once('exit', (code) => { + // We can perform any custom cleanup here: + // - storage-acebase should close the db file + // - storage-mssql / sqlite should close connection + // - indexes should close their files + if (this.indexes.supported) { + this.indexes.close(); } }); + this.nodeLocker = { + lock: async (path, tid, write, comment) => { + const lock = await this.ipc.lock({ path, tid, write, comment }); + return lock; + }, + }; + // this.transactionManager = new IPCTransactionManager(this.ipc); + this._lastTid = 0; + } // end of constructor + async close() { + // Close the database by calling exit on the ipc channel, which will emit an 'exit' event when the database can be safely closed. + await this.ipc.exit(); } -} -exports.SimpleCache = SimpleCache; - -},{"./utils":61}],55:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.Colorize = exports.SetColorsEnabled = exports.ColorsSupported = exports.ColorStyle = void 0; -const process_1 = require("./process"); -// See from https://en.wikipedia.org/wiki/ANSI_escape_code -const FontCode = { - bold: 1, - dim: 2, - italic: 3, - underline: 4, - inverse: 7, - hidden: 8, - strikethrough: 94, -}; -const ColorCode = { - black: 30, - red: 31, - green: 32, - yellow: 33, - blue: 34, - magenta: 35, - cyan: 36, - white: 37, - grey: 90, - // Bright colors: - brightRed: 91, - // TODO, other bright colors -}; -const BgColorCode = { - bgBlack: 40, - bgRed: 41, - bgGreen: 42, - bgYellow: 43, - bgBlue: 44, - bgMagenta: 45, - bgCyan: 46, - bgWhite: 47, - bgGrey: 100, - bgBrightRed: 101, - // TODO, other bright colors -}; -const ResetCode = { - all: 0, - color: 39, - background: 49, - bold: 22, - dim: 22, - italic: 23, - underline: 24, - inverse: 27, - hidden: 28, - strikethrough: 29, -}; -var ColorStyle; -(function (ColorStyle) { - ColorStyle["reset"] = "reset"; - ColorStyle["bold"] = "bold"; - ColorStyle["dim"] = "dim"; - ColorStyle["italic"] = "italic"; - ColorStyle["underline"] = "underline"; - ColorStyle["inverse"] = "inverse"; - ColorStyle["hidden"] = "hidden"; - ColorStyle["strikethrough"] = "strikethrough"; - ColorStyle["black"] = "black"; - ColorStyle["red"] = "red"; - ColorStyle["green"] = "green"; - ColorStyle["yellow"] = "yellow"; - ColorStyle["blue"] = "blue"; - ColorStyle["magenta"] = "magenta"; - ColorStyle["cyan"] = "cyan"; - ColorStyle["grey"] = "grey"; - ColorStyle["bgBlack"] = "bgBlack"; - ColorStyle["bgRed"] = "bgRed"; - ColorStyle["bgGreen"] = "bgGreen"; - ColorStyle["bgYellow"] = "bgYellow"; - ColorStyle["bgBlue"] = "bgBlue"; - ColorStyle["bgMagenta"] = "bgMagenta"; - ColorStyle["bgCyan"] = "bgCyan"; - ColorStyle["bgWhite"] = "bgWhite"; - ColorStyle["bgGrey"] = "bgGrey"; -})(ColorStyle = exports.ColorStyle || (exports.ColorStyle = {})); -function ColorsSupported() { - // Checks for basic color support - if (typeof process_1.default === 'undefined' || !process_1.default.stdout || !process_1.default.env || !process_1.default.platform || process_1.default.platform === 'browser') { - return false; - } - if (process_1.default.platform === 'win32') { - return true; - } - const env = process_1.default.env; - if (env.COLORTERM) { - return true; - } - if (env.TERM === 'dumb') { - return false; - } - if (env.CI || env.TEAMCITY_VERSION) { - return !!env.TRAVIS; - } - if (['iTerm.app', 'HyperTerm', 'Hyper', 'MacTerm', 'Apple_Terminal', 'vscode'].includes(env.TERM_PROGRAM)) { - return true; - } - if (/^xterm-256|^screen|^xterm|^vt100|color|ansi|cygwin|linux/i.test(env.TERM)) { - return true; - } - return false; -} -exports.ColorsSupported = ColorsSupported; -let _enabled = ColorsSupported(); -function SetColorsEnabled(enabled) { - _enabled = ColorsSupported() && enabled; -} -exports.SetColorsEnabled = SetColorsEnabled; -function Colorize(str, style) { - if (!_enabled) { - return str; + get path() { + return `${this.settings.path}/${this.name}.acebase`; } - const openCodes = [], closeCodes = []; - const addStyle = (style) => { - if (style === ColorStyle.reset) { - openCodes.push(ResetCode.all); - } - else if (style in FontCode) { - openCodes.push(FontCode[style]); - closeCodes.push(ResetCode[style]); - } - else if (style in ColorCode) { - openCodes.push(ColorCode[style]); - closeCodes.push(ResetCode.color); + /** + * Checks if a value can be stored in a parent object, or if it should + * move to a dedicated record. Uses settings.maxInlineValueSize + * @param value + */ + valueFitsInline(value) { + if (typeof value === 'number' || typeof value === 'boolean' || value instanceof Date) { + return true; } - else if (style in BgColorCode) { - openCodes.push(BgColorCode[style]); - closeCodes.push(ResetCode.background); + else if (typeof value === 'string') { + if (value.length > this.settings.maxInlineValueSize) { + return false; + } + // if the string has unicode chars, its byte size will be bigger than value.length + const encoded = encodeString(value); + return encoded.length < this.settings.maxInlineValueSize; } - }; - if (style instanceof Array) { - style.forEach(addStyle); - } - else { - addStyle(style); - } - // const open = '\u001b[' + openCodes.join(';') + 'm'; - // const close = '\u001b[' + closeCodes.join(';') + 'm'; - const open = openCodes.map(code => '\u001b[' + code + 'm').join(''); - const close = closeCodes.map(code => '\u001b[' + code + 'm').join(''); - // return open + str + close; - return str.split('\n').map(line => open + line + close).join('\n'); -} -exports.Colorize = Colorize; -String.prototype.colorize = function (style) { - return Colorize(this, style); -}; - -},{"./process":52}],56:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.SimpleEventEmitter = void 0; -function runCallback(callback, data) { - try { - callback(data); - } - catch (err) { - console.error('Error in subscription callback', err); - } -} -const _subscriptions = Symbol('subscriptions'); -const _oneTimeEvents = Symbol('oneTimeEvents'); -class SimpleEventEmitter { - constructor() { - this[_subscriptions] = []; - this[_oneTimeEvents] = new Map(); - } - on(event, callback) { - if (this[_oneTimeEvents].has(event)) { - return runCallback(callback, this[_oneTimeEvents].get(event)); + else if (value instanceof acebase_core_1.PathReference) { + if (value.path.length > this.settings.maxInlineValueSize) { + return false; + } + // if the path has unicode chars, its byte size will be bigger than value.path.length + const encoded = encodeString(value.path); + return encoded.length < this.settings.maxInlineValueSize; + } + else if (value instanceof ArrayBuffer) { + return value.byteLength < this.settings.maxInlineValueSize; + } + else if (value instanceof Array) { + return value.length === 0; + } + else if (typeof value === 'object') { + return Object.keys(value).length === 0; + } + else { + throw new TypeError('What else is there?'); } - this[_subscriptions].push({ event, callback, once: false }); - return this; } - off(event, callback) { - this[_subscriptions] = this[_subscriptions].filter(s => s.event !== event || (callback && s.callback !== callback)); - return this; + /** + * 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 + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _writeNode(path, value, options) { + throw new Error('This method must be implemented by subclass'); } - once(event, callback) { - return new Promise(resolve => { - const ourCallback = (data) => { - resolve(data); - callback === null || callback === void 0 ? void 0 : callback(data); - }; - if (this[_oneTimeEvents].has(event)) { - runCallback(ourCallback, this[_oneTimeEvents].get(event)); + getUpdateImpact(path, suppressEvents) { + let topEventPath = path; + let hasValueSubscribers = false; + // Get all subscriptions that should execute on the data (includes events on child nodes as well) + const eventSubscriptions = suppressEvents ? [] : this.subscriptions.getAllSubscribersForPath(path); + // Get all subscriptions for data on this or ancestor nodes, determines what data to load before processing + const valueSubscribers = suppressEvents ? [] : this.subscriptions.getValueSubscribersForPath(path); + if (valueSubscribers.length > 0) { + hasValueSubscribers = true; + const eventPaths = valueSubscribers + .map(sub => { return { path: sub.dataPath, keys: acebase_core_1.PathInfo.getPathKeys(sub.dataPath) }; }) + .sort((a, b) => { + if (a.keys.length < b.keys.length) { + return -1; + } + else if (a.keys.length > b.keys.length) { + return 1; + } + return 0; + }); + const first = eventPaths[0]; + topEventPath = first.path; + if (valueSubscribers.filter(sub => sub.dataPath === topEventPath).every(sub => sub.type === 'mutated' || sub.type.startsWith('notify_'))) { + // Prevent loading of all data on path, so it'll only load changing properties + hasValueSubscribers = false; } - else { - this[_subscriptions].push({ event, callback: ourCallback, once: true }); + topEventPath = acebase_core_1.PathInfo.fillVariables(topEventPath, path); // fill in any wildcards in the subscription path + } + const indexes = this.indexes.getAll(path, { childPaths: true, parentPaths: true }) + .map(index => ({ index, keys: acebase_core_1.PathInfo.getPathKeys(index.path) })) + .sort((a, b) => { + if (a.keys.length < b.keys.length) { + return -1; } - }); + else if (a.keys.length > b.keys.length) { + return 1; + } + return 0; + }) + .map(obj => obj.index); + const keysFilter = []; + if (indexes.length > 0) { + indexes.sort((a, b) => { + if (typeof a._pathKeys === 'undefined') { + a._pathKeys = acebase_core_1.PathInfo.getPathKeys(a.path); + } + if (typeof b._pathKeys === 'undefined') { + b._pathKeys = acebase_core_1.PathInfo.getPathKeys(b.path); + } + if (a._pathKeys.length < b._pathKeys.length) { + return -1; + } + else if (a._pathKeys.length > b._pathKeys.length) { + return 1; + } + return 0; + }); + const topIndex = indexes[0]; + const topIndexPath = topIndex.path === path ? path : acebase_core_1.PathInfo.fillVariables(`${topIndex.path}/*`, path); + if (topIndexPath.length < topEventPath.length) { + // index is on a higher path than any value subscriber. + // eg: + // path = 'restaurants/1/rating' + // topEventPath = 'restaurants/1/rating' (because of 'value' event on 'restaurants/*/rating') + // topIndexPath = 'restaurants/1' (because of index on 'restaurants(/*)', key 'name', included key 'rating') + // set topEventPath to topIndexPath, but include only: + // - indexed keys on that path, + // - any additional child keys for all value event subscriptions in that path (they can never be different though?) + topEventPath = topIndexPath; + indexes.filter(index => index.path === topIndex.path).forEach(index => { + const keys = [index.key].concat(index.includeKeys); + keys.forEach(key => !keysFilter.includes(key) && keysFilter.push(key)); + }); + } + } + return { topEventPath, eventSubscriptions, valueSubscribers, hasValueSubscribers, indexes, keysFilter }; } - emit(event, data) { - if (this[_oneTimeEvents].has(event)) { - throw new Error(`Event "${event}" was supposed to be emitted only once`); + /** + * Wrapper for _writeNode, handles triggering change events, index updating. + * @returns Returns a promise that resolves with an object that contains storage specific details, + * plus the applied mutations if transaction logging is enabled + */ + async _writeNodeWithTracking(path, value, options = { + merge: false, + waitForIndexUpdates: true, + suppress_events: false, + context: null, + impact: null, + }) { + options = options || {}; + if (!options.tid && !options.transaction) { + throw new Error('_writeNodeWithTracking MUST be executed with a tid OR transaction!'); } - for (let i = 0; i < this[_subscriptions].length; i++) { - const s = this[_subscriptions][i]; - if (s.event !== event) { - continue; + options.merge = options.merge === true; + // Does the value meet schema requirements? + const validation = this.validateSchema(path, value, { updates: options.merge }); + if (!validation.ok) { + throw new errors_js_1.SchemaValidationError(validation.reason); + } + const tid = options.tid; + const transaction = options.transaction; + // Is anyone interested in the values changing on this path? + let topEventData = null; + const updateImpact = options.impact ? options.impact : this.getUpdateImpact(path, options.suppress_events); + const { topEventPath, eventSubscriptions, hasValueSubscribers, indexes } = updateImpact; + let { keysFilter } = updateImpact; + const writeNode = () => { + if (typeof options._customWriteFunction === 'function') { + return options._customWriteFunction(); } - runCallback(s.callback, data); - if (s.once) { - this[_subscriptions].splice(i, 1); - i--; + if (topEventData) { + // Pass loaded data to _writeNode, speeds up recursive calls + // This prevents reloading and/or overwriting of unchanged child nodes + const pathKeys = acebase_core_1.PathInfo.getPathKeys(path); + const eventPathKeys = acebase_core_1.PathInfo.getPathKeys(topEventPath); + const trailKeys = pathKeys.slice(eventPathKeys.length); + let currentValue = topEventData; + while (trailKeys.length > 0 && currentValue !== null) { + const childKey = trailKeys.shift(); + currentValue = typeof currentValue === 'object' && childKey in currentValue ? currentValue[childKey] : null; + } + options.currentValue = currentValue; } + return this._writeNode(path, value, options); + }; + const transactionLoggingEnabled = this.settings.transactions && this.settings.transactions.log === true; + if (eventSubscriptions.length === 0 && indexes.length === 0 && !transactionLoggingEnabled) { + // Nobody's interested in value changes. Write node without tracking + return writeNode(); } - return this; - } - emitOnce(event, data) { - if (this[_oneTimeEvents].has(event)) { - throw new Error(`Event "${event}" was supposed to be emitted only once`); + if (!hasValueSubscribers && options.merge === true && keysFilter.length === 0) { + // only load properties being updated + keysFilter = Object.keys(value); + if (topEventPath !== path) { + const trailPath = path.slice(topEventPath.length); + keysFilter = keysFilter.map(key => `${trailPath}/${key}`); + } } - this.emit(event, data); - this[_oneTimeEvents].set(event, data); // Mark event as being emitted once for future subscribers - this.off(event); // Remove all listeners for this event, they won't fire again - return this; - } - pipe(event, eventEmitter) { - this.on(event, (data) => { - eventEmitter.emit(event, data); - }); - } - pipeOnce(event, eventEmitter) { - this.once(event, (data) => { - eventEmitter.emitOnce(event, data); - }); - } -} -exports.SimpleEventEmitter = SimpleEventEmitter; - -},{}],57:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.SimpleObservable = void 0; -/** - * rxjs is an optional dependency that only needs installing when any of AceBase's observe methods are used. - * If for some reason rxjs is not available (eg in test suite), we can provide a shim. This class is used when - * `db.setObservable("shim")` is called - */ -class SimpleObservable { - constructor(create) { - this._active = false; - this._subscribers = []; - this._create = create; - } - subscribe(subscriber) { - if (!this._active) { - const next = (value) => { - // emit value to all subscribers - this._subscribers.forEach(s => { - try { - s(value); - } - catch (err) { - console.error('Error in subscriber callback:', err); - } - }); - }; - const observer = { next }; - this._cleanup = this._create(observer); - this._active = true; + const eventNodeInfo = await this.getNodeInfo(topEventPath, { transaction, tid }); + let currentValue = null; + if (eventNodeInfo.exists) { + const valueOptions = { transaction, tid }; + if (keysFilter.length > 0) { + valueOptions.include = keysFilter; + } + if (topEventPath === '' && typeof valueOptions.include === 'undefined') { + this.logger.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. Use "mutated" or "mutations" events instead'); + } + const node = await this.getNode(topEventPath, valueOptions); + currentValue = node.value; + } + topEventData = currentValue; + // Now proceed with node updating + const result = (await writeNode()) || {}; + // Build data for old/new comparison + let newTopEventData, modifiedData; + if (path === topEventPath) { + if (options.merge) { + if (topEventData === null) { + newTopEventData = value instanceof Array ? [] : {}; + } + else { + // Create shallow copy of previous object value + newTopEventData = topEventData instanceof Array ? [] : {}; + Object.keys(topEventData).forEach(key => { + newTopEventData[key] = topEventData[key]; + }); + } + } + else { + newTopEventData = value; + } + modifiedData = newTopEventData; } - this._subscribers.push(subscriber); - const unsubscribe = () => { - this._subscribers.splice(this._subscribers.indexOf(subscriber), 1); - if (this._subscribers.length === 0) { - this._active = false; - this._cleanup(); + else { + // topEventPath is on a higher path, so we have to adjust the value deeper down + const trailPath = path.slice(topEventPath.length).replace(/^\//, ''); + const trailKeys = acebase_core_1.PathInfo.getPathKeys(trailPath); + // Create shallow copy of the original object (let unchanged properties reference existing objects) + if (topEventData === null) { + // the node didn't exist prior to the update (or was not loaded) + newTopEventData = typeof trailKeys[0] === 'number' ? [] : {}; } - }; - const subscription = { - unsubscribe, - }; - return subscription; - } -} -exports.SimpleObservable = SimpleObservable; - -},{}],58:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.EventStream = exports.EventPublisher = exports.EventSubscription = void 0; -class EventSubscription { - /** - * @param stop function that stops the subscription from receiving future events - */ - constructor(stop) { - this.stop = stop; - this._internal = { - state: 'init', - activatePromises: [], - }; - } - /** - * Notifies when subscription is activated or canceled - * @param callback optional callback to run each time activation state changes - * @returns returns a promise that resolves once activated, or rejects when it is denied (and no callback was supplied) - */ - activated(callback) { - if (callback) { - this._internal.activatePromises.push({ callback }); - if (this._internal.state === 'active') { - callback(true); + else { + newTopEventData = topEventData instanceof Array ? [] : {}; + Object.keys(topEventData).forEach(key => { + newTopEventData[key] = topEventData[key]; + }); } - else if (this._internal.state === 'canceled') { - callback(false, this._internal.cancelReason); + modifiedData = newTopEventData; + while (trailKeys.length > 0) { + const childKey = trailKeys.shift(); + // Create shallow copy of object at target + if (!options.merge && trailKeys.length === 0) { + modifiedData[childKey] = value; + } + else { + const original = modifiedData[childKey]; + const shallowCopy = typeof childKey === 'number' ? [...original] : Object.assign({}, original); + modifiedData[childKey] = shallowCopy; + } + modifiedData = modifiedData[childKey]; } } - // Changed behaviour: now also returns a Promise when the callback is used. - // This allows for 1 activated call to both handle: first activation result, - // and any future events using the callback - return new Promise((resolve, reject) => { - if (this._internal.state === 'active') { - return resolve(); + if (options.merge) { + // Update target value with updates + Object.keys(value).forEach(key => { + modifiedData[key] = value[key]; + }); + } + // assert(topEventData !== newTopEventData, 'shallow copy must have been made!'); + const dataChanges = compareValues(topEventData, newTopEventData); + if (dataChanges === 'identical') { + result.mutations = []; + return result; + } + // Fix: remove null property values (https://github.com/appy-one/acebase/issues/2) + function removeNulls(obj) { + if (obj === null || typeof obj !== 'object') { + return obj; + } // Nothing to do + Object.keys(obj).forEach(prop => { + const val = obj[prop]; + if (val === null) { + delete obj[prop]; + if (obj instanceof Array) { + obj.length--; + } // Array items can only be removed from the end, + } + if (typeof val === 'object') { + removeNulls(val); + } + }); + } + removeNulls(newTopEventData); + // Trigger all index updates + // TODO: Let indexes subscribe to "mutations" event, saves a lot of work because we are preparing + // before/after copies of the relevant data here, and then the indexes go check what data changed... + const indexUpdates = []; + indexes.map(index => ({ index, keys: acebase_core_1.PathInfo.getPathKeys(index.path) })) + .sort((a, b) => { + // Deepest paths should fire first, then bubble up the tree + if (a.keys.length < b.keys.length) { + return 1; } - else if (this._internal.state === 'canceled' && !callback) { - return reject(new Error(this._internal.cancelReason)); + else if (a.keys.length > b.keys.length) { + return -1; } - // eslint-disable-next-line @typescript-eslint/no-empty-function - const noop = () => { }; - this._internal.activatePromises.push({ - resolve, - reject: callback ? noop : reject, // Don't reject when callback is used: let callback handle this (prevents UnhandledPromiseRejection if only callback is used) + return 0; + }) + .forEach(({ index }) => { + // Index is either on the top event path, or on a child path + // Example situation: + // path = "users/ewout/posts/1" (a post was added) + // topEventPath = "users/ewout" (a "child_changed" event was on "users") + // index.path is "users/*/posts" + // index must be called with data of "users/ewout/posts/1" + const pathKeys = acebase_core_1.PathInfo.getPathKeys(topEventPath); + const indexPathKeys = acebase_core_1.PathInfo.getPathKeys(index.path + '/*'); + const trailKeys = indexPathKeys.slice(pathKeys.length); + // let { oldValue, newValue } = updatedData; + const oldValue = topEventData; + const newValue = newTopEventData; + if (trailKeys.length === 0) { + (0, assert_js_1.assert)(pathKeys.length === indexPathKeys.length, 'check logic'); + // Index is on updated path + const p = this.ipc.isMaster + ? index.handleRecordUpdate(topEventPath, oldValue, newValue) + : this.ipc.sendRequest({ type: 'index.update', fileName: index.fileName, path: topEventPath, oldValue, newValue }); + indexUpdates.push(p); + return; // next index + } + const getAllIndexUpdates = (path, oldValue, newValue) => { + if (oldValue === null && newValue === null) { + return []; + } + const pathKeys = acebase_core_1.PathInfo.getPathKeys(path); + const indexPathKeys = acebase_core_1.PathInfo.getPathKeys(index.path + '/*'); + const trailKeys = indexPathKeys.slice(pathKeys.length); + if (trailKeys.length === 0) { + (0, assert_js_1.assert)(pathKeys.length === indexPathKeys.length, 'check logic'); + return [{ path, oldValue, newValue }]; + } + let results = []; + let trailPath = ''; + while (trailKeys.length > 0) { + const subKey = trailKeys.shift(); + if (typeof subKey === 'string' && (subKey === '*' || subKey.startsWith('$'))) { + // Recursion needed + const 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 childPath = acebase_core_1.PathInfo.getChildPath(trailPath, key); + const childValues = getChildValues(key, oldValue, newValue); + const subTrailPath = acebase_core_1.PathInfo.getChildPath(path, childPath); + const childResults = getAllIndexUpdates(subTrailPath, childValues.oldValue, childValues.newValue); + results = results.concat(childResults); + }); + break; + } + else { + const values = getChildValues(subKey, oldValue, newValue); + oldValue = values.oldValue; + newValue = values.newValue; + if (oldValue === null && newValue === null) { + break; + } + trailPath = acebase_core_1.PathInfo.getChildPath(trailPath, subKey); + } + } + return results; + }; + const results = getAllIndexUpdates(topEventPath, oldValue, newValue); + results.forEach(result => { + const p = this.ipc.isMaster + ? index.handleRecordUpdate(result.path, result.oldValue, result.newValue) + : this.ipc.sendRequest({ type: 'index.update', fileName: index.fileName, path: result.path, oldValue: result.oldValue, newValue: result.newValue }); + indexUpdates.push(p); }); }); - } - /** (for internal use) */ - _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(); + const callSubscriberWithValues = (sub, oldValue, newValue, variables = []) => { + let trigger = true; + let type = sub.type; + if (type.startsWith('notify_')) { + type = type.slice('notify_'.length); } - else { - p.callback && p.callback(false, cancelReason); - p.reject && p.reject(cancelReason); + if (type === 'mutated') { + return; // Ignore here, requires different logic } - } - } -} -exports.EventSubscription = EventSubscription; -class EventPublisher { - /** - * - * @param publish function that publishes a new value to subscribers, return if there are any active subscribers - * @param start function that notifies subscribers their subscription is activated - * @param cancel function that notifies subscribers their subscription has been canceled, removes all subscriptions - */ - constructor(publish, start, cancel) { - this.publish = publish; - this.start = start; - this.cancel = cancel; - } -} -exports.EventPublisher = EventPublisher; -class EventStream { - constructor(eventPublisherCallback) { - const subscribers = []; - let noMoreSubscribersCallback; - let activationState; // TODO: refactor to string only: STATE_INIT, STATE_STOPPED, STATE_ACTIVATED, STATE_CANCELED - const STATE_STOPPED = 'stopped (no more subscribers)'; - this.subscribe = (callback, activationCallback) => { - if (typeof callback !== 'function') { - throw new TypeError('callback must be a function'); + else if (type === 'child_changed' && (oldValue === null || newValue === null)) { + trigger = false; } - else if (activationState === STATE_STOPPED) { - throw new Error('stream can\'t be used anymore because all subscribers were stopped'); + else if (type === 'value' || type === 'child_changed') { + const changes = compareValues(oldValue, newValue); + trigger = changes !== 'identical'; } - const sub = { - callback, - activationCallback: function (activated, cancelReason) { - activationCallback === null || activationCallback === void 0 ? void 0 : activationCallback(activated, cancelReason); - this.subscription._setActivationState(activated, cancelReason); - }, - subscription: new EventSubscription(function stop() { - subscribers.splice(subscribers.indexOf(this), 1); - return checkActiveSubscribers(); - }), - }; - subscribers.push(sub); - if (typeof activationState !== 'undefined') { - if (activationState === true) { - activationCallback === null || activationCallback === void 0 ? void 0 : activationCallback(true); - sub.subscription._setActivationState(true); - } - else if (typeof activationState === 'string') { - activationCallback === null || activationCallback === void 0 ? void 0 : activationCallback(false, activationState); - sub.subscription._setActivationState(false, activationState); - } + else if (type === 'child_added') { + trigger = oldValue === null && newValue !== null; } - return sub.subscription; - }; - const checkActiveSubscribers = () => { - let ret; - if (subscribers.length === 0) { - ret = noMoreSubscribersCallback === null || noMoreSubscribersCallback === void 0 ? void 0 : noMoreSubscribersCallback(); - activationState = STATE_STOPPED; + else if (type === 'child_removed') { + trigger = oldValue !== null && newValue === null; } - return Promise.resolve(ret); - }; - this.unsubscribe = (callback) => { - const remove = callback - ? subscribers.filter(sub => sub.callback === callback) - : subscribers; - remove.forEach(sub => { - const i = subscribers.indexOf(sub); - subscribers.splice(i, 1); + if (!trigger) { + return; + } + const pathKeys = acebase_core_1.PathInfo.getPathKeys(sub.dataPath); + variables.forEach(variable => { + // only replaces first occurrence (so multiple *'s will be processed 1 by 1) + const index = pathKeys.indexOf(variable.name); + (0, assert_js_1.assert)(index >= 0, `Variable "${variable.name}" not found in subscription dataPath "${sub.dataPath}"`); + pathKeys[index] = variable.value; }); - checkActiveSubscribers(); + const dataPath = pathKeys.reduce((path, key) => acebase_core_1.PathInfo.getChildPath(path, key), ''); + this.subscriptions.trigger(sub.type, sub.subscriptionPath, dataPath, oldValue, newValue, options.context); }; - this.stop = () => { - // Stop (remove) all subscriptions - subscribers.splice(0); - checkActiveSubscribers(); + const prepareMutationEvents = (currentPath, oldValue, newValue, compareResult) => { + const batch = []; + const result = compareResult || compareValues(oldValue, newValue); + if (result === 'identical') { + return batch; // no changes on subscribed path + } + else if (typeof result === 'string') { + // We are on a path that has an actual change + batch.push({ path: currentPath, oldValue, newValue }); + } + // else if (oldValue instanceof Array || newValue instanceof Array) { + // // Trigger mutated event on the array itself instead of on individual indexes. + // // DO convert both arrays to objects because they are sparse + // const oldObj = {}, newObj = {}; + // result.added.forEach(index => { + // oldObj[index] = null; + // newObj[index] = newValue[index]; + // }); + // result.removed.forEach(index => { + // oldObj[index] = oldValue[index]; + // newObj[index] = null; + // }); + // result.changed.forEach(index => { + // oldObj[index] = oldValue[index]; + // newObj[index] = newValue[index]; + // }); + // batch.push({ path: currentPath, oldValue: oldObj, newValue: newObj }); + // } + else { + // DISABLED array handling here, because if a client is using a cache db this will cause problems + // because individual array entries should never be modified. + // if (oldValue instanceof Array && newValue instanceof Array) { + // // Make sure any removed events on arrays will be triggered from last to first + // result.removed.sort((a,b) => a < b ? 1 : -1); + // } + result.changed.forEach(info => { + const childPath = acebase_core_1.PathInfo.getChildPath(currentPath, info.key); + const childValues = getChildValues(info.key, oldValue, newValue); + const childBatch = prepareMutationEvents(childPath, childValues.oldValue, childValues.newValue, info.change); + batch.push(...childBatch); + }); + result.added.forEach(key => { + const childPath = acebase_core_1.PathInfo.getChildPath(currentPath, key); + batch.push({ path: childPath, oldValue: null, newValue: newValue[key] }); + }); + if (oldValue instanceof Array && newValue instanceof Array) { + result.removed.sort((a, b) => a < b ? 1 : -1); + } + result.removed.forEach(key => { + const childPath = acebase_core_1.PathInfo.getChildPath(currentPath, key); + batch.push({ path: childPath, oldValue: oldValue[key], newValue: null }); + }); + } + return batch; }; - /** - * For publishing side: adds a value that will trigger callbacks to all subscribers - * @param val - * @returns returns whether there are subscribers left - */ - const publish = (val) => { - subscribers.forEach(sub => { - try { - sub.callback(val); + // Add mutations to result (only if transaction logging is enabled) + if (transactionLoggingEnabled && this.settings.type !== 'transaction') { + result.mutations = (() => { + const trailPath = path.slice(topEventPath.length).replace(/^\//, ''); + const trailKeys = acebase_core_1.PathInfo.getPathKeys(trailPath); + let oldValue = topEventData, newValue = newTopEventData; + while (trailKeys.length > 0) { + const key = trailKeys.shift(); + ({ oldValue, newValue } = getChildValues(key, oldValue, newValue)); } - catch (err) { - console.error(`Error running subscriber callback: ${err.message}`); + const compareResults = compareValues(oldValue, newValue); + const batch = prepareMutationEvents(path, oldValue, newValue, compareResults); + const mutations = batch.map(m => ({ target: acebase_core_1.PathInfo.getPathKeys(m.path.slice(path.length)), prev: m.oldValue, val: m.newValue })); // key: PathInfo.get(m.path).key + return mutations; + })(); + } + const triggerAllEvents = () => { + // Notify all event subscriptions, should be executed with a delay + // this.logger.debug(`Triggering events caused by ${options && options.merge ? '(merge) ' : ''}write on "${path}":`, value); + eventSubscriptions + .filter(sub => !['mutated', 'mutations', 'notify_mutated', 'notify_mutations'].includes(sub.type)) + .map(sub => { + const keys = acebase_core_1.PathInfo.getPathKeys(sub.dataPath); + return { + sub, + keys, + }; + }) + .sort((a, b) => { + // Deepest paths should fire first, then bubble up the tree + if (a.keys.length < b.keys.length) { + return 1; + } + else if (a.keys.length > b.keys.length) { + return -1; + } + return 0; + }) + .forEach(({ sub }) => { + const process = (currentPath, oldValue, newValue, variables = []) => { + const trailPath = sub.dataPath.slice(currentPath.length).replace(/^\//, ''); + const trailKeys = acebase_core_1.PathInfo.getPathKeys(trailPath); + while (trailKeys.length > 0) { + const subKey = trailKeys.shift(); + if (typeof subKey === 'string' && (subKey === '*' || subKey[0] === '$')) { + // Fire on all relevant child keys + const allKeys = oldValue === null ? [] : Object.keys(oldValue).map(key => oldValue instanceof Array ? parseInt(key) : key); + newValue !== null && Object.keys(newValue).forEach(key => { + const keyOrIndex = newValue instanceof Array ? parseInt(key) : key; + !allKeys.includes(keyOrIndex) && 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(acebase_core_1.PathInfo.getChildPath(currentPath, subKey), childValues.oldValue, childValues.newValue, vars); + } + }); + return; // We can stop processing + } + else { + currentPath = acebase_core_1.PathInfo.getChildPath(currentPath, subKey); + const childValues = getChildValues(subKey, oldValue, newValue); + oldValue = childValues.oldValue; + newValue = childValues.newValue; + } + } + callSubscriberWithValues(sub, oldValue, newValue, variables); + }; + if (sub.type.startsWith('notify_') && acebase_core_1.PathInfo.get(sub.eventPath).isAncestorOf(topEventPath)) { + // Notify event on a higher path than we have loaded data on + // We can trigger the notify event on the subscribed path + // Eg: + // path === 'users/ewout', updates === { name: 'Ewout Stortenbeker' } + // sub.path === 'users' or '', sub.type === 'notify_child_changed' + // => OK to trigger if dataChanges !== 'removed' and 'added' + const isOnParentPath = acebase_core_1.PathInfo.get(sub.eventPath).isParentOf(topEventPath); + const trigger = (sub.type === 'notify_value') + || (sub.type === 'notify_child_changed' && (!isOnParentPath || !['added', 'removed'].includes(dataChanges))) + || (sub.type === 'notify_child_removed' && dataChanges === 'removed' && isOnParentPath) + || (sub.type === 'notify_child_added' && dataChanges === 'added' && isOnParentPath); + trigger && this.subscriptions.trigger(sub.type, sub.subscriptionPath, sub.dataPath, null, null, options.context); + } + else { + // Subscription is on current or deeper path + process(topEventPath, topEventData, newTopEventData); + } + }); + // The only events we haven't processed now are 'mutated' events. + // They require different logic: we'll call them for all nested properties of the updated path, that + // actually did change. They do not bubble up like 'child_changed' does. + const mutationEvents = eventSubscriptions.filter(sub => ['mutated', 'mutations', 'notify_mutated', 'notify_mutations'].includes(sub.type)); + mutationEvents.forEach(sub => { + // Get the target data this subscription is interested in + const currentPath = topEventPath; + // const trailPath = sub.eventPath.slice(currentPath.length).replace(/^\//, ''); // eventPath can contain vars and * ? + const trailKeys = acebase_core_1.PathInfo.getPathKeys(sub.eventPath).slice(acebase_core_1.PathInfo.getPathKeys(currentPath).length); //PathInfo.getPathKeys(trailPath); + const events = []; + const oldValue = topEventData; + const newValue = newTopEventData; + const processNextTrailKey = (target, currentTarget, oldValue, newValue, vars) => { + if (target.length === 0) { + // Add it + return events.push({ target: currentTarget, oldValue, newValue, vars }); + } + const subKey = target[0]; + const keys = new Set(); + const isWildcardKey = typeof subKey === 'string' && (subKey === '*' || subKey.startsWith('$')); + if (isWildcardKey) { + // Recursive for each key in oldValue and newValue + if (oldValue !== null && typeof oldValue === 'object') { + Object.keys(oldValue).forEach(key => keys.add(key)); + } + if (newValue !== null && typeof newValue === 'object') { + Object.keys(newValue).forEach(key => keys.add(key)); + } + } + else { + keys.add(subKey); // just one specific key + } + for (const key of keys) { + const childValues = getChildValues(key, oldValue, newValue); + oldValue = childValues.oldValue; + newValue = childValues.newValue; + processNextTrailKey(target.slice(1), currentTarget.concat(key), oldValue, newValue, isWildcardKey ? vars.concat({ name: subKey, value: key }) : vars); + } + }; + processNextTrailKey(trailKeys, [], oldValue, newValue, []); + for (const event of events) { + const targetPath = acebase_core_1.PathInfo.get(currentPath).child(event.target).path; + const batch = prepareMutationEvents(targetPath, event.oldValue, event.newValue); + if (batch.length === 0) { + continue; + } + const isNotifyEvent = sub.type.startsWith('notify_'); + if (['mutated', 'notify_mutated'].includes(sub.type)) { + // Send all mutations 1 by 1 + batch.forEach((mutation, index) => { + const context = options.context; // const context = cloneObject(options.context); + // context.acebase_mutated_event = { nr: index + 1, total: batch.length }; // Add context info about number of mutations + const prevVal = isNotifyEvent ? null : mutation.oldValue; + const newVal = isNotifyEvent ? null : mutation.newValue; + this.subscriptions.trigger(sub.type, sub.subscriptionPath, mutation.path, prevVal, newVal, context); + }); + } + else if (['mutations', 'notify_mutations'].includes(sub.type)) { + // Send 1 batch with all mutations + // const oldValues = isNotifyEvent ? null : batch.map(m => ({ target: PathInfo.getPathKeys(mutation.path.slice(sub.subscriptionPath.length)), val: m.oldValue })); // batch.reduce((obj, mutation) => (obj[mutation.path.slice(sub.subscriptionPath.length).replace(/^\//, '') || '.'] = mutation.oldValue, obj), {}); + // const newValues = isNotifyEvent ? null : batch.map(m => ({ target: PathInfo.getPathKeys(mutation.path.slice(sub.subscriptionPath.length)), val: m.newValue })) //batch.reduce((obj, mutation) => (obj[mutation.path.slice(sub.subscriptionPath.length).replace(/^\//, '') || '.'] = mutation.newValue, obj), {}); + const subscriptionPathKeys = acebase_core_1.PathInfo.getPathKeys(sub.subscriptionPath); + const values = isNotifyEvent ? null : batch.map(m => ({ target: acebase_core_1.PathInfo.getPathKeys(m.path).slice(subscriptionPathKeys.length), prev: m.oldValue, val: m.newValue })); + const dataPath = acebase_core_1.PathInfo.get(acebase_core_1.PathInfo.getPathKeys(targetPath).slice(0, subscriptionPathKeys.length)).path; + this.subscriptions.trigger(sub.type, sub.subscriptionPath, dataPath, null, values, options.context); + } } }); - if (subscribers.length === 0) { - checkActiveSubscribers(); - } - return subscribers.length > 0; - }; - /** - * For publishing side: let subscribers know their subscription is activated. Should be called only once - */ - const start = (allSubscriptionsStoppedCallback) => { - activationState = true; - noMoreSubscribersCallback = allSubscriptionsStoppedCallback; - subscribers.forEach(sub => { - var _a; - (_a = sub.activationCallback) === null || _a === void 0 ? void 0 : _a.call(sub, true); - }); - }; - /** - * For publishing side: let subscribers know their subscription has been canceled. Should be called only once - */ - const cancel = (reason) => { - activationState = reason; - subscribers.forEach(sub => { - var _a; - (_a = sub.activationCallback) === null || _a === void 0 ? void 0 : _a.call(sub, false, reason || new Error('unknown reason')); - }); - subscribers.splice(0); // Clear all }; - const publisher = new EventPublisher(publish, start, cancel); - eventPublisherCallback(publisher); - } -} -exports.EventStream = EventStream; - -},{}],59:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.deserialize2 = exports.serialize2 = exports.serialize = exports.detectSerializeVersion = exports.deserialize = void 0; -const path_reference_1 = require("./path-reference"); -const utils_1 = require("./utils"); -const ascii85_1 = require("./ascii85"); -const path_info_1 = require("./path-info"); -const partial_array_1 = require("./partial-array"); -/* - There are now 2 different serialization methods for transporting values. - - v1: - The original version (v1) created an object with "map" and "val" properties. - The "map" property was made optional in v1.14.1 so they won't be present for values needing no serializing - - v2: - The new version replaces serialized values inline by objects containing ".type" and ".val" properties. - This serializing method was introduced by `export` and `import` methods because they use streaming and - are unable to prepare type mappings up-front. This format is smaller in transmission (in many cases), - and easier to read and process. - - original: { "date": (some date) } - v1 serialized: { "map": { "date": "date" }, "val": { date: "2022-04-22T07:49:23Z" } } - v2 serialized: { "date": { ".type": "date", ".val": "2022-04-22T07:49:23Z" } } - - original: (some date) - v1 serialized: { "map": "date", "val": "2022-04-22T07:49:23Z" } - v2 serialized: { ".type": "date", ".val": "2022-04-22T07:49:23Z" } - comment: top level value that need serializing is wrapped in an object with ".type" and ".val". v1 is smaller in this case - - original: 'some string' - v1 serialized: { "map": {}, "val": "some string" } - v2 serialized: "some string" - comment: primitive types such as strings don't need serializing and are returned as is in v2 - - original: { "date": (some date), "text": "Some string" } - v1 serialized: { "map": { "date": "date" }, "val": { date: "2022-04-22T07:49:23Z", "text": "Some string" } } - v2 serialized: { "date": { ".type": "date", ".val": "2022-04-22T07:49:23Z" }, "text": "Some string" } -*/ -/** - * Original deserialization method using global `map` and `val` properties - * @param data - * @returns - */ -const deserialize = (data) => { - if (data.map === null || typeof data.map === 'undefined') { - if (typeof data.val === 'undefined') { - throw new Error('serialized value must have a val property'); + // Wait for all index updates to complete + if (options.waitForIndexUpdates === false) { + indexUpdates.splice(0); // Remove all index update promises, so we don't wait for them to resolve } - return data.val; + await Promise.all(indexUpdates); + defer(triggerAllEvents); // Delayed execution + return result; } - const deserializeValue = (type, val) => { - if (type === 'date') { - // Date was serialized as a string (UTC) - return new Date(val); - } - else if (type === 'binary') { - // ascii85 encoded binary data - return ascii85_1.ascii85.decode(val); - } - else if (type === 'reference') { - return new path_reference_1.PathReference(val); - } - else if (type === 'regexp') { - return new RegExp(val.pattern, val.flags); - } - else if (type === 'array') { - return new partial_array_1.PartialArray(val); - } - else if (type === 'bigint') { - return BigInt(val); - } - return val; - }; - if (typeof data.map === 'string') { - // Single value - return deserializeValue(data.map, data.val); + /** + * Enumerates all children of a given Node for reflection purposes + * @param path + * @param options optional options used by implementation for recursive calls + * @returns returns a generator object that calls .next for each child until the .next callback returns false + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getChildren(path, options) { + throw new Error('This method must be implemented by subclass'); } - Object.keys(data.map).forEach(path => { - const type = data.map[path]; - const keys = path_info_1.PathInfo.getPathKeys(path); - let parent = data; - let key = 'val'; - let val = data.val; - keys.forEach(k => { - key = k; - parent = val; - val = val[key]; // If an error occurs here, there's something wrong with the calling code... - }); - parent[key] = deserializeValue(type, val); - }); - return data.val; -}; -exports.deserialize = deserialize; -/** - * Function to detect the used serialization method with for the given object - * @param data - * @returns - */ -const detectSerializeVersion = (data) => { - if (typeof data !== 'object' || data === null) { - // This can only be v2, which allows primitive types to bypass serializing - return 2; + /** + * @deprecated Use `getNode` instead + * Gets a node's value by delegating to getNode, returning only the value + * @param path + * @param options optional options that can limit the amount of (sub)data being loaded, and any other implementation specific options for recusrsive calls + */ + async getNodeValue(path, options = {}) { + const node = await this.getNode(path, options); + return node.value; } - if ('map' in data && 'val' in data) { - return 1; + /** + * Gets a node's value and (if supported) revision + * @param path + * @param options optional options that can limit the amount of (sub)data being loaded, and any other implementation specific options for recusrsive calls + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getNode(path, options) { + throw new Error('This method must be implemented by subclass'); } - else if ('val' in data) { - // If it's v1, 'val' will be the only key in the object because serialize2 adds ".version": 2 to the object to prevent confusion. - if (Object.keys(data).length > 1) { - return 2; - } - return 1; + /** + * Retrieves info about a node (existence, wherabouts etc) + * @param {string} path + * @param {object} [options] optional options used by implementation for recursive calls + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getNodeInfo(path, options) { + throw new Error('This method must be implemented by subclass'); } - return 2; -}; -exports.detectSerializeVersion = detectSerializeVersion; -/** - * Original serialization method using global `map` and `val` properties - * @param data - * @returns - */ -const serialize = (obj) => { - var _a; - // Recursively find dates and binary data - if (obj === null || typeof obj !== 'object' || obj instanceof Date || obj instanceof ArrayBuffer || obj instanceof path_reference_1.PathReference || obj instanceof RegExp) { - // Single value - const ser = (0, exports.serialize)({ value: obj }); - return { - map: (_a = ser.map) === null || _a === void 0 ? void 0 : _a.value, - val: ser.val.value, - }; + /** + * Creates or overwrites a node. Delegates to updateNode on a parent if + * path is not the root. + * @param path + * @param value + * @param options optional options used by implementation for recursive calls + * @returns Returns a new cursor if transaction logging is enabled + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + setNode(path, value, options) { + throw new Error('This method must be implemented by subclass'); } - obj = (0, utils_1.cloneObject)(obj); // Make sure we don't alter the original object - const process = (obj, mappings, prefix) => { - if (obj instanceof partial_array_1.PartialArray) { - mappings[prefix] = 'array'; - } - Object.keys(obj).forEach(key => { - const val = obj[key]; - const path = prefix.length === 0 ? key : `${prefix}/${key}`; - if (typeof val === 'bigint') { - obj[key] = val.toString(); - mappings[path] = 'bigint'; + /** + * Updates a node by merging an existing node with passed updates object, + * or creates it by delegating to updateNode on the parent path. + * @param path + * @param updates object with key/value pairs + * @returns Returns a new cursor if transaction logging is enabled + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + updateNode(path, updates, options) { + throw new Error('This method must be implemented by subclass'); + } + /** + * Updates a node by getting its value, running a callback function that transforms + * the current value and returns the new value to be stored. Assures the read value + * does not change while the callback runs, or runs the callback again if it did. + * @param path + * @param callback function that transforms current value and returns the new value to be stored. Can return a Promise + * @param options optional options used by implementation for recursive calls + * @returns Returns a new cursor if transaction logging is enabled + */ + async transactNode(path, callback, options = { no_lock: false, suppress_events: false, context: null }) { + const useFakeLock = options && options.no_lock === true; + const tid = this.createTid(); + const lock = useFakeLock + ? { tid, release: NOOP } // Fake lock, we'll use revision checking & retrying instead + : await this.nodeLocker.lock(path, tid, true, 'transactNode'); + try { + let changed = false; + const changeCallback = () => { changed = true; }; + if (useFakeLock) { + // Monitor value changes + this.subscriptions.add(path, 'notify_value', changeCallback); } - else if (val instanceof Date) { - // serialize date to UTC string - obj[key] = val.toISOString(); - mappings[path] = 'date'; + const node = await this.getNode(path, { tid }); + const checkRevision = node.revision; + let newValue; + try { + newValue = callback(node.value); + if (newValue instanceof Promise) { + newValue = await newValue.catch(err => { + this.logger.error(`Error in transaction callback: ${err.message}`); + }); + } } - else if (val instanceof ArrayBuffer) { - // Serialize binary data with ascii85 - obj[key] = ascii85_1.ascii85.encode(val); //ascii85.encode(Buffer.from(val)).toString(); - mappings[path] = 'binary'; + catch (err) { + this.logger.error(`Error in transaction callback: ${err.message}`); } - else if (val instanceof path_reference_1.PathReference) { - obj[key] = val.path; - mappings[path] = 'reference'; + if (typeof newValue === 'undefined') { + // Callback did not return value. Cancel transaction + return; } - else if (val instanceof RegExp) { - // Queries using the 'matches' filter with a regular expression can now also be used on remote db's - obj[key] = { pattern: val.source, flags: val.flags }; - mappings[path] = 'regexp'; + // asserting revision is only needed when no_lock option was specified + if (useFakeLock) { + this.subscriptions.remove(path, 'notify_value', changeCallback); } - else if (typeof val === 'object' && val !== null) { - process(val, mappings, path); + if (changed) { + throw new node_errors_js_1.NodeRevisionError('Node changed'); } - }); - }; - const mappings = {}; - process(obj, mappings, ''); - const serialized = { val: obj }; - if (Object.keys(mappings).length > 0) { - serialized.map = mappings; - } - return serialized; -}; -exports.serialize = serialize; -/** - * New serialization method using inline `.type` and `.val` properties - * @param obj - * @returns - */ -const serialize2 = (obj) => { - // Recursively find data that needs serializing - const getSerializedValue = (val) => { - if (typeof val === 'bigint') { - // serialize bigint to string - return { - '.type': 'bigint', - '.val': val.toString(), - }; - } - else if (val instanceof Date) { - // serialize date to UTC string - return { - '.type': 'date', - '.val': val.toISOString(), - }; - } - else if (val instanceof ArrayBuffer) { - // Serialize binary data with ascii85 - return { - '.type': 'binary', - '.val': ascii85_1.ascii85.encode(val), - }; + const cursor = await this.setNode(path, newValue, { assert_revision: checkRevision, tid: lock.tid, suppress_events: options.suppress_events, context: options.context }); + return cursor; } - else if (val instanceof path_reference_1.PathReference) { - return { - '.type': 'reference', - '.val': val.path, - }; + catch (err) { + if (err instanceof node_errors_js_1.NodeRevisionError) { + // try again + console.warn(`node value changed, running again. Error: ${err.message}`); + return this.transactNode(path, callback, options); + } + else { + throw err; + } } - else if (val instanceof RegExp) { - // Queries using the 'matches' filter with a regular expression can now also be used on remote db's - return { - '.type': 'regexp', - '.val': `/${val.source}/${val.flags}`, // new: shorter - // '.val': { - // pattern: val.source, - // flags: val.flags - // } - }; + finally { + lock.release(); } - else if (typeof val === 'object' && val !== null) { - if (val instanceof Array) { - const copy = []; - for (let i = 0; i < val.length; i++) { - copy[i] = getSerializedValue(val[i]); + } + /** + * Checks if a node's value matches the passed criteria + * @param path + * @param criteria criteria to test + * @param options optional options used by implementation for recursive calls + * @returns returns a promise that resolves with a boolean indicating if it matched the criteria + */ + async matchNode(path, criteria, options) { + var _a; + const tid = (_a = options === null || options === void 0 ? void 0 : options.tid) !== null && _a !== void 0 ? _a : acebase_core_1.ID.generate(); + const checkNode = async (path, criteria) => { + if (criteria.length === 0) { + return Promise.resolve(true); // No criteria, so yes... It matches! + } + const criteriaKeys = criteria.reduce((keys, cr) => { + let key = cr.key; + if (typeof key === 'string' && key.includes('/')) { + // Descendant key criterium, use child key only (eg 'address' of 'address/city') + key = key.slice(0, key.indexOf('/')); } - return copy; + if (keys.indexOf(key) < 0) { + keys.push(key); + } + return keys; + }, []); + const unseenKeys = criteriaKeys.slice(); + let isMatch = true; + const delayedMatchPromises = []; + try { + await this.getChildren(path, { tid, keyFilter: criteriaKeys }).next(childInfo => { + var _a; + const keyOrIndex = (_a = childInfo.key) !== null && _a !== void 0 ? _a : childInfo.index; + unseenKeys.includes(keyOrIndex) && unseenKeys.splice(unseenKeys.indexOf(childInfo.key), 1); + const keyCriteria = criteria + .filter(cr => cr.key === keyOrIndex) + .map(cr => ({ op: cr.op, compare: cr.compare })); + const keyResult = keyCriteria.length > 0 ? checkChild(childInfo, keyCriteria) : { isMatch: true, promises: [] }; + isMatch = keyResult.isMatch; + if (isMatch) { + delayedMatchPromises.push(...keyResult.promises); + const childCriteria = criteria + .filter(cr => typeof cr.key === 'string' && cr.key.startsWith(`${typeof keyOrIndex === 'number' ? `[${keyOrIndex}]` : keyOrIndex}/`)) + .map(cr => { + const key = cr.key.slice(cr.key.indexOf('/') + 1); + return { key, op: cr.op, compare: cr.compare }; + }); + if (childCriteria.length > 0) { + const childPath = acebase_core_1.PathInfo.getChildPath(path, childInfo.key); + const childPromise = checkNode(childPath, childCriteria) + .then(isMatch => ({ isMatch })); + delayedMatchPromises.push(childPromise); + } + } + if (!isMatch || unseenKeys.length === 0) { + return false; // Stop iterating + } + }); + if (isMatch) { + const results = await Promise.all(delayedMatchPromises); + isMatch = results.every(res => res.isMatch); + } + if (!isMatch) { + return false; + } + // Now, also check keys that weren't found in the node. (a criterium may be "!exists") + isMatch = unseenKeys.every(keyOrIndex => { + const childInfo = new node_info_js_1.NodeInfo(Object.assign(Object.assign(Object.assign({}, (typeof keyOrIndex === 'number' && { index: keyOrIndex })), (typeof keyOrIndex === 'string' && { key: keyOrIndex })), { exists: false })); + const childCriteria = criteria + .filter(cr => typeof cr.key === 'string' && cr.key.startsWith(`${typeof keyOrIndex === 'number' ? `[${keyOrIndex}]` : keyOrIndex}/`)) + .map(cr => ({ op: cr.op, compare: cr.compare })); + if (childCriteria.length > 0 && !checkChild(childInfo, childCriteria).isMatch) { + return false; + } + const keyCriteria = criteria + .filter(cr => cr.key === keyOrIndex) + .map(cr => ({ op: cr.op, compare: cr.compare })); + if (keyCriteria.length === 0) { + return true; // There were only child criteria, and they matched (otherwise we wouldn't be here) + } + const result = checkChild(childInfo, keyCriteria); + return result.isMatch; + }); + return isMatch; } - else { - const copy = {}; //val instanceof Array ? [] : {} as SerializedValueV2; - if (val instanceof partial_array_1.PartialArray) { - // Mark the object as partial ("sparse") array - copy['.type'] = 'array'; + catch (err) { + this.logger.error(`Error matching on "${path}": `, err); + throw err; + } + }; // checkNode + /** + * + * @param child + * @param criteria criteria to test + */ + 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; } - for (const prop in val) { - copy[prop] = getSerializedValue(val[prop]); + else if (f.op === 'exists' || (f.op === '!=' && (typeof f.compare === 'undefined' || f.compare === null))) { + proceed = child.exists; + } + else if ((f.op === 'contains' || f.op === '!contains') && f.compare instanceof Array && f.compare.length === 0) { + // Added for #135: empty compare array for contains/!contains must match all values + proceed = true; + } + else if (!child.exists) { + proceed = false; + } + else { + if (child.address) { + if (child.valueType === node_value_types_js_1.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 }]) + .then(isMatch => { + return { key: child.key, isMatch }; + }); + promises.push(p); + proceed = true; + } + else if (child.valueType === node_value_types_js_1.VALUE_TYPES.ARRAY && ['contains', '!contains'].indexOf(f.op) >= 0) { + // TODO: refactor to use child stream + const p = this.getNode(child.path, { tid }) + .then(({ value: arr }) => { + // const i = arr.indexOf(f.compare); + // return { key: child.key, isMatch: (i >= 0 && f.op === "contains") || (i < 0 && f.op === "!contains") }; + const isMatch = f.op === 'contains' + // "contains" + ? f.compare instanceof Array + ? f.compare.every(val => arr.includes(val)) // Match if ALL of the passed values are in the array + : arr.includes(f.compare) + // "!contains" + : f.compare instanceof Array + ? !f.compare.some(val => arr.includes(val)) // DON'T match if ANY of the passed values is in the array + : !arr.includes(f.compare); + return { key: child.key, isMatch }; + }); + promises.push(p); + proceed = true; + } + else if (child.valueType === node_value_types_js_1.VALUE_TYPES.STRING) { + const p = this.getNode(child.path, { tid }) + .then(node => { + return { key: child.key, isMatch: this.test(node.value, f.op, f.compare) }; + }); + promises.push(p); + proceed = true; + } + else { + proceed = false; + } + } + else if (child.type === node_value_types_js_1.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 === node_value_types_js_1.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 { + let ret = this.test(child.value, f.op, f.compare); + if (ret instanceof Promise) { + promises.push(ret); + ret = true; + } + proceed = ret; + } } - return copy; - } - } - else { - // Primitive value. Don't serialize - return val; - } - }; - const serialized = getSerializedValue(obj); - if (serialized !== null && typeof serialized === 'object' && 'val' in serialized && Object.keys(serialized).length === 1) { - // acebase-core v1.14.1 made the 'map' property optional. - // This v2 serialized object might be confused with a v1 without mappings, because it only has a "val" property - // To prevent this, mark the serialized object with version 2 - serialized['.version'] = 2; - } - return serialized; -}; -exports.serialize2 = serialize2; -/** - * New deserialization method using inline `.type` and `.val` properties - * @param obj - * @returns - */ -const deserialize2 = (data) => { - if (typeof data !== 'object' || data === null) { - // primitive value, not serialized - return data; + return proceed; + }); // fs.every + return { isMatch, promises }; + }; // checkChild + return checkNode(path, criteria); } - if (typeof data['.type'] === 'undefined') { - // No type given: this is a plain object or array - if (data instanceof Array) { - // Plain array, deserialize items into a copy - const copy = []; - const arr = data; - for (let i = 0; i < arr.length; i++) { - copy.push((0, exports.deserialize2)(arr[i])); - } - return copy; + test(val, op, compare) { + if (op === '<') { + return val < compare; } - else { - // Plain object, deserialize properties into a copy - const copy = {}; - const obj = data; - for (const prop in obj) { - copy[prop] = (0, exports.deserialize2)(obj[prop]); - } - return copy; + if (op === '<=') { + return val <= compare; } - } - else if (typeof data['.type'] === 'string') { - const dataType = data['.type'].toLowerCase(); - if (dataType === 'bigint') { - const val = data['.val']; - return BigInt(val); + if (op === '==') { + return val === compare; } - else if (dataType === 'array') { - // partial ("sparse") array, deserialize children into a copy - const arr = data; - const copy = {}; - for (const index in arr) { - copy[index] = (0, exports.deserialize2)(arr[index]); - } - delete copy['.type']; - return new partial_array_1.PartialArray(copy); + if (op === '!=') { + return val !== compare; } - else if (dataType === 'date') { - // Date was serialized as a string (UTC) - const val = data['.val']; - return new Date(val); + if (op === '>') { + return val > compare; } - else if (dataType === 'binary') { - // ascii85 encoded binary data - const val = data['.val']; - return ascii85_1.ascii85.decode(val); + if (op === '>=') { + return val >= compare; } - else if (dataType === 'reference') { - const val = data['.val']; - return new path_reference_1.PathReference(val); + if (op === 'in') { + return compare.indexOf(val) >= 0; } - else if (dataType === 'regexp') { - const val = data['.val']; - if (typeof val === 'string') { - // serialized as '/(pattern)/flags' - const match = /^\/(.*)\/([a-z]+)$/.exec(val); - return new RegExp(match[1], match[2]); - } - // serialized as object with pattern & flags properties - return new RegExp(val.pattern, val.flags); + if (op === '!in') { + return compare.indexOf(val) < 0; } - } - throw new Error(`Unknown data type "${data['.type']}" in serialized value`); -}; -exports.deserialize2 = deserialize2; - -},{"./ascii85":37,"./partial-array":49,"./path-info":50,"./path-reference":51,"./utils":61}],60:[function(require,module,exports){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.TypeMappings = void 0; -const utils_1 = require("./utils"); -const path_info_1 = require("./path-info"); -const data_reference_1 = require("./data-reference"); -const data_snapshot_1 = require("./data-snapshot"); -/** - * (for internal use) - gets the mapping set for a specific path - */ -function get(mappings, path) { - // path points to the mapped (object container) location - path = path.replace(/^\/|\/$/g, ''); // trim slashes - const keys = path_info_1.PathInfo.getPathKeys(path); - const mappedPath = Object.keys(mappings).find(mpath => { - const mkeys = path_info_1.PathInfo.getPathKeys(mpath); - if (mkeys.length !== keys.length) { - return false; // Can't be a match + 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; } - return mkeys.every((mkey, index) => { - if (mkey === '*' || (typeof mkey === 'string' && mkey[0] === '$')) { - return true; // wildcard - } - return mkey === keys[index]; - }); - }); - const mapping = mappings[mappedPath]; - return mapping; -} -/** - * (for internal use) - gets the mapping set for a specific path's parent - */ -function map(mappings, path) { - // path points to the object location, its parent should have the mapping - const targetPath = path_info_1.PathInfo.get(path).parentPath; - if (targetPath === null) { - return; - } - return get(mappings, targetPath); -} -/** - * (for internal use) - gets all mappings set for a specific path and all subnodes - * @returns returns array of all matched mappings in path - */ -function mapDeep(mappings, entryPath) { - // returns mapping for this node, and all mappings for nested nodes - // entryPath: "users/ewout" - // mappingPath: "users" - // mappingPath: "users/*/posts" - entryPath = entryPath.replace(/^\/|\/$/g, ''); // trim slashes - // Start with current path's parent node - const pathInfo = path_info_1.PathInfo.get(entryPath); - const startPath = pathInfo.parentPath; - const keys = startPath ? path_info_1.PathInfo.getPathKeys(startPath) : []; - // Every path that starts with startPath, is a match - // TODO: refactor to return Object.keys(mappings),filter(...) - const matches = Object.keys(mappings).reduce((m, mpath) => { - //const mkeys = mpath.length > 0 ? mpath.split("/") : []; - const mkeys = path_info_1.PathInfo.getPathKeys(mpath); - if (mkeys.length < keys.length) { - return m; // Can't be a match + if (op === 'matches') { + return compare.test(val.toString()); } - let isMatch = true; - if (keys.length === 0 && startPath !== null) { - // Only match first node's children if mapping pattern is "*" or "$variable" - isMatch = mkeys.length === 1 && (mkeys[0] === '*' || (typeof mkeys[0] === 'string' && mkeys[0][0] === '$')); + if (op === '!matches') { + return !compare.test(val.toString()); } - else { - mkeys.every((mkey, index) => { - if (index >= keys.length) { - return false; // stop .every loop - } - else if ((mkey === '*' || (typeof mkey === 'string' && mkey[0] === '$')) || mkey === keys[index]) { - return true; // continue .every loop - } - else { - isMatch = false; - return false; // stop .every loop - } - }); + if (op === 'between') { + return val >= compare[0] && val <= compare[1]; } - if (isMatch) { - const mapping = mappings[mpath]; - m.push({ path: mpath, type: mapping }); + if (op === '!between') { + return val < compare[0] || val > compare[1]; + } + if (op === 'has' || op === '!has') { + const has = typeof val === 'object' && compare in val; + return op === 'has' ? has : !has; } - return m; - }, []); - return matches; -} -/** - * (for internal use) - serializes or deserializes an object using type mappings - * @returns returns the (de)serialized value - */ -function process(db, mappings, path, obj, action) { - if (obj === null || typeof obj !== 'object') { - return obj; + if (op === 'contains' || op === '!contains') { + // TODO: rename to "includes"? + const includes = typeof val === 'object' && val instanceof Array && val.includes(compare); + return op === 'contains' ? includes : !includes; + } + return false; } - const keys = path_info_1.PathInfo.getPathKeys(path); // path.length > 0 ? path.split("/") : []; - const m = mapDeep(mappings, path); - const changes = []; - m.sort((a, b) => path_info_1.PathInfo.getPathKeys(a.path).length > path_info_1.PathInfo.getPathKeys(b.path).length ? -1 : 1); // Deepest paths first - m.forEach(mapping => { - const mkeys = path_info_1.PathInfo.getPathKeys(mapping.path); //mapping.path.length > 0 ? mapping.path.split("/") : []; - mkeys.push('*'); - const mTrailKeys = mkeys.slice(keys.length); - if (mTrailKeys.length === 0) { - const vars = path_info_1.PathInfo.extractVariables(mapping.path, path); - const ref = new data_reference_1.DataReference(db, path, vars); - if (action === 'serialize') { - // serialize this object - obj = mapping.type.serialize(obj, ref); + /** + * Export a specific path's data to a stream + * @param path + * @param write function that writes to a stream, or stream object that has a write method that (optionally) returns a promise the export needs to wait for before continuing + * @returns returns a promise that resolves once all data is exported + */ + async exportNode(path, writeFn, options = { format: 'json', type_safe: true }) { + if ((options === null || options === void 0 ? void 0 : options.format) !== 'json') { + throw new Error('Only json output is currently supported'); + } + const write = typeof writeFn !== 'function' + ? writeFn.write.bind(writeFn) // Using the "old" stream argument. Use its write method for backward compatibility + : writeFn; + const stringifyValue = (type, val) => { + const escape = (str) => str + .replace(/\\/g, '\\\\') // forward slashes + .replace(/"/g, '\\"') // quotes + .replace(/\r/g, '\\r') // carriage return + .replace(/\n/g, '\\n') // line feed + .replace(/\t/g, '\\t') // tabs + .replace(/[\u0000-\u001f]/g, // other control characters + // other control characters + ch => `\\u${ch.charCodeAt(0).toString(16).padStart(4, '0')}`); + if (type === node_value_types_js_1.VALUE_TYPES.DATETIME) { + val = `"${val.toISOString()}"`; + if (options.type_safe) { + val = `{".type":"date",".val":${val}}`; // Previously: "Date" + } } - else if (action === 'deserialize') { - // deserialize this object - const snap = new data_snapshot_1.DataSnapshot(ref, obj); - obj = mapping.type.deserialize(snap); + else if (type === node_value_types_js_1.VALUE_TYPES.STRING) { + val = `"${escape(val)}"`; } - return; - } - // Find all nested objects at this trail path - const process = (parentPath, parent, keys) => { - if (obj === null || typeof obj !== 'object') { - return obj; + else if (type === node_value_types_js_1.VALUE_TYPES.ARRAY) { + val = '[]'; } - const key = keys[0]; - let children = []; - if (key === '*' || (typeof key === 'string' && key[0] === '$')) { - // Include all children - if (parent instanceof Array) { - children = parent.map((val, index) => ({ key: index, val })); - } - else { - children = Object.keys(parent).map(k => ({ key: k, val: parent[k] })); - } + else if (type === node_value_types_js_1.VALUE_TYPES.OBJECT) { + val = '{}'; } - else { - // Get the 1 child - const child = parent[key]; - if (typeof child === 'object') { - children.push({ key, val: child }); + else if (type === node_value_types_js_1.VALUE_TYPES.BINARY) { + val = `"${escape(acebase_core_1.ascii85.encode(val))}"`; // TODO: use base64 instead, no escaping needed + if (options.type_safe) { + val = `{".type":"binary",".val":${val}}`; // Previously: "Buffer" } } - children.forEach(child => { - const childPath = path_info_1.PathInfo.getChildPath(parentPath, child.key); - const vars = path_info_1.PathInfo.extractVariables(mapping.path, childPath); - const ref = new data_reference_1.DataReference(db, childPath, vars); - if (keys.length === 1) { - // TODO: this alters the existing object, we must build our own copy! - if (action === 'serialize') { - // serialize this object - changes.push({ parent, key: child.key, original: parent[child.key] }); - parent[child.key] = mapping.type.serialize(child.val, ref); - } - else if (action === 'deserialize') { - // deserialize this object - const snap = new data_snapshot_1.DataSnapshot(ref, child.val); - parent[child.key] = mapping.type.deserialize(snap); - } + else if (type === node_value_types_js_1.VALUE_TYPES.REFERENCE) { + val = `"${val.path}"`; + if (options.type_safe) { + val = `{".type":"reference",".val":${val}}`; // Previously: "PathReference" } - else { - // Dig deeper - process(childPath, child.val, keys.slice(1)); + } + else if (type === node_value_types_js_1.VALUE_TYPES.BIGINT) { + // Unfortnately, JSON.parse does not support 0n bigint json notation + val = `"${val}"`; + if (options.type_safe) { + val = `{".type":"bigint",".val":${val}}`; } - }); + } + return val; }; - process(path, obj, mTrailKeys); - }); - if (action === 'serialize') { - // Clone this serialized object so any types that remained - // will become plain objects without functions, and we can restore - // the original object's values if any mappings were processed. - // This will also prevent circular references - obj = (0, utils_1.cloneObject)(obj); - if (changes.length > 0) { - // Restore the changes made to the original object - changes.forEach(change => { - change.parent[change.key] = change.original; - }); + let objStart = '', objEnd = ''; + const nodeInfo = await this.getNodeInfo(path); + if (!nodeInfo.exists) { + return write('null'); } - } - return obj; -} -const _mappings = Symbol('mappings'); -class TypeMappings { - constructor(db) { - this.db = db; - this[_mappings] = {}; - } - /** (for internal use) */ - get mappings() { return this[_mappings]; } - /** (for internal use) */ - map(path) { - return map(this[_mappings], path); - } - /** - * Maps objects that are stored in a specific path to a class, so they can automatically be - * serialized when stored to, and deserialized (instantiated) when loaded from the database. - * @param path path to an object container, eg "users" or "users/*\/posts" - * @param type class to bind all child objects of path to - * Best practice is to implement 2 methods for instantiation and serializing of your objects: - * 1) `static create(snap: DataSnapshot)` and 2) `serialize(ref: DataReference)`. See example - * @param options (optional) You can specify the functions to use to - * serialize and/or instantiate your class. If you do not specificy a creator (constructor) method, - * AceBase will call `YourClass.create(snapshot)` method if it exists, or create an instance of - * YourClass with `new YourClass(snapshot)`. - * If you do not specifiy a serializer method, AceBase will call `YourClass.prototype.serialize(ref)` - * if it exists, or tries storing your object's fields unaltered. NOTE: `this` in your creator - * function will point to `YourClass`, and `this` in your serializer function will point to the - * `instance` of `YourClass`. - * @example - * class User { - * static create(snap: DataSnapshot): User { - * // Deserialize (instantiate) User from plain database object - * let user = new User(); - * Object.assign(user, snap.val()); // Copy all properties to user - * user.id = snap.ref.key; // Add the key as id - * return user; - * } - * serialize(ref: DataReference) { - * // Serialize user for database storage - * return { - * name: this.name - * email: this.email - * }; - * } - * } - * db.types.bind('users', User); // Automatically uses serialize and static create methods - */ - bind(path, type, options = {}) { - // Maps objects that are stored in a specific path to a constructor method, - // so they are automatically deserialized - if (typeof path !== 'string') { - throw new TypeError('path must be a string'); + else if (nodeInfo.type === node_value_types_js_1.VALUE_TYPES.OBJECT) { + objStart = '{'; + objEnd = '}'; } - if (typeof type !== 'function') { - throw new TypeError('constructor must be a function'); + else if (nodeInfo.type === node_value_types_js_1.VALUE_TYPES.ARRAY) { + objStart = '['; + objEnd = ']'; } - if (typeof options.serializer === 'undefined') { - // if (typeof type.prototype.serialize === 'function') { - // // Use .serialize instance method - // options.serializer = type.prototype.serialize; - // } - // Use object's serialize method upon serialization (if available) + else { + // Node has no children, get and export its value + const node = await this.getNode(path); + const val = stringifyValue(nodeInfo.type, node.value); + return write(val); } - else if (typeof options.serializer === 'string') { - if (typeof type.prototype[options.serializer] === 'function') { - options.serializer = type.prototype[options.serializer]; + if (objStart) { + const p = write(objStart); + if (p instanceof Promise) { + await p; + } + } + let output = '', outputCount = 0; + const pending = []; + await this.getChildren(path) + .next(childInfo => { + if (childInfo.address) { + // Export child recursively + pending.push(childInfo); } else { - throw new TypeError(`${type.name}.prototype.${options.serializer} is not a function, cannot use it as serializer`); + if (outputCount++ > 0) { + output += ','; + } + if (typeof childInfo.key === 'string') { + output += `"${childInfo.key}":`; + } + output += stringifyValue(childInfo.type, childInfo.value); } - } - 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') { - // Use static .create as creator method - options.creator = type.create; + }); + if (output) { + const p = write(output); + if (p instanceof Promise) { + await p; } } - else if (typeof options.creator === 'string') { - if (typeof type[options.creator] === 'function') { - options.creator = type[options.creator]; + while (pending.length > 0) { + const childInfo = pending.shift(); + let output = outputCount++ > 0 ? ',' : ''; + const key = typeof childInfo.index === 'number' ? childInfo.index : childInfo.key; + if (typeof key === 'string') { + output += `"${key}":`; } - else { - throw new TypeError(`${type.name}.${options.creator} is not a function, cannot use it as creator`); + if (output) { + const p = write(output); + if (p instanceof Promise) { + await p; + } } + await this.exportNode(acebase_core_1.PathInfo.getChildPath(path, key), write, options); } - 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`); + if (objEnd) { + const p = write(objEnd); + if (p instanceof Promise) { + await p; + } } - path = path.replace(/^\/|\/$/g, ''); // trim slashes - this[_mappings][path] = { - db: this.db, - type, - creator: options.creator, - serializer: options.serializer, - deserialize(snap) { - // run constructor method - 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; - }, - }; } /** - * @internal (for internal use) - * Serializes any child in given object that has a type mapping - * @param path | path to the object's location - * @param obj object to serialize - */ - serialize(path, obj) { - return process(this.db, this[_mappings], path, obj, 'serialize'); - } - /** - * @internal (for internal use) - * Deserialzes any child in given object that has a type mapping - * @param path path to the object's location - * @param obj object to deserialize + * Import a specific path's data from a stream + * @param path + * @param read read function that streams a new chunk of data + * @returns returns a promise that resolves once all data is imported */ - deserialize(path, obj) { - return process(this.db, this[_mappings], path, obj, 'deserialize'); - } -} -exports.TypeMappings = TypeMappings; - -},{"./data-reference":42,"./data-snapshot":43,"./path-info":50,"./utils":61}],61:[function(require,module,exports){ -(function (global,Buffer){(function (){ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getGlobalObject = exports.defer = exports.getChildValues = exports.getMutations = exports.compareValues = exports.ObjectDifferences = exports.valuesAreEqual = exports.cloneObject = exports.concatTypedArrays = exports.decodeString = exports.encodeString = exports.bytesToBigint = exports.bigintToBytes = exports.bytesToNumber = exports.numberToBytes = void 0; -const path_reference_1 = require("./path-reference"); -const process_1 = require("./process"); -const partial_array_1 = require("./partial-array"); -function numberToBytes(number) { - const bytes = new Uint8Array(8); - const view = new DataView(bytes.buffer); - view.setFloat64(0, number); - return new Array(...bytes); -} -exports.numberToBytes = numberToBytes; -function bytesToNumber(bytes) { - const length = Array.isArray(bytes) ? bytes.length : bytes.byteLength; - if (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; -} -exports.bytesToNumber = bytesToNumber; -const hasBigIntSupport = (() => { - try { - return typeof BigInt(0) === 'bigint'; - } - catch (err) { - return false; - } -})(); -const noBigIntError = 'BigInt is not supported on this platform'; -const bigIntFunctions = { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - bigintToBytes(number) { throw new Error(noBigIntError); }, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - bytesToBigint(bytes) { throw new Error(noBigIntError); }, -}; -if (hasBigIntSupport) { - const big = { - zero: BigInt(0), - one: BigInt(1), - two: BigInt(2), - eight: BigInt(8), - ff: BigInt(0xff), - }; - bigIntFunctions.bigintToBytes = function bigintToBytes(number) { - if (typeof number !== 'bigint') { - throw new Error('number must be a bigint'); - } - const bytes = []; - const negative = number < big.zero; - do { - const byte = Number(number & big.ff); // NOTE: bits are inverted on negative numbers - bytes.push(byte); - number = number >> big.eight; - } while (number !== (negative ? -big.one : big.zero)); - bytes.reverse(); // little-endian - if (negative ? bytes[0] < 128 : bytes[0] >= 128) { - bytes.unshift(negative ? 255 : 0); // extra sign byte needed - } - return bytes; - }; - bigIntFunctions.bytesToBigint = function bytesToBigint(bytes) { - const negative = bytes[0] >= 128; - let number = big.zero; - for (let b of bytes) { - if (negative) { - b = ~b & 0xff; - } // Invert the bits - number = (number << big.eight) + BigInt(b); - } - if (negative) { - number = -(number + big.one); - } - return number; - }; -} -exports.bigintToBytes = bigIntFunctions.bigintToBytes; -exports.bytesToBigint = bigIntFunctions.bytesToBigint; -/** - * Converts a string to a utf-8 encoded Uint8Array - */ -function encodeString(str) { - if (typeof TextEncoder !== 'undefined') { - // Modern browsers, Node.js v11.0.0+ (or v8.3.0+ with util.TextEncoder) - const encoder = new TextEncoder(); - return encoder.encode(str); - } - else if (typeof Buffer === 'function') { - // Node.js - const buf = Buffer.from(str, 'utf-8'); - return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength); - } - else { - // Older browsers. Manually encode - const arr = []; - for (let i = 0; i < str.length; i++) { - let code = str.charCodeAt(i); - if (code > 128) { - // Attempt simple UTF-8 conversion. See https://en.wikipedia.org/wiki/UTF-8 - if ((code & 0xd800) === 0xd800) { - // code starts with 1101 10...: this is a 2-part utf-16 char code - const nextCode = str.charCodeAt(i + 1); - if ((nextCode & 0xdc00) !== 0xdc00) { - // next code must start with 1101 11... - throw new Error('follow-up utf-16 character does not start with 0xDC00'); - } - i++; - const p1 = code & 0x3ff; // Only use last 10 bits - const p2 = nextCode & 0x3ff; - // Create code point from these 2: (see https://en.wikipedia.org/wiki/UTF-16) - code = 0x10000 | (p1 << 10) | p2; + async importNode(path, read, options = { format: 'json', method: 'set' }) { + const chunkSize = 256 * 1024; // 256KB + const maxQueueBytes = 1024 * 1024; // 1MB + const state = { + data: '', + index: 0, + offset: 0, + queue: [], + queueStartByte: 0, + timesFlushed: 0, + get processedBytes() { + return this.offset + this.index; + }, + }; + const readNextChunk = async (append = false) => { + let data = await read(chunkSize); + if (data === null) { + if (state.data) { + throw new Error(`Unexpected EOF at index ${state.offset + state.data.length}`); } - if (code < 2048) { - // Use 2 bytes for 11 bit value, first byte starts with 110xxxxx (0xc0), 2nd byte with 10xxxxxx (0x80) - const b1 = 0xc0 | ((code >> 6) & 0x1f); // 0xc0 = 11000000, 0x1f = 11111 - const b2 = 0x80 | (code & 0x3f); // 0x80 = 10000000, 0x3f = 111111 - arr.push(b1, b2); + else { + throw new Error('Unable to read data from stream'); } - else if (code < 65536) { - // Use 3 bytes for 16-bit value, bits per byte: 4, 6, 6 - const b1 = 0xe0 | ((code >> 12) & 0xf); // 0xe0 = 11100000, 0xf = 1111 - const b2 = 0x80 | ((code >> 6) & 0x3f); // 0x80 = 10000000, 0x3f = 111111 - const b3 = 0x80 | (code & 0x3f); - arr.push(b1, b2, b3); + } + else if (typeof data === 'object') { + data = acebase_core_1.Utils.decodeString(data); + } + if (append) { + state.data += data; + } + else { + state.offset += state.data.length; + state.data = data; + state.index = 0; + } + }; + const readBytes = async (length) => { + let str = ''; + if (state.index + length >= state.data.length) { + str = state.data.slice(state.index); + length -= str.length; + await readNextChunk(); + } + str += state.data.slice(state.index, state.index + length); + state.index += length; + return str; + }; + const assertBytes = async (length) => { + if (state.index + length > state.data.length) { + await readNextChunk(true); + } + if (state.index + length > state.data.length) { + throw new Error('Not enough data available from stream'); + } + }; + const consumeToken = async (token) => { + // const str = state.data.slice(state.index, state.index + token.length); + const str = await readBytes(token.length); + if (str !== token) { + throw new Error(`Unexpected character "${str[0]}" at index ${state.offset + state.index}, expected "${token}"`); + } + }; + const consumeSpaces = async () => { + const spaces = [' ', '\t', '\r', '\n']; + while (true) { + if (state.index >= state.data.length) { + await readNextChunk(); } - else if (code < 2097152) { - // Use 4 bytes for 21-bit value, bits per byte: 3, 6, 6, 6 - const b1 = 0xf0 | ((code >> 18) & 0x7); // 0xf0 = 11110000, 0x7 = 111 - const b2 = 0x80 | ((code >> 12) & 0x3f); // 0x80 = 10000000, 0x3f = 111111 - const b3 = 0x80 | ((code >> 6) & 0x3f); // 0x80 = 10000000, 0x3f = 111111 - const b4 = 0x80 | (code & 0x3f); - arr.push(b1, b2, b3, b4); + if (spaces.includes(state.data[state.index])) { + state.index++; } else { - throw new Error(`Cannot convert character ${str.charAt(i)} (code ${code}) to utf-8`); + break; } } - else { - arr.push(code < 128 ? code : 63); // 63 = ? + }; + /** + * Reads number of bytes from the stream but does not consume them + */ + const peekBytes = async (length) => { + await assertBytes(length); + const index = state.index; + return state.data.slice(index, index + length); + }; + /** + * Tries to detect what type of value to expect, but does not read it + * @returns + */ + const peekValueType = async () => { + await consumeSpaces(); + const ch = await peekBytes(1); + switch (ch) { + case '"': return 'string'; + case '{': return 'object'; + case '[': return 'array'; + case 'n': return 'null'; + case 'u': return 'undefined'; + case 't': + case 'f': + return 'boolean'; + default: { + if (ch === '-' || (ch >= '0' && ch <= '9')) { + return 'number'; + } + throw new Error(`Unknown value at index ${state.offset + state.index}`); + } } - } - return new Uint8Array(arr); - } -} -exports.encodeString = encodeString; -/** - * Converts a utf-8 encoded buffer to string - */ -function decodeString(buffer) { - if (typeof TextDecoder !== 'undefined') { - // Modern browsers, Node.js v11.0.0+ (or v8.3.0+ with util.TextDecoder) - 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') { - // Node.js (v10 and below) - if (buffer instanceof Array) { - buffer = Uint8Array.from(buffer); // convert to typed array - } - if (!(buffer instanceof Buffer) && 'buffer' in buffer && buffer.buffer instanceof ArrayBuffer) { - const typedArray = buffer; - buffer = Buffer.from(typedArray.buffer, typedArray.byteOffset, typedArray.byteLength); // Convert typed array to node.js Buffer - } - if (!(buffer instanceof Buffer)) { - throw new Error('Unsupported buffer argument'); - } - return buffer.toString('utf-8'); - } - else { - // Older browsers. Manually decode! - if (!(buffer instanceof Uint8Array) && 'buffer' in buffer && buffer['buffer'] instanceof ArrayBuffer) { - // Convert TypedArray to Uint8Array - const typedArray = buffer; - buffer = new Uint8Array(typedArray.buffer, typedArray.byteOffset, typedArray.byteLength); - } - if (buffer instanceof Buffer || buffer instanceof Array || buffer instanceof Uint8Array) { + }; + /** + * Reads a string from the stream at current index. Expects current character to be " + */ + const readString = async () => { + await consumeToken('"'); let str = ''; - for (let i = 0; i < buffer.length; i++) { - let code = buffer[i]; - if (code > 128) { - // Decode Unicode character - if ((code & 0xf0) === 0xf0) { - // 4 byte char - const b1 = code, b2 = buffer[i + 1], b3 = buffer[i + 2], b4 = buffer[i + 3]; - code = ((b1 & 0x7) << 18) | ((b2 & 0x3f) << 12) | ((b3 & 0x3f) << 6) | (b4 & 0x3f); - i += 3; + let i = state.index; + // Read until next (unescaped) quote + while (state.data[i] !== '"' || state.data[i - 1] === '\\') { + i++; + if (i >= state.data.length) { + str += state.data.slice(state.index); + await readNextChunk(); + i = 0; + } + } + str += state.data.slice(state.index, i); + state.index = i + 1; + return unescape(str); + }; + const readBoolean = async () => { + if (state.data[state.index] === 't') { + await consumeToken('true'); + } + else if (state.data[state.index] === 'f') { + await consumeToken('false'); + } + throw new Error(`Expected true or false at index ${state.offset + state.index}`); + }; + const readNumber = async () => { + let str = ''; + let i = state.index; + // Read until non-number character is encountered + const nrChars = ['-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', 'e', 'b', 'f', 'x', 'o', 'n']; // b: 0b110101, x: 0x3a, o: 0o01, n: 29723n, e: 10e+23, f: ? + while (nrChars.includes(state.data[i])) { + i++; + if (i >= state.data.length) { + str += state.data.slice(state.index); + await readNextChunk(); + i = 0; + } + } + str += state.data.slice(state.index, i); + state.index = i; + const nr = str.endsWith('n') ? BigInt(str.slice(0, -1)) : str.includes('.') ? parseFloat(str) : parseInt(str); + return nr; + }; + const readValue = async () => { + await consumeSpaces(); + const type = await peekValueType(); + const value = await (() => { + switch (type) { + case 'string': return readString(); + case 'object': return {}; + case 'array': return []; + case 'number': return readNumber(); + case 'null': return null; + case 'undefined': return undefined; + case 'boolean': return readBoolean(); + } + })(); + return { type, value }; + }; + const unescape = (str) => str.replace(/\\n/g, '\n').replace(/\\"/g, '"'); + const getTypeSafeValue = (path, obj) => { + const type = obj['.type']; + let val = obj['.val']; + switch (type) { + case 'Date': + case 'date': { + val = new Date(val); + break; + } + case 'Buffer': + case 'binary': { + val = unescape(val); + if (val.startsWith('<~')) { + // Ascii85 encoded + val = acebase_core_1.ascii85.decode(val); } - else if ((code & 0xe0) === 0xe0) { - // 3 byte char - const b1 = code, b2 = buffer[i + 1], b3 = buffer[i + 2]; - code = ((b1 & 0xf) << 12) | ((b2 & 0x3f) << 6) | (b3 & 0x3f); - i += 2; + else { + // base64 not implemented yet + throw new Error(`Import error: Unexpected encoding for value for value at path "/${path}"`); } - else if ((code & 0xc0) === 0xc0) { - // 2 byte char - const b1 = code, b2 = buffer[i + 1]; - code = ((b1 & 0x1f) << 6) | (b2 & 0x3f); - i++; + break; + } + case 'PathReference': + case 'reference': { + val = new acebase_core_1.PathReference(val); + break; + } + case 'bigint': { + val = BigInt(val); + break; + } + default: + throw new Error(`Import error: Unsupported type "${type}" for value at path "/${path}"`); + } + return val; + }; + const context = { acebase_import_id: acebase_core_1.ID.generate() }; + const childOptions = { suppress_events: options.suppress_events, context }; + /** + * Work in progress (not used yet): queue nodes to store to improve performance + */ + const enqueue = async (target, value) => { + state.queue.push({ target, value }); + if (state.processedBytes >= state.queueStartByte + maxQueueBytes) { + // Flush queue, group queued (set) items as update operations on their parents + const operations = state.queue.reduce((updates, item) => { + // Optimization idea: find all data we know is complete, add that as 1 set if method !== 'merge' + // Example: queue is something like [ + // "users/user1": {}, + // "users/user1/email": "user@example.com" + // "users/user1/addresses": {}, + // "users/user1/addresses/address1": {}, + // "users/user1/addresses/address1/city": "Amsterdam", + // "users/user1/addresses/address2": {}, // We KNOW "users/user1/addresses/address1" is not coming back + // "users/user1/addresses/address2/city": "Berlin", + // "users/user2": {} // <-- We KNOW "users/user1" is not coming back! + //] + if (item.target.path === path) { + // This is the import target. If method is 'set' and this is the first flush, add it as 'set' operation. + // Use 'update' in all other cases + updates.push(Object.assign({ op: options.method === 'set' && state.timesFlushed === 0 ? 'set' : 'update' }, item)); + } + else { + // Find parent to merge with + const parent = updates.find(other => other.target.isParentOf(item.target)); + if (parent) { + parent.value[item.target.key] = item.value; + } + else { + // Parent not found. If method is 'merge', use 'update', otherwise use or 'set' + updates.push(Object.assign({ op: options.method === 'merge' ? 'update' : 'set' }, item)); + } + } + return updates; + }, []); + // Fresh state + state.queueStartByte = state.processedBytes; + state.queue = []; + state.timesFlushed++; + // Execute db updates + } + if (target.path === path) { + // This is the import target. If method === 'set' + } + }; + const importObject = async (target) => { + await consumeToken('{'); + await consumeSpaces(); + const nextChar = await peekBytes(1); + if (nextChar === '}') { + state.index++; + return this.setNode(target.path, {}, childOptions); + } + let childCount = 0; + let obj = {}; + let flushedBefore = false; + const flushObject = async () => { + let p; + if (!flushedBefore) { + flushedBefore = true; + p = this.setNode(target.path, obj, childOptions); + } + else if (Object.keys(obj).length > 0) { + p = this.updateNode(target.path, obj, childOptions); + } + obj = {}; + if (p) { + await p; + } + }; + const promises = []; + while (true) { + await consumeSpaces(); + const property = await readString(); // readPropertyName(); + await consumeSpaces(); + await consumeToken(':'); + await consumeSpaces(); + const { value, type } = await readValue(); + obj[property] = value; + childCount++; + if (['object', 'array'].includes(type)) { + // Flush current imported value before proceeding with object/array child + promises.push(flushObject()); + if (type === 'object') { + // Import child object/array + await importObject(target.child(property)); } else { - throw new Error('invalid utf-8 data'); + await importArray(target.child(property)); } } - if (code >= 65536) { - // Split into 2-part utf-16 char codes - code ^= 0x10000; - const p1 = 0xd800 | (code >> 10); - const p2 = 0xdc00 | (code & 0x3ff); - str += String.fromCharCode(p1); - str += String.fromCharCode(p2); - } - else { - str += String.fromCharCode(code); + // What comes next? End of object ('}') or new property (',')? + await consumeSpaces(); + const nextChar = await peekBytes(1); + if (nextChar === '}') { + // Done importing this object + state.index++; + break; } + // Assume comma now + await consumeToken(','); } - return str; - } - else { - throw new Error('Unsupported buffer argument'); - } - } -} -exports.decodeString = decodeString; -function concatTypedArrays(a, b) { - const c = new a.constructor(a.length + b.length); - c.set(a); - c.set(b, a.length); - return c; -} -exports.concatTypedArrays = concatTypedArrays; -function cloneObject(original, stack) { - var _a; - if (((_a = original === null || original === void 0 ? void 0 : original.constructor) === null || _a === void 0 ? void 0 : _a.name) === '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)) { - // FIX for typed array being converted to objects with numeric properties: - // Convert Buffer or TypedArray to ArrayBuffer - 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 path_reference_1.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 path_reference_1.PathReference || val instanceof RegExp) { // || val instanceof ID - return val; - } - else if (typeof val === 'object') { - stack.push(val); - val = cloneObject(val, stack); - stack.pop(); - return val; - } - else { - return val; // Anything other can just be copied - } - }; - if (typeof stack === 'undefined') { - stack = [original]; - } - const clone = original instanceof Array ? [] : original instanceof partial_array_1.PartialArray ? new partial_array_1.PartialArray() : {}; - Object.keys(original).forEach(key => { - const val = original[key]; - if (typeof val === 'function') { - return; // skip functions - } - clone[key] = cloneValue(val); - }); - return clone; -} -exports.cloneObject = cloneObject; -const isTypedArray = (val) => typeof val === 'object' && ['ArrayBuffer', 'Buffer', 'Uint8Array', 'Uint16Array', 'Uint32Array', 'Int8Array', 'Int16Array', 'Int32Array'].includes(val.constructor.name); -// CONSIDER: updating isTypedArray to: const isTypedArray = val => typeof val === 'object' && 'buffer' in val && 'byteOffset' in val && 'byteLength' in val; -function valuesAreEqual(val1, val2) { - if (val1 === val2) { - return true; - } - if (typeof val1 !== typeof val2) { - return false; - } - if (typeof val1 === 'object' || typeof val2 === 'object') { - if (val1 === null || val2 === null) { - return false; - } - if (val1 instanceof path_reference_1.PathReference || val2 instanceof path_reference_1.PathReference) { - return val1 instanceof path_reference_1.PathReference && val2 instanceof path_reference_1.PathReference && val1.path === val2.path; - } - if (val1 instanceof Date || val2 instanceof Date) { - return val1 instanceof Date && val2 instanceof Date && val1.getTime() === val2.getTime(); - } - if (val1 instanceof Array || val2 instanceof Array) { - return val1 instanceof Array && val2 instanceof Array && val1.length === val2.length && val1.every((item, i) => valuesAreEqual(val1[i], val2[i])); - } - if (isTypedArray(val1) || isTypedArray(val2)) { - if (!isTypedArray(val1) || !isTypedArray(val2) || val1.byteLength === val2.byteLength) { - return false; + const isTypedValue = childCount === 2 && '.type' in obj && '.val' in obj; + if (isTypedValue) { + // This is a value that was exported with type safety. + // Do not store as object, but convert to original value + // Note that this is done regardless of options.type_safe + const val = getTypeSafeValue(target.path, obj); + return this.setNode(target.path, val, childOptions); } - const typed1 = val1 instanceof ArrayBuffer ? new Uint8Array(val1) : new Uint8Array(val1.buffer, val1.byteOffset, val1.byteLength), typed2 = val2 instanceof ArrayBuffer ? new Uint8Array(val2) : new Uint8Array(val2.buffer, val2.byteOffset, val2.byteLength); - return typed1.every((val, i) => typed2[i] === val); - } - const keys1 = Object.keys(val1), keys2 = Object.keys(val2); - return keys1.length === keys2.length && keys1.every(key => keys2.includes(key)) && keys1.every(key => valuesAreEqual(val1[key], val2[key])); - } - return false; -} -exports.valuesAreEqual = valuesAreEqual; -class ObjectDifferences { - constructor(added, removed, changed) { - this.added = added; - this.removed = removed; - this.changed = changed; - } - forChild(key) { - if (this.added.includes(key)) { - return 'added'; - } - if (this.removed.includes(key)) { - return 'removed'; - } - const changed = this.changed.find(ch => ch.key === key); - return changed ? changed.change : 'identical'; - } -} -exports.ObjectDifferences = ObjectDifferences; -function compareValues(oldVal, newVal, sortedResults = false) { - 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 (isTypedArray(oldVal) || isTypedArray(newVal)) { - // One or both values are typed arrays. - if (!isTypedArray(oldVal) || !isTypedArray(newVal)) { - return 'changed'; - } - // Both are typed. Compare lengths and byte content of typed arrays - const typed1 = oldVal instanceof Uint8Array ? oldVal : oldVal instanceof ArrayBuffer ? new Uint8Array(oldVal) : new Uint8Array(oldVal.buffer, oldVal.byteOffset, oldVal.byteLength); - const typed2 = newVal instanceof Uint8Array ? newVal : newVal instanceof ArrayBuffer ? new Uint8Array(newVal) : new Uint8Array(newVal.buffer, newVal.byteOffset, newVal.byteLength); - return typed1.byteLength === typed2.byteLength && typed1.every((val, i) => typed2[i] === val) ? 'identical' : 'changed'; - } - else if (oldVal instanceof Date || newVal instanceof Date) { - return oldVal instanceof Date && newVal instanceof Date && oldVal.getTime() === newVal.getTime() ? 'identical' : 'changed'; - } - else if (oldVal instanceof path_reference_1.PathReference || newVal instanceof path_reference_1.PathReference) { - return oldVal instanceof path_reference_1.PathReference && newVal instanceof path_reference_1.PathReference && oldVal.path === newVal.path ? 'identical' : 'changed'; - } - else if (typeof oldVal === 'object') { - // Do key-by-key comparison of objects - const isArray = oldVal instanceof Array; - const getKeys = (obj) => { - let keys = Object.keys(obj).filter(key => !voids.includes(obj[key])); - if (isArray) { - keys = keys.map((v) => parseInt(v)); + promises.push(flushObject()); + await Promise.all(promises); + }; + const importArray = async (target) => { + await consumeToken('['); + await consumeSpaces(); + const nextChar = await peekBytes(1); + if (nextChar === ']') { + state.index++; + return this.setNode(target.path, [], childOptions); + } + let flushedBefore = false; + let arr = []; + let updates = {}; + const flushArray = async () => { + let p; + if (!flushedBefore) { + // Store array + flushedBefore = true; + p = this.setNode(target.path, arr, childOptions); + arr = null; // GC + } + else if (Object.keys(updates).length > 0) { + // Flush updates + p = this.updateNode(target.path, updates, childOptions); + updates = {}; + } + if (p) { + await p; + } + }; + const pushChild = (value, index) => { + if (flushedBefore) { + updates[index] = value; + } + else { + arr.push(value); + } + }; + const promises = []; + let index = 0; + while (true) { + await consumeSpaces(); + const { value, type } = await readValue(); + pushChild(value, index); + if (['object', 'array'].includes(type)) { + // Flush current imported value before proceeding with object/array child + promises.push(flushArray()); // No need to await now + if (type === 'object') { + // Import child object/array + await importObject(target.child(index)); + } + else { + await importArray(target.child(index)); + } + } + // What comes next? End of array (']') or new property (',')? + await consumeSpaces(); + const nextChar = await peekBytes(1); + if (nextChar === ']') { + // Done importing this array + state.index++; + break; + } + // Assume comma now + await consumeToken(','); + index++; } - return keys; + promises.push(flushArray()); + await Promise.all(promises); }; - const oldKeys = getKeys(oldVal); - const newKeys = getKeys(newVal); - const removedKeys = oldKeys.filter(key => !newKeys.includes(key)); - const addedKeys = newKeys.filter(key => !oldKeys.includes(key)); - const changedKeys = newKeys.reduce((changed, key) => { - if (oldKeys.includes(key)) { - const val1 = oldVal[key]; - const val2 = newVal[key]; - const c = compareValues(val1, val2); - if (c !== 'identical') { - changed.push({ key, change: c }); + const start = async () => { + const { value, type } = await readValue(); + if (['object', 'array'].includes(type)) { + // Object or array value, has not been read yet + const target = acebase_core_1.PathInfo.get(path); + if (type === 'object') { + await importObject(target); + } + else { + await importArray(target); } } - return changed; - }, []); - if (addedKeys.length === 0 && removedKeys.length === 0 && changedKeys.length === 0) { - return 'identical'; + else { + // Simple value + await this.setNode(path, value, childOptions); + } + }; + return start(); + } + /** + * Adds, updates or removes a schema definition to validate node values before they are stored at the specified path + * @param path target path to enforce the schema on, can include wildcards. Eg: 'users/*\/posts/*' or 'users/$uid/posts/$postid' + * @param schema schema type definitions. When null value is passed, a previously set schema is removed. + */ + setSchema(path, schema, warnOnly = false) { + if (typeof schema === 'undefined') { + throw new TypeError('schema argument must be given'); } - else { - return new ObjectDifferences(addedKeys, removedKeys, sortedResults ? changedKeys.sort((a, b) => a.key < b.key ? -1 : 1) : changedKeys); + if (schema === null) { + // Remove previously set schema on path + const i = this._schemas.findIndex(s => s.path === path); + i >= 0 && this._schemas.splice(i, 1); + return; } - } - return 'changed'; -} -exports.compareValues = compareValues; -function getMutations(oldVal, newVal, sortedResults = false) { - const process = (target, compareResult, prev, val) => { - switch (compareResult) { - case 'identical': return []; - case 'changed': return [{ target, prev, val }]; - case 'added': return [{ target, prev: null, val }]; - case 'removed': return [{ target, prev, val: null }]; - default: { - let changes = []; - compareResult.added.forEach(key => changes.push({ target: target.concat(key), prev: null, val: val[key] })); - compareResult.removed.forEach(key => changes.push({ target: target.concat(key), prev: prev[key], val: null })); - compareResult.changed.forEach(item => { - const childChanges = process(target.concat(item.key), item.change, prev[item.key], val[item.key]); - changes = changes.concat(childChanges); - }); - return changes; - } + // Parse schema, add or update it + const definition = new acebase_core_1.SchemaDefinition(schema, { + warnOnly, + warnCallback: (message) => this.logger.warn(message), + }); + const item = this._schemas.find(s => s.path === path); + if (item) { + item.schema = definition; + } + else { + this._schemas.push({ path, schema: definition }); + this._schemas.sort((a, b) => { + const ka = acebase_core_1.PathInfo.getPathKeys(a.path), kb = acebase_core_1.PathInfo.getPathKeys(b.path); + if (ka.length === kb.length) { + return 0; + } + return ka.length < kb.length ? -1 : 1; + }); } - }; - const compareResult = compareValues(oldVal, newVal, sortedResults); - return process([], compareResult, oldVal, newVal); -} -exports.getMutations = getMutations; -function 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, newValue }; -} -exports.getChildValues = getChildValues; -function defer(fn) { - process_1.default.nextTick(fn); -} -exports.defer = defer; -function getGlobalObject() { - var _a; - if (typeof globalThis !== 'undefined') { - return globalThis; } - if (typeof global !== 'undefined') { - return global; + /** + * Gets currently active schema definition for the specified path + */ + getSchema(path) { + const item = this._schemas.find(item => item.path === path); + return item ? { path, schema: item.schema.source, text: item.schema.text } : null; } - if (typeof window !== 'undefined') { - return window; + /** + * Gets all currently active schema definitions + */ + getSchemas() { + return this._schemas.map(item => ({ path: item.path, schema: item.schema.source, text: item.schema.text })); } - if (typeof self !== 'undefined') { - return self; + /** + * Validates the schemas of the node being updated and its children + * @param path path being written to + * @param value the new value, or updates to current value + * @example + * // define schema for each tag of each user post: + * db.schema.set( + * 'users/$uid/posts/$postId/tags/$tagId', + * { name: 'string', 'link_id?': 'number' } + * ); + * + * // Insert that will fail: + * db.ref('users/352352/posts/572245').set({ + * text: 'this is my post', + * tags: { sometag: 'deny this' } // <-- sometag must be typeof object + * }); + * + * // Insert that will fail: + * db.ref('users/352352/posts/572245').set({ + * text: 'this is my post', + * tags: { + * tag1: { name: 'firstpost', link_id: 234 }, + * tag2: { name: 'newbie' }, + * tag3: { title: 'Not allowed' } // <-- title property not allowed + * } + * }); + * + * // Update that fails if post does not exist: + * db.ref('users/352352/posts/572245/tags/tag1').update({ + * name: 'firstpost' + * }); // <-- post is missing property text + */ + validateSchema(path, value, options = { updates: false }) { + let result = { ok: true }; + const pathInfo = acebase_core_1.PathInfo.get(path); + this._schemas.filter(s => pathInfo.isOnTrailOf(s.path)).every(s => { + if (pathInfo.isDescendantOf(s.path)) { + // Given check path is a descendant of this schema definition's path + const ancestorPath = acebase_core_1.PathInfo.fillVariables(s.path, path); + const trailKeys = pathInfo.keys.slice(acebase_core_1.PathInfo.getPathKeys(s.path).length); + result = s.schema.check(ancestorPath, value, options.updates, trailKeys); + return result.ok; + } + // Given check path is on schema definition's path or on a higher path + const trailKeys = acebase_core_1.PathInfo.getPathKeys(s.path).slice(pathInfo.keys.length); + if (options.updates === true && trailKeys.length > 0 && !(trailKeys[0] in value)) { + // Fixes #217: this update on a higher path does not affect any data at schema's target path + return result.ok; + } + const partial = options.updates === true && trailKeys.length === 0; + const check = (path, value, trailKeys) => { + if (trailKeys.length === 0) { + // Check this node + return s.schema.check(path, value, partial); + } + else if (value === null) { + return { ok: true }; // Not at the end of trail, but nothing more to check + } + const key = trailKeys[0]; + if (typeof key === 'string' && (key === '*' || key[0] === '$')) { + // Wildcard. Check each key in value recursively + if (value === null || typeof value !== 'object') { + // Can't check children, because there are none. This is + // possible if another rule permits the value at current path + // to be something else than an object. + return { ok: true }; + } + let result; + Object.keys(value).every(childKey => { + const childPath = acebase_core_1.PathInfo.getChildPath(path, childKey); + const childValue = value[childKey]; + result = check(childPath, childValue, trailKeys.slice(1)); + return result.ok; + }); + return result; + } + else { + const childPath = acebase_core_1.PathInfo.getChildPath(path, key); + const childValue = value[key]; + return check(childPath, childValue, trailKeys.slice(1)); + } + }; + result = check(path, value, trailKeys); + return result.ok; + }); + return result; } - return (_a = (function () { return this; }())) !== null && _a !== void 0 ? _a : Function('return this')(); } -exports.getGlobalObject = getGlobalObject; +exports.Storage = Storage; -}).call(this)}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {},require("buffer").Buffer) -},{"./partial-array":49,"./path-reference":51,"./process":52,"buffer":62}],62:[function(require,module,exports){ +},{"../assert.js":31,"../data-index/index.js":34,"../ipc/index.js":35,"../node-errors.js":38,"../node-info.js":39,"../node-value-types.js":41,"../promise-fs/index.js":43,"./errors.js":55,"./indexes.js":57,"acebase-core":12}],62:[function(require,module,exports){ -},{}]},{},[6])(6) +},{}]},{},[33])(33) }); diff --git a/dist/browser.min.js b/dist/browser.min.js index 80a1304..905c288 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{await this.ready();if(this.api.storage instanceof binary_1.AceBaseStorage){await this.api.storage.repairNode(path,options)}else if(!this.api.storage.repairNode){throw new Error(`repairNode is not supported with chosen storage engine`)}},repairNodeTree:async path=>{await this.ready();const storage=this.api.storage;await storage.repairNodeTree(path)}};const apiSettings={db:this,settings:settings};this.api=new api_local_1.LocalApi(dbname,apiSettings,(()=>{this.emit("ready")}))}async close(){await this.api.storage.close()}get settings(){const ipc=this.api.storage.ipc,debug=this.debug;return{get logLevel(){return debug.level},set logLevel(level){debug.setLevel(level)},get ipcEvents(){return ipc.eventsEnabled},set ipcEvents(enabled){ipc.eventsEnabled=enabled}}}static WithLocalStorage(dbname,settings={}){const db=(0,local_storage_1.createLocalStorageInstance)(dbname,settings);return db}static WithIndexedDB(dbname,init={}){throw new Error(`IndexedDB storage can only be used in browser contexts`)}}exports.AceBase=AceBase},{"./api-local":3,"./storage/binary":18,"./storage/custom/indexed-db/settings":23,"./storage/custom/local-storage":25,"acebase-core":46}],3:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.LocalApi=void 0;const acebase_core_1=require("acebase-core");const binary_1=require("./storage/binary");const sqlite_1=require("./storage/sqlite");const mssql_1=require("./storage/mssql");const custom_1=require("./storage/custom");const node_value_types_1=require("./node-value-types");const query_1=require("./query");const node_errors_1=require("./node-errors");class LocalApi extends acebase_core_1.Api{constructor(dbname="default",init,readyCallback){super();this.db=init.db;this.logger=init.db.logger;const storageEnv={logLevel:init.settings.logLevel,logColors:init.settings.logColors,logger:init.settings.logger};if(typeof init.settings.storage==="object"){if(sqlite_1.SQLiteStorageSettings&&init.settings.storage instanceof sqlite_1.SQLiteStorageSettings){this.storage=new sqlite_1.SQLiteStorage(dbname,init.settings.storage,storageEnv)}else if(mssql_1.MSSQLStorageSettings&&init.settings.storage instanceof mssql_1.MSSQLStorageSettings){this.storage=new mssql_1.MSSQLStorage(dbname,init.settings.storage,storageEnv)}else if(custom_1.CustomStorageSettings&&init.settings.storage instanceof custom_1.CustomStorageSettings){this.storage=new custom_1.CustomStorage(dbname,init.settings.storage,storageEnv)}else{const storageSettings=init.settings.storage instanceof binary_1.AceBaseStorageSettings?init.settings.storage:new binary_1.AceBaseStorageSettings(init.settings.storage);this.storage=new binary_1.AceBaseStorage(dbname,storageSettings,storageEnv)}}else{this.storage=new binary_1.AceBaseStorage(dbname,new binary_1.AceBaseStorageSettings,storageEnv)}this.storage.on("ready",readyCallback)}async stats(options){return this.storage.stats}subscribe(path,event,callback){this.storage.subscriptions.add(path,event,callback)}unsubscribe(path,event,callback){this.storage.subscriptions.remove(path,event,callback)}async set(path,value,options={suppress_events:false,context:null}){const cursor=await this.storage.setNode(path,value,{suppress_events:options.suppress_events,context:options.context});return Object.assign({},cursor&&{cursor:cursor})}async update(path,updates,options={suppress_events:false,context:null}){const cursor=await this.storage.updateNode(path,updates,{suppress_events:options.suppress_events,context:options.context});return Object.assign({},cursor&&{cursor:cursor})}get transactionLoggingEnabled(){return this.storage.settings.transactions&&this.storage.settings.transactions.log===true}async get(path,options){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`)}const node=await this.storage.getNode(path,options);return{value:node.value,context:{acebase_cursor:node.cursor},cursor:node.cursor}}async transaction(path,callback,options={suppress_events:false,context:null}){const cursor=await this.storage.transactNode(path,callback,{suppress_events:options.suppress_events,context:options.context});return Object.assign({},cursor&&{cursor:cursor})}async exists(path){const nodeInfo=await this.storage.getNodeInfo(path);return nodeInfo.exists}async query(path,query,options={snapshots:false}){const results=await(0,query_1.executeQuery)(this,path,query,options);return results}createIndex(path,key,options){return this.storage.indexes.create(path,key,options)}async getIndexes(){return this.storage.indexes.list()}async deleteIndex(filePath){return this.storage.indexes.delete(filePath)}async reflect(path,type,args){args=args||{};const getChildren=async(path,limit=50,skip=0,from=null)=>{if(typeof limit==="string"){limit=parseInt(limit)}if(typeof skip==="string"){skip=parseInt(skip)}const children=[];let n=0,stop=false,more=false;await this.storage.getChildren(path,Object.assign({},["number","string"].includes(typeof from)&&{fromKey:from})).next((childInfo=>{if(stop){more=true;return false}n++;const include=skip===0||n>skip;if(include){children.push(Object.assign({key:typeof childInfo.key==="string"?childInfo.key:childInfo.index,type:childInfo.valueTypeName,value:childInfo.value},typeof childInfo.address==="object"&&"pageNr"in childInfo.address&&{address:{pageNr:childInfo.address.pageNr,recordNr:childInfo.address.recordNr}}))}stop=limit>0&&children.length===limit})).catch((err=>{if(!(err instanceof node_errors_1.NodeNotFoundError)){throw err}}));return{more:more,list:children}};switch(type){case"children":{const result=await getChildren(path,args.limit,args.skip,args.from);return result}case"info":{const info={key:"",exists:false,type:"unknown",value:undefined,address:undefined,children:{count:0,more:false,list:[]}};const nodeInfo=await this.storage.getNodeInfo(path,{include_child_count:args.child_count===true});info.key=typeof nodeInfo.key!=="undefined"?nodeInfo.key:nodeInfo.index;info.exists=nodeInfo.exists;info.type=nodeInfo.exists?nodeInfo.valueTypeName:undefined;info.value=nodeInfo.value;info.address=typeof nodeInfo.address==="object"&&"pageNr"in nodeInfo.address?{pageNr:nodeInfo.address.pageNr,recordNr:nodeInfo.address.recordNr}:undefined;const isObjectOrArray=nodeInfo.exists&&nodeInfo.address&&[node_value_types_1.VALUE_TYPES.OBJECT,node_value_types_1.VALUE_TYPES.ARRAY].includes(nodeInfo.type);if(args.child_count===true){info.children={count:isObjectOrArray?nodeInfo.childCount:0}}else if(typeof args.child_limit==="number"&&args.child_limit>0){if(isObjectOrArray){info.children=await getChildren(path,args.child_limit,args.child_skip,args.child_from)}}return info}}}export(path,stream,options={format:"json",type_safe:true}){return this.storage.exportNode(path,stream,options)}import(path,read,options={format:"json",suppress_events:false,method:"set"}){return this.storage.importNode(path,read,options)}async setSchema(path,schema,warnOnly=false){return this.storage.setSchema(path,schema,warnOnly)}async getSchema(path){return this.storage.getSchema(path)}async getSchemas(){return this.storage.getSchemas()}async validateSchema(path,value,isUpdate){return this.storage.validateSchema(path,value,{updates:isUpdate})}async getMutations(filter){if(typeof this.storage.getMutations!=="function"){throw new Error("Used storage type does not support getMutations")}if(typeof filter!=="object"){throw new Error("No filter specified")}if(typeof filter.cursor!=="string"&&typeof filter.timestamp!=="number"){throw new Error("No cursor or timestamp given")}return this.storage.getMutations(filter)}async getChanges(filter){if(typeof this.storage.getChanges!=="function"){throw new Error("Used storage type does not support getChanges")}if(typeof filter!=="object"){throw new Error("No filter specified")}if(typeof filter.cursor!=="string"&&typeof filter.timestamp!=="number"){throw new Error("No cursor or timestamp given")}return this.storage.getChanges(filter)}}exports.LocalApi=LocalApi},{"./node-errors":11,"./node-value-types":14,"./query":17,"./storage/binary":18,"./storage/custom":21,"./storage/mssql":31,"./storage/sqlite":32,"acebase-core":46}],4:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.assert=void 0;function assert(condition,error){if(!condition){throw new Error(`Assertion failed: ${error!==null&&error!==void 0?error:"check your code"}`)}}exports.assert=assert},{}],5:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.AsyncTaskBatch=void 0;class AsyncTaskBatch{constructor(limit=1e3,options){this.limit=limit;this.options=options;this.added=0;this.scheduled=[];this.running=0;this.results=[];this.done=false}async execute(task,index){var _a,_b;try{this.running++;const result=await task();this.results[index]=result;this.running--;if(this.running===0&&this.scheduled.length===0){this.done=true;(_a=this.doneCallback)===null||_a===void 0?void 0:_a.call(this,this.results)}else if(this.scheduled.length>0){const next=this.scheduled.shift();this.execute(next.task,next.index)}}catch(err){this.done=true;(_b=this.errorCallback)===null||_b===void 0?void 0:_b.call(this,err)}}add(task){var _a;if(this.done){throw new Error(`Cannot add to a batch that has already finished. Use wait option and start batch processing manually if you are adding tasks in an async loop`)}const index=this.added++;if(((_a=this.options)===null||_a===void 0?void 0:_a.wait)!==true&&this.running{this.doneCallback=resolve;this.errorCallback=reject}));return this.results}}exports.AsyncTaskBatch=AsyncTaskBatch},{}],6:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.SchemaValidationError=exports.StorageSettings=exports.ICustomStorageNodeMetaData=exports.ICustomStorageNode=exports.CustomStorageHelpers=exports.CustomStorageSettings=exports.CustomStorageTransaction=exports.MSSQLStorageSettings=exports.SQLiteStorageSettings=exports.AceBaseStorageSettings=exports.IndexedDBStorageSettings=exports.LocalStorageSettings=exports.AceBaseLocalSettings=exports.AceBase=exports.PartialArray=exports.proxyAccess=exports.ID=exports.ObjectCollection=exports.TypeMappings=exports.PathReference=exports.EventSubscription=exports.EventStream=exports.DataReferencesArray=exports.DataSnapshotsArray=exports.DataReference=exports.DataSnapshot=void 0;const acebase_core_1=require("acebase-core");Object.defineProperty(exports,"DataReference",{enumerable:true,get:function(){return acebase_core_1.DataReference}});Object.defineProperty(exports,"DataSnapshot",{enumerable:true,get:function(){return acebase_core_1.DataSnapshot}});Object.defineProperty(exports,"EventSubscription",{enumerable:true,get:function(){return acebase_core_1.EventSubscription}});Object.defineProperty(exports,"PathReference",{enumerable:true,get:function(){return acebase_core_1.PathReference}});Object.defineProperty(exports,"TypeMappings",{enumerable:true,get:function(){return acebase_core_1.TypeMappings}});Object.defineProperty(exports,"ID",{enumerable:true,get:function(){return acebase_core_1.ID}});Object.defineProperty(exports,"proxyAccess",{enumerable:true,get:function(){return acebase_core_1.proxyAccess}});Object.defineProperty(exports,"DataSnapshotsArray",{enumerable:true,get:function(){return acebase_core_1.DataSnapshotsArray}});Object.defineProperty(exports,"ObjectCollection",{enumerable:true,get:function(){return acebase_core_1.ObjectCollection}});Object.defineProperty(exports,"DataReferencesArray",{enumerable:true,get:function(){return acebase_core_1.DataReferencesArray}});Object.defineProperty(exports,"EventStream",{enumerable:true,get:function(){return acebase_core_1.EventStream}});Object.defineProperty(exports,"PartialArray",{enumerable:true,get:function(){return acebase_core_1.PartialArray}});const acebase_local_1=require("./acebase-local");const acebase_browser_1=require("./acebase-browser");Object.defineProperty(exports,"AceBase",{enumerable:true,get:function(){return acebase_browser_1.BrowserAceBase}});const custom_1=require("./storage/custom");const acebase={AceBase:acebase_browser_1.BrowserAceBase,AceBaseLocalSettings:acebase_local_1.AceBaseLocalSettings,DataReference:acebase_core_1.DataReference,DataSnapshot:acebase_core_1.DataSnapshot,EventSubscription:acebase_core_1.EventSubscription,PathReference:acebase_core_1.PathReference,TypeMappings:acebase_core_1.TypeMappings,CustomStorageSettings:custom_1.CustomStorageSettings,CustomStorageTransaction:custom_1.CustomStorageTransaction,CustomStorageHelpers:custom_1.CustomStorageHelpers,ID:acebase_core_1.ID,proxyAccess:acebase_core_1.proxyAccess,DataSnapshotsArray:acebase_core_1.DataSnapshotsArray};if(typeof window!=="undefined"){window.acebase=acebase;window.AceBase=acebase_browser_1.BrowserAceBase}exports.default=acebase;var acebase_local_2=require("./acebase-local");Object.defineProperty(exports,"AceBaseLocalSettings",{enumerable:true,get:function(){return acebase_local_2.AceBaseLocalSettings}});Object.defineProperty(exports,"LocalStorageSettings",{enumerable:true,get:function(){return acebase_local_2.LocalStorageSettings}});Object.defineProperty(exports,"IndexedDBStorageSettings",{enumerable:true,get:function(){return acebase_local_2.IndexedDBStorageSettings}});var binary_1=require("./storage/binary");Object.defineProperty(exports,"AceBaseStorageSettings",{enumerable:true,get:function(){return binary_1.AceBaseStorageSettings}});var sqlite_1=require("./storage/sqlite");Object.defineProperty(exports,"SQLiteStorageSettings",{enumerable:true,get:function(){return sqlite_1.SQLiteStorageSettings}});var mssql_1=require("./storage/mssql");Object.defineProperty(exports,"MSSQLStorageSettings",{enumerable:true,get:function(){return mssql_1.MSSQLStorageSettings}});var custom_2=require("./storage/custom");Object.defineProperty(exports,"CustomStorageTransaction",{enumerable:true,get:function(){return custom_2.CustomStorageTransaction}});Object.defineProperty(exports,"CustomStorageSettings",{enumerable:true,get:function(){return custom_2.CustomStorageSettings}});Object.defineProperty(exports,"CustomStorageHelpers",{enumerable:true,get:function(){return custom_2.CustomStorageHelpers}});Object.defineProperty(exports,"ICustomStorageNode",{enumerable:true,get:function(){return custom_2.ICustomStorageNode}});Object.defineProperty(exports,"ICustomStorageNodeMetaData",{enumerable:true,get:function(){return custom_2.ICustomStorageNodeMetaData}});var storage_1=require("./storage");Object.defineProperty(exports,"StorageSettings",{enumerable:true,get:function(){return storage_1.StorageSettings}});Object.defineProperty(exports,"SchemaValidationError",{enumerable:true,get:function(){return storage_1.SchemaValidationError}})},{"./acebase-browser":1,"./acebase-local":2,"./storage":29,"./storage/binary":18,"./storage/custom":21,"./storage/mssql":31,"./storage/sqlite":32,"acebase-core":46}],7:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.ArrayIndex=exports.GeoIndex=exports.FullTextIndex=exports.DataIndex=void 0;const not_supported_1=require("../not-supported");class DataIndex extends not_supported_1.NotSupported{}exports.DataIndex=DataIndex;class FullTextIndex extends not_supported_1.NotSupported{}exports.FullTextIndex=FullTextIndex;class GeoIndex extends not_supported_1.NotSupported{}exports.GeoIndex=GeoIndex;class ArrayIndex extends not_supported_1.NotSupported{}exports.ArrayIndex=ArrayIndex},{"../not-supported":15}],8:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.NetIPCServer=exports.IPCSocketPeer=exports.RemoteIPCPeer=exports.IPCPeer=void 0;const acebase_core_1=require("acebase-core");const ipc_1=require("./ipc");const not_supported_1=require("../not-supported");Object.defineProperty(exports,"RemoteIPCPeer",{enumerable:true,get:function(){return not_supported_1.NotSupported}});Object.defineProperty(exports,"IPCSocketPeer",{enumerable:true,get:function(){return not_supported_1.NotSupported}});Object.defineProperty(exports,"NetIPCServer",{enumerable:true,get:function(){return not_supported_1.NotSupported}});class IPCPeer extends ipc_1.AceBaseIPCPeer{constructor(storage){super(storage,acebase_core_1.ID.generate());this.masterPeerId=this.id;this.ipcType="browser.bcc";addEventListener("beforeunload",(()=>{this.exit()}));if(typeof BroadcastChannel!=="undefined"){this.channel=new BroadcastChannel(`acebase:${storage.name}`)}else if(typeof localStorage!=="undefined"){const listeners=[null];const notImplemented=()=>{throw new Error("Not implemented")};this.channel={name:`acebase:${storage.name}`,postMessage:message=>{const messageId=acebase_core_1.ID.generate(),key=`acebase:${storage.name}:${this.id}:${messageId}`,payload=JSON.stringify(acebase_core_1.Transport.serialize(message));localStorage.setItem(key,payload);setTimeout((()=>localStorage.removeItem(key)),10)},set onmessage(handler){listeners[0]=handler},set onmessageerror(handler){notImplemented()},close(){notImplemented()},addEventListener(event,callback){if(event!=="message"){notImplemented()}listeners.push(callback)},removeEventListener(event,callback){const i=listeners.indexOf(callback);i>=1&&listeners.splice(i,1)},dispatchEvent(event){listeners.forEach((callback=>{try{callback&&callback(event)}catch(err){console.error(err)}}));return true}};addEventListener("storage",(event=>{const[acebase,dbname,peerId,messageId]=event.key.split(":");if(acebase!=="acebase"||dbname!==storage.name||peerId===this.id||event.newValue===null){return}const message=acebase_core_1.Transport.deserialize(JSON.parse(event.newValue));this.channel.dispatchEvent({data:message})}))}else{this.logger.warn(`[BroadcastChannel] not supported`);this.sendMessage=()=>{};return}this.channel.addEventListener("message",(async event=>{const message=event.data;if(message.to&&message.to!==this.id){return}this.logger.trace(`[BroadcastChannel] received: `,message);if(message.type==="hello"&&message.frompeer.id)).concat(this.id).filter((id=>id!==this.masterPeerId));this.masterPeerId=allPeerIds.sort()[0];this.logger.info(`[BroadcastChannel] ${this.masterPeerId===this.id?"We are":`tab ${this.masterPeerId} is`} the new master. Requesting ${this._locks.length} locks (${this._locks.filter((r=>!r.granted)).length} pending)`);const requests=this._locks.splice(0);await Promise.all(requests.filter((req=>req.granted)).map((async req=>{let released,movedToParent;req.lock.release=()=>new Promise((resolve=>released=resolve));req.lock.moveToParent=()=>new Promise((resolve=>movedToParent=resolve));const lock=await this.lock({path:req.lock.path,write:req.lock.forWriting,tid:req.lock.tid,comment:req.lock.comment});if(movedToParent){const newLock=await lock.moveToParent();movedToParent(newLock)}if(released){await lock.release();released()}})));await Promise.all(requests.filter((req=>!req.granted)).map((async req=>{await this.lock(req.request)})))}return this.handleMessage(message)}));const helloMsg={type:"hello",from:this.id,data:undefined};this.sendMessage(helloMsg)}sendMessage(message){this.logger.trace(`[BroadcastChannel] sending: `,message);this.channel.postMessage(message)}}exports.IPCPeer=IPCPeer},{"../not-supported":15,"./ipc":9,"acebase-core":46}],9:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.AceBaseIPCPeer=exports.AceBaseIPCPeerExitingError=void 0;const acebase_core_1=require("acebase-core");const node_lock_1=require("../node-lock");class AceBaseIPCPeerExitingError extends Error{constructor(message){super(`Exiting: ${message}`)}}exports.AceBaseIPCPeerExitingError=AceBaseIPCPeerExitingError;class AceBaseIPCPeer extends acebase_core_1.SimpleEventEmitter{get isMaster(){return this.masterPeerId===this.id}constructor(storage,id,dbname=storage.name){super();this.storage=storage;this.id=id;this.dbname=dbname;this.ipcType="ipc";this.ourSubscriptions=[];this.remoteSubscriptions=[];this.peers=[];this._exiting=false;this._locks=[];this._requests=new Map;this._eventsEnabled=true;this._nodeLocker=new node_lock_1.NodeLocker(storage.logger,storage.settings.lockTimeout);this.logger=storage.logger;storage.on("subscribe",(subscription=>{this.logger.trace(`database subscription being added on peer ${this.id}`);const remoteSubscription=this.remoteSubscriptions.find((sub=>sub.callback===subscription.callback));if(remoteSubscription){return}const othersAlreadyNotifying=this.ourSubscriptions.some((sub=>sub.event===subscription.event&&sub.path===subscription.path));this.ourSubscriptions.push(subscription);if(othersAlreadyNotifying){return}const message={type:"subscribe",from:this.id,data:{path:subscription.path,event:subscription.event}};this.sendMessage(message)}));storage.on("unsubscribe",(subscription=>{const remoteSubscription=this.remoteSubscriptions.find((sub=>sub.callback===subscription.callback));if(remoteSubscription){this.remoteSubscriptions.splice(this.remoteSubscriptions.indexOf(remoteSubscription),1);return}this.ourSubscriptions.filter((sub=>sub.path===subscription.path&&(!subscription.event||sub.event===subscription.event)&&(!subscription.callback||sub.callback===subscription.callback))).forEach((sub=>{this.ourSubscriptions.splice(this.ourSubscriptions.indexOf(sub),1);const message={type:"unsubscribe",from:this.id,data:{path:sub.path,event:sub.event}};this.sendMessage(message)}))}))}async exit(code=0){if(this._exiting){return this.once("exit")}this._exiting=true;this.logger.warn(`Received ${this.isMaster?"master":"worker "+this.id} process exit request`);if(this._locks.length>0){this.logger.warn(`Waiting for ${this.isMaster?"master":"worker"} ${this.id} locks to clear`);await this.once("locks-cleared")}this.sayGoodbye(this.id);this.logger.warn(`${this.isMaster?"Master":"Worker "+this.id} will now exit`);this.emitOnce("exit",code)}sayGoodbye(forPeerId){const bye={type:"bye",from:forPeerId,data:undefined};this.sendMessage(bye)}addPeer(id,sendReply=true){if(this._exiting){return}const peer=this.peers.find((w=>w.id===id));if(!peer){this.peers.push({id:id,lastSeen:Date.now()})}if(sendReply){const helloMessage={type:"hello",from:this.id,to:id,data:undefined};this.sendMessage(helloMessage);this.ourSubscriptions.forEach((sub=>{const message={type:"subscribe",from:this.id,to:id,data:{path:sub.path,event:sub.event}};this.sendMessage(message)}))}}removePeer(id,ignoreUnknown=false){if(this._exiting){return}const peer=this.peers.find((peer=>peer.id===id));if(!peer){if(!ignoreUnknown){throw new Error(`We are supposed to know this peer!`)}return}this.peers.splice(this.peers.indexOf(peer),1);const subscriptions=this.remoteSubscriptions.filter((sub=>sub.for===id));subscriptions.forEach((sub=>{this.remoteSubscriptions.splice(this.remoteSubscriptions.indexOf(sub),1);this.storage.subscriptions.remove(sub.path,sub.event,sub.callback)}))}addRemoteSubscription(peerId,details){if(this._exiting){return}if(this.remoteSubscriptions.some((sub=>sub.for===peerId&&sub.event===details.event&&sub.path===details.path))){return}const subscribeCallback=(err,path,val,previous,context)=>{const eventMessage={type:"event",from:this.id,to:peerId,path:details.path,event:details.event,data:{path:path,val:val,previous:previous,context:context}};this.sendMessage(eventMessage)};this.remoteSubscriptions.push({for:peerId,event:details.event,path:details.path,callback:subscribeCallback});this.storage.subscriptions.add(details.path,details.event,subscribeCallback)}cancelRemoteSubscription(peerId,details){const sub=this.remoteSubscriptions.find((sub=>sub.for===peerId&&sub.event===details.event&&sub.path===details.event));if(!sub){return}this.storage.subscriptions.remove(details.path,details.event,sub.callback)}async handleMessage(message){switch(message.type){case"hello":return this.addPeer(message.from,message.to!==this.id);case"bye":return this.removePeer(message.from,true);case"subscribe":return this.addRemoteSubscription(message.from,message.data);case"unsubscribe":return this.cancelRemoteSubscription(message.from,message.data);case"event":{if(!this._eventsEnabled){break}const eventMessage=message;const context=eventMessage.data.context||{};context.acebase_ipc={type:this.ipcType,origin:eventMessage.from};const subscriptions=this.ourSubscriptions.filter((sub=>sub.event===eventMessage.event&&sub.path===eventMessage.path));subscriptions.forEach((sub=>{sub.callback(null,eventMessage.data.path,eventMessage.data.val,eventMessage.data.previous,context)}));break}case"lock-request":{if(!this.isMaster){throw new Error(`Workers are not supposed to receive lock requests!`)}const request=message;const result={type:"lock-result",id:request.id,from:this.id,to:request.from,ok:true,data:undefined};try{const lock=await this.lock(request.data);result.data={id:lock.id,path:lock.path,tid:lock.tid,write:lock.forWriting,expires:lock.expires,comment:lock.comment}}catch(err){result.ok=false;result.reason=err.stack||err.message||err}return this.sendMessage(result)}case"lock-result":{if(this.isMaster){throw new Error(`Masters are not supposed to receive results for lock requests!`)}const result=message;const request=this._requests.get(result.id);if(typeof request!=="object"){throw new Error(`The request must be known to us!`)}if(result.ok){request.resolve(result.data)}else{request.reject(new Error(result.reason))}return}case"unlock-request":{if(!this.isMaster){throw new Error(`Workers are not supposed to receive unlock requests!`)}const request=message;const result={type:"unlock-result",id:request.id,from:this.id,to:request.from,ok:true,data:{id:request.data.id}};try{const lockInfo=this._locks.find((l=>{var _a;return((_a=l.lock)===null||_a===void 0?void 0:_a.id)===request.data.id}));await lockInfo.lock.release()}catch(err){result.ok=false;result.reason=err.stack||err.message||err}return this.sendMessage(result)}case"unlock-result":{if(this.isMaster){throw new Error(`Masters are not supposed to receive results for unlock requests!`)}const result=message;const request=this._requests.get(result.id);if(typeof request!=="object"){throw new Error(`The request must be known to us!`)}if(result.ok){request.resolve(result.data)}else{request.reject(new Error(result.reason))}return}case"move-lock-request":{if(!this.isMaster){throw new Error(`Workers are not supposed to receive move lock requests!`)}const request=message;const result={type:"lock-result",id:request.id,from:this.id,to:request.from,ok:true,data:undefined};try{let movedLock;const lockRequest=this._locks.find((r=>{var _a;return((_a=r.lock)===null||_a===void 0?void 0:_a.id)===request.data.id}));if(request.data.move_to==="parent"){movedLock=await lockRequest.lock.moveToParent()}else{throw new Error(`Unknown lock move_to "${request.data.move_to}"`)}lockRequest.lock=movedLock;result.data={id:movedLock.id,path:movedLock.path,tid:movedLock.tid,write:movedLock.forWriting,expires:movedLock.expires,comment:movedLock.comment}}catch(err){result.ok=false;result.reason=err.stack||err.message||err}return this.sendMessage(result)}case"notification":{return this.emit("notification",message)}case"request":{return this.emit("request",message)}case"result":{const result=message;const request=this._requests.get(result.id);if(typeof request!=="object"){throw new Error(`Result of unknown request received`)}if(result.ok){request.resolve(result.data)}else{request.reject(new Error(result.reason))}}}}async lock(details){if(this._exiting){const tidApproved=this._locks.find((l=>l.tid===details.tid&&l.granted));if(!tidApproved){throw new AceBaseIPCPeerExitingError("new transaction lock denied because the IPC peer is exiting")}}const removeLock=lockDetails=>{this._locks.splice(this._locks.indexOf(lockDetails),1);if(this._locks.length===0){this.emit("locks-cleared")}};if(this.isMaster){const lockInfo={tid:details.tid,granted:false,request:details,lock:null};this._locks.push(lockInfo);const lock=await this._nodeLocker.lock(details.path,details.tid,details.write,details.comment);lockInfo.tid=lock.tid;lockInfo.granted=true;const createIPCLock=lock=>({get id(){return lock.id},get tid(){return lock.tid},get path(){return lock.path},get forWriting(){return lock.forWriting},get expires(){return lock.expires},get comment(){return lock.comment},get state(){return lock.state},release:async()=>{await lock.release();removeLock(lockInfo)},moveToParent:async()=>{const parentLock=await lock.moveToParent();lockInfo.lock=createIPCLock(parentLock);return lockInfo.lock}});lockInfo.lock=createIPCLock(lock);return lockInfo.lock}else{const lockInfo={tid:details.tid,granted:false,request:details,lock:null};this._locks.push(lockInfo);const createIPCLock=result=>{lockInfo.granted=true;lockInfo.tid=result.tid;lockInfo.lock={id:result.id,tid:result.tid,path:result.path,forWriting:result.write,state:node_lock_1.LOCK_STATE.LOCKED,expires:result.expires,comment:result.comment,release:async()=>{const req={type:"unlock-request",id:acebase_core_1.ID.generate(),from:this.id,to:this.masterPeerId,data:{id:lockInfo.lock.id}};await this.request(req);lockInfo.lock.state=node_lock_1.LOCK_STATE.DONE;this.logger.trace(`Worker ${this.id} released lock ${lockInfo.lock.id} (tid ${lockInfo.lock.tid}, ${lockInfo.lock.comment}, "/${lockInfo.lock.path}", ${lockInfo.lock.forWriting?"write":"read"})`);removeLock(lockInfo)},moveToParent:async()=>{const req={type:"move-lock-request",id:acebase_core_1.ID.generate(),from:this.id,to:this.masterPeerId,data:{id:lockInfo.lock.id,move_to:"parent"}};let result;try{result=await this.request(req)}catch(err){lockInfo.lock.state=node_lock_1.LOCK_STATE.DONE;removeLock(lockInfo);throw err}lockInfo.lock=createIPCLock(result);return lockInfo.lock}};return lockInfo.lock};const req={type:"lock-request",id:acebase_core_1.ID.generate(),from:this.id,to:this.masterPeerId,data:details};let result,err;try{result=await this.request(req)}catch(e){err=e;result=null}if(err){removeLock(lockInfo);throw err}return createIPCLock(result)}}async request(req){let resolve,reject;const promise=new Promise(((rs,rj)=>{resolve=result=>{this._requests.delete(req.id);rs(result)};reject=err=>{this._requests.delete(req.id);rj(err)}}));this._requests.set(req.id,{resolve:resolve,reject:reject,request:req});this.sendMessage(req);return promise}sendRequest(request){const req={type:"request",from:this.id,to:this.masterPeerId,id:acebase_core_1.ID.generate(),data:request};return this.request(req).catch((err=>{this.logger.error(err);throw err}))}replyRequest(requestMessage,result){const reply={type:"result",id:requestMessage.id,ok:true,from:this.id,to:requestMessage.from,data:result};this.sendMessage(reply)}sendNotification(notification){const msg={type:"notification",from:this.id,data:notification};this.sendMessage(msg)}get eventsEnabled(){return this._eventsEnabled}set eventsEnabled(enabled){this.logger.info(`ipc events ${enabled?"enabled":"disabled"}`);this._eventsEnabled=enabled}}exports.AceBaseIPCPeer=AceBaseIPCPeer},{"../node-lock":13,"acebase-core":46}],10:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.RemovedNodeAddress=exports.NodeAddress=void 0;class NodeAddress{constructor(path){this.path=path}toString(){return`"/${this.path}"`}equals(address){return this.path===address.path}}exports.NodeAddress=NodeAddress;class RemovedNodeAddress extends NodeAddress{constructor(path){super(path)}toString(){return`"/${this.path}" (removed)`}equals(address){return address instanceof RemovedNodeAddress&&this.path===address.path}}exports.RemovedNodeAddress=RemovedNodeAddress},{}],11:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.NodeRevisionError=exports.NodeNotFoundError=void 0;class NodeNotFoundError extends Error{}exports.NodeNotFoundError=NodeNotFoundError;class NodeRevisionError extends Error{}exports.NodeRevisionError=NodeRevisionError},{}],12:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.NodeInfo=void 0;const node_value_types_1=require("./node-value-types");const acebase_core_1=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;this.childCount=info.childCount;if(typeof this.path==="string"&&(typeof this.key==="undefined"&&typeof this.index==="undefined")){const pathInfo=acebase_core_1.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(0,node_value_types_1.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.toString()}`}else{return`"${this.path}" is ${this.valueTypeName} with value ${this.value}`}}}exports.NodeInfo=NodeInfo},{"./node-value-types":14,"acebase-core":46}],13:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.NodeLock=exports.NodeLocker=exports.NodeLockError=exports.LOCK_STATE=void 0;const acebase_core_1=require("acebase-core");const assert_1=require("./assert");const DEBUG_MODE=false;const DEFAULT_LOCK_TIMEOUT=120;exports.LOCK_STATE={PENDING:"pending",LOCKED:"locked",EXPIRED:"expired",DONE:"done"};class NodeLockError extends Error{constructor(message,lock){super(message);this.lock=lock}}exports.NodeLockError=NodeLockError;class NodeLocker{constructor(logger,lockTimeout=DEFAULT_LOCK_TIMEOUT){this.logger=logger;this._locks=[];this._lastTid=0;this.timeout=lockTimeout*1e3}setTimeout(timeout){this.timeout=timeout*1e3}createTid(){return DEBUG_MODE?++this._lastTid:acebase_core_1.ID.generate()}_allowLock(path,tid,forWriting){const conflict=this._locks.find((otherLock=>otherLock.tid!==tid&&otherLock.state===exports.LOCK_STATE.LOCKED&&(forWriting||otherLock.forWriting)));return{allow:!conflict,conflict:conflict}}quit(){return new Promise((resolve=>{if(this._locks.length===0){return resolve()}this._quit=resolve}))}_rejectLock(lock,err){this._locks.splice(this._locks.indexOf(lock),1);clearTimeout(lock.timeout);try{lock.reject(err)}catch(err){console.error(`Unhandled promise rejection:`,err)}}_processLockQueue(){if(this._quit){const quitError=new Error("Quitting");this._locks.filter((lock=>lock.state===exports.LOCK_STATE.PENDING)).forEach((lock=>this._rejectLock(lock,quitError)));if(this._locks.length===0){this._quit()}}const pending=this._locks.filter((lock=>lock.state===exports.LOCK_STATE.PENDING)).sort(((a,b)=>{if(a.priority&&!b.priority){return-1}else if(!a.priority&&b.priority){return 1}return a.requested-b.requested}));pending.forEach((lock=>{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((err=>this._rejectLock(lock,err)))}}))}async lock(path,tid,forWriting=true,comment="",options={withPriority:false,noTimeout:false}){let lock,proceed;if(path instanceof NodeLock){lock=path;proceed=true}else if(this._locks.findIndex((l=>l.tid===tid&&l.state===exports.LOCK_STATE.EXPIRED))>=0){const expiredLock=this._locks.find((l=>l.tid===tid&&l.state===exports.LOCK_STATE.EXPIRED));throw new NodeLockError(`lock on tid ${tid} has expired, not allowed to continue`,expiredLock!==null&&expiredLock!==void 0?expiredLock:null)}else if(this._quit&&!options.withPriority){const refLock=this._locks.find((l=>l.tid===tid&&l.path===path));throw new NodeLockError(`Quitting`,refLock!==null&&refLock!==void 0?refLock:null)}else{DEBUG_MODE&&console.error(`${forWriting?"write":"read"} lock requested on "${path}" by tid ${tid} (${comment})`);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){DEBUG_MODE&&console.error(`${lock.forWriting?"write":"read"} lock ALLOWED on "${lock.path}" by tid ${lock.tid} (${lock.comment})`);lock.state=exports.LOCK_STATE.LOCKED;if(typeof lock.granted==="number"){}else{lock.granted=Date.now();if(options.noTimeout!==true){lock.expires=Date.now()+this.timeout;let timeoutCount=0;const timeoutHandler=()=>{if(lock.state!==exports.LOCK_STATE.LOCKED){return}timeoutCount++;if(timeoutCount<=3){this.logger.warn(`${lock.forWriting?"write":"read"} lock on "/${lock.path}" is taking long [${timeoutCount}]; tid=${lock.tid} comment=${lock.comment}`);lock.warned=true;lock.timeout=setTimeout(timeoutHandler,this.timeout/4);return}this.logger.error(`${lock.forWriting?"write":"read"} lock on "/${lock.path}" expired! tid=${lock.tid} comment=${lock.comment}`);lock.state=exports.LOCK_STATE.EXPIRED;this._processLockQueue()};lock.timeout=setTimeout(timeoutHandler,this.timeout/4)}}return lock}else{(0,assert_1.assert)(lock.state===exports.LOCK_STATE.PENDING);return new Promise(((resolve,reject)=>{lock.resolve=resolve;lock.reject=reject}))}}unlock(lockOrId,comment,processQueue=true){var _a,_b;let lock,i;if(lockOrId instanceof NodeLock){lock=lockOrId;i=this._locks.indexOf(lock)}else{const id=lockOrId;i=this._locks.findIndex((l=>l.id===id));lock=this._locks[i]}if(i<0){const msg=`lock on "/${(_a=lock===null||lock===void 0?void 0:lock.path)!==null&&_a!==void 0?_a:"?"}" for tid ${(_b=lock===null||lock===void 0?void 0:lock.tid)!==null&&_b!==void 0?_b:"?"} wasn't found; ${comment}`;throw new NodeLockError(msg,lock!==null&&lock!==void 0?lock:null)}lock.state=exports.LOCK_STATE.DONE;clearTimeout(lock.timeout);if(lock.warned){this.logger.info(`long running ${lock.forWriting?"write":"read"} lock on "${lock.path}" by tid ${lock.tid} has been released`)}this._locks.splice(i,1);DEBUG_MODE&&console.error(`${lock.forWriting?"write":"read"} lock RELEASED on "${lock.path}" by tid ${lock.tid}`);processQueue&&this._processLockQueue();return lock}list(){return this._locks||[]}isAllowed(path,tid,forWriting){return this._allowLock(path,tid,forWriting).allow}}exports.NodeLocker=NodeLocker;let lastid=0;class NodeLock{static get LOCK_STATE(){return exports.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=exports.LOCK_STATE.PENDING;this.requested=Date.now();this.comment="";this.waitingFor=null;this.id=++lastid;this.history=[];this.warned=false}async release(comment){this.history.push({action:"release",path:this.path,forWriting:this.forWriting,comment:comment});return this.locker.unlock(this,comment||this.comment)}async moveToParent(){const parentPath=acebase_core_1.PathInfo.get(this.path).parentPath;const allowed=this.locker.isAllowed(parentPath,this.tid,this.forWriting);if(allowed){DEBUG_MODE&&console.error(`moveToParent ALLOWED for ${this.forWriting?"write":"read"} lock on "${this.path}" by tid ${this.tid} (${this.comment})`);this.history.push({path:this.path,forWriting:this.forWriting,action:"moving to parent"});this.waitingFor=null;this.path=parentPath;return this}else{DEBUG_MODE&&console.error(`moveToParent QUEUED for ${this.forWriting?"write":"read"} lock on "${this.path}" by tid ${this.tid} (${this.comment})`);this.locker.unlock(this,`moveLockToParent: ${this.comment}`,false);const newLock=await this.locker.lock(parentPath,this.tid,this.forWriting,this.comment,{withPriority:true});DEBUG_MODE&&console.error(`QUEUED moveToParent ALLOWED for ${this.forWriting?"write":"read"} lock on "${this.path}" by tid ${this.tid} (${this.comment})`);newLock.history=this.history;newLock.history.push({path:this.path,forWriting:this.forWriting,action:"moving to parent through queue (priority)"});return newLock}}}exports.NodeLock=NodeLock},{"./assert":4,"acebase-core":46}],14:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.getValueType=exports.getNodeValueType=exports.getValueTypeName=exports.VALUE_TYPES=void 0;const acebase_core_1=require("acebase-core");const nodeValueTypes={OBJECT:1,ARRAY:2,NUMBER:3,BOOLEAN:4,STRING:5,BIGINT:7,DATETIME:6,BINARY:8,REFERENCE:9};exports.VALUE_TYPES=nodeValueTypes;function getValueTypeName(valueType){switch(valueType){case exports.VALUE_TYPES.ARRAY:return"array";case exports.VALUE_TYPES.BINARY:return"binary";case exports.VALUE_TYPES.BOOLEAN:return"boolean";case exports.VALUE_TYPES.DATETIME:return"date";case exports.VALUE_TYPES.NUMBER:return"number";case exports.VALUE_TYPES.OBJECT:return"object";case exports.VALUE_TYPES.REFERENCE:return"reference";case exports.VALUE_TYPES.STRING:return"string";case exports.VALUE_TYPES.BIGINT:return"bigint";default:"unknown"}}exports.getValueTypeName=getValueTypeName;function getNodeValueType(value){if(value instanceof Array){return exports.VALUE_TYPES.ARRAY}else if(value instanceof acebase_core_1.PathReference){return exports.VALUE_TYPES.REFERENCE}else if(value instanceof ArrayBuffer){return exports.VALUE_TYPES.BINARY}else if(typeof value==="string"){return exports.VALUE_TYPES.STRING}else if(typeof value==="object"){return exports.VALUE_TYPES.OBJECT}else if(typeof value==="bigint"){return exports.VALUE_TYPES.BIGINT}throw new Error(`Invalid value for standalone node: ${value}`)}exports.getNodeValueType=getNodeValueType;function getValueType(value){if(value instanceof Array){return exports.VALUE_TYPES.ARRAY}else if(value instanceof acebase_core_1.PathReference){return exports.VALUE_TYPES.REFERENCE}else if(value instanceof ArrayBuffer){return exports.VALUE_TYPES.BINARY}else if(value instanceof Date){return exports.VALUE_TYPES.DATETIME}else if(typeof value==="string"){return exports.VALUE_TYPES.STRING}else if(typeof value==="object"){return exports.VALUE_TYPES.OBJECT}else if(typeof value==="number"){return exports.VALUE_TYPES.NUMBER}else if(typeof value==="boolean"){return exports.VALUE_TYPES.BOOLEAN}else if(typeof value==="bigint"){return exports.VALUE_TYPES.BIGINT}throw new Error(`Unknown value type: ${value}`)}exports.getValueType=getValueType},{"acebase-core":46}],15:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.NotSupported=void 0;class NotSupported{constructor(context="browser"){throw new Error(`This feature is not supported in ${context} context`)}}exports.NotSupported=NotSupported},{}],16:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.pfs=void 0;class pfs{static get hasFileSystem(){return false}static get fs(){return null}}exports.pfs=pfs},{}],17:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.executeQuery=void 0;const acebase_core_1=require("acebase-core");const node_value_types_1=require("./node-value-types");const node_errors_1=require("./node-errors");const data_index_1=require("./data-index");const async_task_batch_1=require("./async-task-batch");const noop=()=>{};async function executeQuery(api,path,query,options={snapshots:false,include:undefined,exclude:undefined,child_objects:undefined,eventHandler:noop}){var _a,_b,_c,_d,_e,_f;if(typeof options!=="object"){options={}}if(typeof options.snapshots==="undefined"){options.snapshots=false}const context={};if((_a=api.storage.settings.transactions)===null||_a===void 0?void 0:_a.log){context.acebase_cursor=acebase_core_1.ID.generate()}const queryFilters=query.filters.map((f=>Object.assign({},f)));const querySort=query.order.map((s=>Object.assign({},s)));const sortMatches=matches=>{matches.sort(((a,b)=>{const compare=i=>{const o=querySort[i];const trailKeys=acebase_core_1.PathInfo.getPathKeys(typeof o.key==="number"?`[${o.key}]`:o.key);const left=trailKeys.reduce(((val,key)=>val!==null&&typeof val==="object"&&key in val?val[key]:null),a.val);const right=trailKeys.reduce(((val,key)=>val!==null&&typeof val==="object"&&key in val?val[key]:null),b.val);if(left===null){return right===null?0:o.ascending?-1:1}if(right===null){return o.ascending?1:-1}if(left==right){if(i{if(preResults.length===0){return[]}const maxBatchSize=50;const batch=new async_task_batch_1.AsyncTaskBatch(maxBatchSize);const results=[];preResults.forEach((({path:path},index)=>batch.add((async()=>{const node=await api.storage.getNode(path,options);const val=node.value;if(val===null){api.logger.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+Math.abs(query.take)){sortMatches(results);results.pop()}}}))));await batch.finish();return results};const pathInfo=acebase_core_1.PathInfo.get(path);const isWildcardPath=pathInfo.keys.some((key=>key==="*"||key.toString().startsWith("$")));const availableIndexes=api.storage.indexes.get(path);const usingIndexes=[];let stop=async()=>{};if(isWildcardPath){const vars=pathInfo.keys.filter((key=>typeof key==="string"&&key.startsWith("$")));const hasExplicitFilterValues=vars.length>0&&vars.every((v=>query.filters.some((f=>f.key===v&&["==","in"].includes(f.op)))));const isRealtime=typeof options.monitor==="object"&&[(_b=options.monitor)===null||_b===void 0?void 0:_b.add,(_c=options.monitor)===null||_c===void 0?void 0:_c.change,(_d=options.monitor)===null||_d===void 0?void 0:_d.remove].some((val=>val===true));if(hasExplicitFilterValues&&!isRealtime){const combinations=[];for(const v of vars){const filters=query.filters.filter((f=>f.key===v));const filterValues=filters.reduce(((values,f)=>{if(f.op==="=="){values.push(f.compare)}if(f.op==="in"){if(!(f.compare instanceof Array)){throw new Error(`compare argument for 'in' operator must be an Array`)}values.push(...f.compare)}return values}),[]);const prevCombinations=combinations.splice(0);filterValues.forEach((fv=>{if(prevCombinations.length===0){combinations.push({[v]:fv})}else{combinations.push(...prevCombinations.map((c=>Object.assign(Object.assign({},c),{[v]:fv}))))}}))}const filters=query.filters.filter((f=>!vars.includes(f.key)));const paths=combinations.map((vars=>acebase_core_1.PathInfo.get(acebase_core_1.PathInfo.getPathKeys(path).map((key=>{var _a;return(_a=vars[key])!==null&&_a!==void 0?_a:key}))).path));const loadData=query.order.length>0;const promises=paths.map((path=>{var _a;return executeQuery(api,path,{filters:filters,take:0,skip:0,order:[]},{snapshots:loadData,cache_mode:options.cache_mode,include:[...(_a=options.include)!==null&&_a!==void 0?_a:[],...query.order.map((o=>o.key))],exclude:options.exclude})}));const resultSets=await Promise.all(promises);let results=resultSets.reduce(((results,set)=>(results.push(...set.results),results)),[]);if(loadData){sortMatches(results)}if(query.skip>0){results.splice(0,query.skip)}if(query.take>0){results.splice(query.take)}if(options.snapshots&&(!loadData||((_e=options.include)===null||_e===void 0?void 0:_e.length)>0||((_f=options.exclude)===null||_f===void 0?void 0:_f.length)>0||!options.child_objects)){const{include:include,exclude:exclude,child_objects:child_objects}=options;results=await loadResultsData(results,{include:include,exclude:exclude,child_objects:child_objects})}return{results:results,context:null,stop:stop}}else if(availableIndexes.length===0){const err=new Error(`Query on wildcard path "/${path}" requires an index`);return Promise.reject(err)}if(queryFilters.length===0){const index=availableIndexes.filter((index=>index.type==="normal"))[0];queryFilters.push({key:index.key,op:"!=",compare:null})}}queryFilters.forEach((filter=>{if(filter.index){return}const indexesOnKey=availableIndexes.filter((index=>index.key===filter.key)).filter((index=>index.validOperators.includes(filter.op)));if(indexesOnKey.length>=1){const otherFilterKeys=queryFilters.filter((f=>f!==filter)).map((f=>f.key));const sortKeys=querySort.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.includes(key)));const forSorting=availableKeys.filter((key=>sortKeys.includes(key)));const forBoth=forOtherFilters.concat(forSorting.filter((index=>!forOtherFilters.includes(index))));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=>{queryFilters.filter((f=>f!==filter&&f.key===key)).forEach((f=>{if(!data_index_1.DataIndex.validOperators.includes(f.op)){return}f.indexUsage="filter";f.index=bestBenificialIndex.index}))}));bestBenificialIndex.sortKeys.forEach((key=>{querySort.filter((s=>s.key===key)).forEach((s=>{s.index=bestBenificialIndex.index}))}))}if(filter.index){usingIndexes.push({index:filter.index,description:filter.index.description})}}));if(querySort.length>0&&query.take!==0&&queryFilters.length===0){querySort.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&&api.logger.info(`Using indexes for query: ${indexDescriptions}`);const tableScanFilters=queryFilters.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=[];queryFilters.forEach((filter=>{if(filter.index&&filter.indexUsage!=="filter"){let promise=filter.index.query(filter.op,filter.compare).then((results=>{var _a,_b;(_a=options.eventHandler)===null||_a===void 0?void 0:_a.call(options,{name:"stats",type:"index_query",source:filter.index.description,stats:results.stats});if(results.hints.length>0){(_b=options.eventHandler)===null||_b===void 0?void 0:_b.call(options,{name:"hints",type:"index_query",source:filter.index.description,hints:results.hints})}return results}));const resultFilters=queryFilters.filter((f=>f.index===filter.index&&f.indexUsage==="filter"));if(resultFilters.length>0){promise=promise.then((results=>{resultFilters.forEach((filter=>{const{key:key,op:op,index:index}=filter;let{compare:compare}=filter;if(typeof compare==="string"&&!index.caseSensitive){compare=compare.toLocaleLowerCase(index.textLocale)}results=results.filterMetadata(key,op,compare)}));return results}))}indexScanPromises.push(promise)}}));const stepsExecuted={filtered:queryFilters.length===0,skipped:query.skip===0,taken:query.take===0,sorted:querySort.length===0,preDataLoaded:false,dataLoaded:false};if(queryFilters.length===0&&query.take===0){api.logger.warn(`Filterless queries must use .take to limit the results. Defaulting to 100 for query on path "${path}"`);query.take=100}if(querySort.length>0&&querySort[0].index){const sortIndex=querySort[0].index;const ascending=query.take<0?!querySort[0].ascending:querySort[0].ascending;if(queryFilters.length===0&&querySort.slice(1).every((s=>sortIndex.allMetadataKeys.includes(s.key)))){api.logger.info(`Using index for sorting: ${sortIndex.description}`);const metadataSort=querySort.slice(1).map((s=>{s.index=sortIndex;return{key:s.key,ascending:s.ascending}}));const promise=sortIndex.take(query.skip,Math.abs(query.take),{ascending:ascending,metadataSort:metadataSort}).then((results=>{var _a,_b;(_a=options.eventHandler)===null||_a===void 0?void 0:_a.call(options,{name:"stats",type:"sort_index_take",source:sortIndex.description,stats:results.stats});if(results.hints.length>0){(_b=options.eventHandler)===null||_b===void 0?void 0:_b.call(options,{name:"hints",type:"sort_index_take",source:sortIndex.description,hints:results.hints})}return results}));indexScanPromises.push(promise);stepsExecuted.skipped=true;stepsExecuted.taken=true;stepsExecuted.sorted=true}}return Promise.all(indexScanPromises).then((async 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=>m.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(querySort.length===0||querySort.every((o=>o.index))){stepsExecuted.preDataLoaded=true;if(!stepsExecuted.sorted&&querySort.length>0){sortMatches(indexedResults)}stepsExecuted.sorted=true;if(!stepsExecuted.skipped&&query.skip>0){indexedResults=query.take<0?indexedResults.slice(0,-query.skip):indexedResults.slice(query.skip)}if(!stepsExecuted.taken&&query.take!==0){indexedResults=query.take<0?indexedResults.slice(query.take):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=querySort.length>0;const childOptions=loadPartialResults?{include:querySort.map((order=>order.key))}:{include:options.include,exclude:options.exclude,child_objects:options.child_objects};return loadResultsData(indexedResults,childOptions).then((results=>{if(querySort.length>0){sortMatches(results)}stepsExecuted.sorted=true;if(query.skip>0){results=query.take<0?results.slice(0,-query.skip):results.slice(query.skip)}if(query.take!==0){results=query.take<0?results.slice(query.take):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))}let matches=[];let preliminaryStop=false;const loadPartialData=querySort.length>0;const childOptions=loadPartialData?{include:querySort.map((order=>order.key))}:{include:options.include,exclude:options.exclude,child_objects:options.child_objects};const batch={promises:[],async add(promise){this.promises.push(promise);if(this.promises.length>=1e3){await Promise.all(this.promises.splice(0))}}};try{await api.storage.getChildren(path,{keyFilter:indexKeyFilter,async:true}).next((child=>{if(child.type!==node_value_types_1.VALUE_TYPES.OBJECT){return}if(!child.address){return}if(preliminaryStop){return false}const matchNode=async()=>{const isMatch=await api.storage.matchNode(child.address.path,tableScanFilters);if(!isMatch){return}const childPath=child.address.path;let result;if(options.snapshots||querySort.length>0){const node=await api.storage.getNode(childPath,childOptions);result={path:childPath,val:node.value}}else{result={path:childPath}}matches.push(result);if(query.take!==0&&matches.length>Math.abs(query.take)+query.skip){if(querySort.length>0){sortMatches(matches)}else if(query.take>0){preliminaryStop=true}matches.pop()}};const p=batch.add(matchNode());if(p instanceof Promise){return p}}))}catch(reason){if(!(reason instanceof node_errors_1.NodeNotFoundError)){api.logger.warn(`Error getting child stream: ${reason}`)}return[]}await Promise.all(batch.promises);stepsExecuted.preDataLoaded=loadPartialData;stepsExecuted.dataLoaded=!loadPartialData;if(querySort.length>0){sortMatches(matches)}stepsExecuted.sorted=true;if(query.skip>0){matches=query.take<0?matches.slice(0,-query.skip):matches.slice(query.skip)}stepsExecuted.skipped=true;if(query.take!==0){matches=query.take<0?matches.slice(query.take):matches.slice(0,query.take)}stepsExecuted.taken=true;if(!stepsExecuted.dataLoaded){matches=await loadResultsData(matches,{include:options.include,exclude:options.exclude,child_objects:options.child_objects});stepsExecuted.dataLoaded=true}return matches})).then((matches=>{if(!stepsExecuted.sorted&&querySort.length>0){sortMatches(matches)}if(!options.snapshots){matches=matches.map((match=>match.path))}if(!stepsExecuted.skipped&&query.skip>0){matches=query.take<0?matches.slice(0,-query.skip):matches.slice(query.skip)}if(!stepsExecuted.taken&&query.take!==0){matches=query.take<0?matches.slice(query.take):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 monitor=options.monitor;const matchedPaths=options.snapshots?matches.map((match=>match.path)):matches.slice();const ref=api.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=()=>{api.unsubscribe(ref.path,"child_changed",childChangedCallback);api.unsubscribe(ref.path,"child_added",childAddedCallback);api.unsubscribe(ref.path,"notify_child_removed",childRemovedCallback)};stop=async()=>{stopMonitoring()};const childChangedCallback=async(err,path,newValue,oldValue)=>{const wasMatch=matchedPaths.includes(path);let keepMonitoring=true;const checkKeys=[];queryFilters.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=queryFilters.filter((filter=>filter.key===key));return filters.every((filter=>{var _a;if(((_a=filter.index)===null||_a===void 0?void 0:_a.textLocaleKey)&&!seenKeys.includes(filter.index.textLocaleKey)){missingKeys.push(filter.index.textLocaleKey);return true}else if(allowedTableScanOperators.includes(filter.op)){return api.storage.test(newValue[key],filter.op,filter.compare)}else{return filter.index.test(newValue,filter.op,filter.compare)}}))}));if(isMatch){missingKeys.push(...checkKeys.filter((key=>!seenKeys.includes(key))));if(!wasMatch&&missingKeys.length>0){const filterQueue=queryFilters.filter((f=>missingKeys.includes(f.key)));const simpleFilters=filterQueue.filter((f=>allowedTableScanOperators.includes(f.op)));const indexFilters=filterQueue.filter((f=>!allowedTableScanOperators.includes(f.op)));if(simpleFilters.length>0){isMatch=await api.storage.matchNode(path,simpleFilters)}if(isMatch&&indexFilters.length>0){const keysToLoad=indexFilters.reduce(((keys,filter)=>{if(!keys.includes(filter.key)){keys.push(filter.key)}if(filter.index instanceof data_index_1.FullTextIndex&&filter.index.config.localeKey&&!keys.includes(filter.index.config.localeKey)){keys.push(filter.index.config.localeKey)}return keys}),[]);const node=await api.storage.getNode(path,{include:keysToLoad});if(node.value===null){return false}isMatch=indexFilters.every((filter=>filter.index.test(node.value,filter.op,filter.compare)))}}}if(isMatch){if(!wasMatch){addMatch(path)}if(options.snapshots){const loadOptions={include:options.include,exclude:options.exclude,child_objects:options.child_objects};const node=await api.storage.getNode(path,loadOptions);newValue=node.value}if(wasMatch&&monitor.change){keepMonitoring=options.eventHandler({name:"change",path:path,value:newValue})!==false}else if(!wasMatch&&monitor.add){keepMonitoring=options.eventHandler({name:"add",path:path,value:newValue})!==false}}else if(wasMatch){removeMatch(path);if(monitor.remove){keepMonitoring=options.eventHandler({name:"remove",path:path,value:oldValue})!==false}}if(keepMonitoring===false){stopMonitoring()}};const childAddedCallback=(err,path,newValue)=>{const isMatch=queryFilters.every((filter=>{if(allowedTableScanOperators.includes(filter.op)){return api.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(monitor.add){keepMonitoring=options.eventHandler({name:"add",path:path,value:options.snapshots?newValue:null})!==false}}if(keepMonitoring===false){stopMonitoring()}};const childRemovedCallback=(err,path,newValue,oldValue)=>{let keepMonitoring=true;removeMatch(path);if(monitor.remove){keepMonitoring=options.eventHandler({name:"remove",path:path,value:options.snapshots?oldValue:null})!==false}if(keepMonitoring===false){stopMonitoring()}};if(options.monitor.add||options.monitor.change||options.monitor.remove){api.subscribe(ref.path,"child_changed",childChangedCallback)}if(options.monitor.remove){api.subscribe(ref.path,"notify_child_removed",childRemovedCallback)}if(options.monitor.add){api.subscribe(ref.path,"child_added",childAddedCallback)}}return{results:matches,context:context,stop:stop}}))}exports.executeQuery=executeQuery},{"./async-task-batch":5,"./data-index":7,"./node-errors":11,"./node-value-types":14,"acebase-core":46}],18:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.AceBaseStorage=exports.AceBaseStorageSettings=void 0;const not_supported_1=require("../../not-supported");class AceBaseStorageSettings extends not_supported_1.NotSupported{}exports.AceBaseStorageSettings=AceBaseStorageSettings;class AceBaseStorage extends not_supported_1.NotSupported{}exports.AceBaseStorage=AceBaseStorage},{"../../not-supported":15}],19:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.createIndex=void 0;const acebase_core_1=require("acebase-core");const data_index_1=require("../data-index");const promise_fs_1=require("../promise-fs");async function createIndex(context,path,key,options){if(!context.storage.indexes.supported){throw new Error("Indexes are not supported in current environment because it requires Node.js fs")}const{ipc:ipc,logger:logger,indexes:indexes,storage:storage}=context;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&&options.config){existingIndex.config=options.config}if(existingIndex&&rebuild!==true){logger.info(`Index on "/${path}/*/${key}" already exists`.colorize(acebase_core_1.ColorStyle.inverse));return existingIndex}if(!ipc.isMaster){const result=await ipc.sendRequest({type:"index.create",path:path,key:key,options:options});if(result.ok){return storage.indexes.add(result.fileName)}throw new Error(result.reason)}await promise_fs_1.pfs.mkdir(`${storage.settings.path}/${storage.name}.acebase`).catch((err=>{if(err.code!=="EEXIST"){throw err}}));const index=existingIndex||(()=>{const{include:include,caseSensitive:caseSensitive,textLocale:textLocale,textLocaleKey:textLocaleKey}=options;const indexOptions={include:include,caseSensitive:caseSensitive,textLocale:textLocale,textLocaleKey:textLocaleKey};switch(indexType){case"array":return new data_index_1.ArrayIndex(storage,path,key,Object.assign({},indexOptions));case"fulltext":return new data_index_1.FullTextIndex(storage,path,key,Object.assign(Object.assign({},indexOptions),{config:options.config}));case"geo":return new data_index_1.GeoIndex(storage,path,key,Object.assign({},indexOptions));default:return new data_index_1.DataIndex(storage,path,key,Object.assign({},indexOptions))}})();if(!existingIndex){indexes.push(index)}try{await index.build()}catch(err){context.logger.error(`Index build on "/${path}/*/${key}" failed: ${err.message} (code: ${err.code})`.colorize(acebase_core_1.ColorStyle.red));if(!existingIndex){indexes.splice(indexes.indexOf(index),1)}throw err}ipc.sendNotification({type:"index.created",fileName:index.fileName,path:path,key:key,options:options});return index}exports.createIndex=createIndex},{"../data-index":7,"../promise-fs":16,"acebase-core":46}],20:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.CustomStorageHelpers=void 0;const acebase_core_1=require("acebase-core");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 acebase_core_1.PathInfo}}exports.CustomStorageHelpers=CustomStorageHelpers},{"acebase-core":46}],21:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.CustomStorage=exports.CustomStorageNodeInfo=exports.CustomStorageNodeAddress=exports.CustomStorageSettings=exports.CustomStorageTransaction=exports.ICustomStorageNode=exports.ICustomStorageNodeMetaData=exports.CustomStorageHelpers=void 0;const acebase_core_1=require("acebase-core");const{compareValues:compareValues}=acebase_core_1.Utils;const node_info_1=require("../../node-info");const node_lock_1=require("../../node-lock");const node_value_types_1=require("../../node-value-types");const node_errors_1=require("../../node-errors");const index_1=require("../index");const helpers_1=require("./helpers");const node_address_1=require("../../node-address");const assert_1=require("../../assert");var helpers_2=require("./helpers");Object.defineProperty(exports,"CustomStorageHelpers",{enumerable:true,get:function(){return helpers_2.CustomStorageHelpers}});class ICustomStorageNodeMetaData{constructor(){this.revision="";this.revision_nr=0;this.created=0;this.modified=0;this.type=0}}exports.ICustomStorageNodeMetaData=ICustomStorageNodeMetaData;class ICustomStorageNode extends ICustomStorageNodeMetaData{constructor(){super();this.value=null}}exports.ICustomStorageNode=ICustomStorageNode;class CustomStorageTransaction{constructor(target){this.production=false;this.target={get originalPath(){return target.path},path:target.path,get write(){return target.write}};this.id=acebase_core_1.ID.generate()}async getChildCount(path){let childCount=0;await this.childrenOf(path,{metadata:false,value:false},(()=>{childCount++;return false}));return childCount}async getMultiple(paths){const map=new Map;await Promise.all(paths.map((path=>this.get(path).then((val=>map.set(path,val))))));return map}async setMultiple(nodes){await Promise.all(nodes.map((({path:path,node:node})=>this.set(path,node))))}async removeMultiple(paths){await Promise.all(paths.map((path=>this.remove(path))))}async 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=helpers_1.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}}exports.CustomStorageTransaction=CustomStorageTransaction;class CustomStorageSettings extends index_1.StorageSettings{constructor(settings){super(settings);this.locking=true;if(typeof settings!=="object"){throw new Error("settings missing")}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.locking=settings.locking!==false;if(this.locking){this.lockTimeout=typeof settings.lockTimeout==="number"?settings.lockTimeout:120}this.ready=settings.ready;const useLocking=this.locking;const nodeLocker=useLocking?new node_lock_1.NodeLocker(console,this.lockTimeout):null;this.getTransaction=async({path:path,write:write})=>{const transaction=await settings.getTransaction({path:path,write:write});(0,assert_1.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}}}exports.CustomStorageSettings=CustomStorageSettings;class CustomStorageNodeAddress{constructor(containerPath){this.path=containerPath}}exports.CustomStorageNodeAddress=CustomStorageNodeAddress;class CustomStorageNodeInfo extends node_info_1.NodeInfo{constructor(info){super(info);this.revision=info.revision;this.revision_nr=info.revision_nr;this.created=info.created;this.modified=info.modified}}exports.CustomStorageNodeInfo=CustomStorageNodeInfo;class CustomStorage extends index_1.Storage{constructor(dbname,settings,env){super(dbname,settings,env);this._customImplementation=settings;this._init()}async _init(){this.logger.info(`Database "${this.name}" details:`.colorize(acebase_core_1.ColorStyle.dim));this.logger.info(`- Type: CustomStorage`.colorize(acebase_core_1.ColorStyle.dim));this.logger.info(`- Path: ${this.settings.path}`.colorize(acebase_core_1.ColorStyle.dim));this.logger.info(`- Max inline value size: ${this.settings.maxInlineValueSize}`.colorize(acebase_core_1.ColorStyle.dim));this.logger.info(`- Autoremove undefined props: ${this.settings.removeVoidProperties}`.colorize(acebase_core_1.ColorStyle.dim));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")}throwImplementationError(message){throw new Error(`CustomStorage "${this._customImplementation.name}" ${message}`)}_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:node_value_types_1.VALUE_TYPES.DATETIME,value:val.getTime()}}else if(val instanceof acebase_core_1.PathReference){return{type:node_value_types_1.VALUE_TYPES.REFERENCE,value:val.path}}else if(val instanceof ArrayBuffer){return{type:node_value_types_1.VALUE_TYPES.BINARY,value:acebase_core_1.ascii85.encode(val)}}else if(typeof val==="object"){(0,assert_1.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===node_value_types_1.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===node_value_types_1.VALUE_TYPES.BINARY){return acebase_core_1.ascii85.decode(val.value)}else if(val.type===node_value_types_1.VALUE_TYPES.DATETIME){return new Date(val.value)}else if(val.type===node_value_types_1.VALUE_TYPES.REFERENCE){return new acebase_core_1.PathReference(val.value)}else{throw new Error(`Unhandled child value type ${val.type}`)}};switch(node.type){case node_value_types_1.VALUE_TYPES.ARRAY:case node_value_types_1.VALUE_TYPES.OBJECT:{const obj=node.value;Object.keys(obj).forEach((key=>{const item=obj[key];if(typeof item==="object"&&"type"in item){obj[key]=getTypedChildValue(item)}}));node.value=obj;break}case node_value_types_1.VALUE_TYPES.BINARY:{node.value=acebase_core_1.ascii85.decode(node.value);break}case node_value_types_1.VALUE_TYPES.REFERENCE:{node.value=new acebase_core_1.PathReference(node.value);break}case node_value_types_1.VALUE_TYPES.STRING:{break}default:throw new Error(`Invalid standalone record value type`)}}async _readNode(path,options){const node=await options.transaction.get(path);if(node===null){return null}if(typeof node!=="object"){this.throwImplementationError(`transaction.get 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=node_value_types_1.VALUE_TYPES.STRING}else if(typeof val==="number"){type=node_value_types_1.VALUE_TYPES.NUMBER}else if(typeof val==="boolean"){type=node_value_types_1.VALUE_TYPES.BOOLEAN}else if(val instanceof Array){type=node_value_types_1.VALUE_TYPES.ARRAY}else if(typeof val==="object"){if("type"in val){const serialized=val;type=serialized.type;val=serialized.value;if(type===node_value_types_1.VALUE_TYPES.DATETIME){val=new Date(val)}else if(type===node_value_types_1.VALUE_TYPES.REFERENCE){val=new acebase_core_1.PathReference(val)}}else{type=node_value_types_1.VALUE_TYPES.OBJECT}}else{throw new Error(`Unknown value type`)}return{type:type,value:val}}async _writeNode(path,value,options){if(!options.merge&&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`)}if(typeof options.diff==="undefined"&&typeof options.currentValue!=="undefined"){const diff=compareValues(options.currentValue,value);if(options.merge&&typeof diff==="object"){diff.removed=diff.removed.filter((key=>value[key]===null))}options.diff=diff}if(options.diff==="identical"){return}const transaction=options.transaction;const currentRow=options.currentValue===null?null:await this._readNode(path,{transaction:transaction});if(options.merge&¤tRow){if(currentRow.type===node_value_types_1.VALUE_TYPES.ARRAY&&!(value instanceof Array)&&typeof value==="object"&&Object.keys(value).some((key=>isNaN(parseInt(key))))){throw new Error(`Cannot merge existing array of path "${path}" with an object`)}if(value instanceof Array&¤tRow.type!==node_value_types_1.VALUE_TYPES.ARRAY){throw new Error(`Cannot merge existing object of path "${path}" with an array`)}}const revision=options.revision||acebase_core_1.ID.generate();const mainNode={type:currentRow&¤tRow.type===node_value_types_1.VALUE_TYPES.ARRAY?node_value_types_1.VALUE_TYPES.ARRAY:node_value_types_1.VALUE_TYPES.OBJECT,value:{}};const childNodeValues={};if(value instanceof Array){mainNode.type=node_value_types_1.VALUE_TYPES.ARRAY;const obj={};for(let i=0;i{if(!(key in value)){value[key]=null}}))}Object.keys(value).forEach((key=>{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}}))}const isArray=mainNode.type===node_value_types_1.VALUE_TYPES.ARRAY;if(currentRow){this.logger.info(`Node "/${path}" is being ${options.merge?"updated":"overwritten"}`.colorize(acebase_core_1.ColorStyle.cyan));if(currentIsObjectOrArray||newIsObjectOrArray){const pathInfo=acebase_core_1.PathInfo.get(path);const keys=[];let checkExecuted=false;const includeChildCheck=childPath=>{checkExecuted=true;if(!transaction.production&&!pathInfo.isParentOf(childPath)){this.throwImplementationError(`"${childPath}" is not a child of "${path}" - childrenOf must only check and return paths that are children`)}return true};const addChildPath=childPath=>{if(!checkExecuted){this.throwImplementationError(`childrenOf did not call checkCallback before addCallback`)}const key=acebase_core_1.PathInfo.get(childPath).key;keys.push(key.toString());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:[],delete:options&&options.merge?Object.keys(value).filter((key=>value[key]===null)):children.current.filter((key=>!children.new.includes(key)))};changes.update=children.new.filter((key=>children.current.includes(key)&&!changes.delete.includes(key)));if(isArray&&options.merge&&(changes.insert.length>0||changes.delete.length>0)){const newArrayKeys=changes.update.concat(changes.insert);const isExhaustive=newArrayKeys.every(((k,index,arr)=>arr.includes(index.toString())));if(!isExhaustive){throw new Error(`Elements cannot be inserted beyond, or removed before the end of an array. Rewrite the whole array at path "${path}" or change your schema to use an object collection instead`)}}const writePromises=Object.keys(childNodeValues).map((key=>{const keyOrIndex=isArray?parseInt(key):key;const childDiff=typeof options.diff==="object"?options.diff.forChild(keyOrIndex):undefined;if(childDiff==="identical"){return}const childPath=pathInfo.childPath(keyOrIndex);const childValue=childNodeValues[keyOrIndex];const currentChildValue=typeof options.currentValue==="undefined"?undefined:options.currentValue!==null&&typeof options.currentValue==="object"&&keyOrIndex in options.currentValue?options.currentValue[keyOrIndex]:null;return this._writeNode(childPath,childValue,{transaction:transaction,revision:revision,merge:false,currentValue:currentChildValue,diff:childDiff})}));const movingNodes=newIsObjectOrArray?keys.filter((key=>key in mainNode.value)):[];const deleteDedicatedKeys=changes.delete.concat(movingNodes);const deletePromises=deleteDedicatedKeys.map((key=>{const keyOrIndex=isArray?parseInt(key):key;const childPath=pathInfo.childPath(keyOrIndex);return this._deleteNode(childPath,{transaction:transaction})}));const promises=writePromises.concat(deletePromises);await Promise.all(promises)}const p=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});if(p instanceof Promise){return await p}}else{this.logger.info(`Node "/${path}" is being created`.colorize(acebase_core_1.ColorStyle.cyan));if(isArray){const arrayKeys=Object.keys(mainNode.value).concat(Object.keys(childNodeValues));const isExhaustive=arrayKeys.every(((k,index,arr)=>arr.includes(index.toString())));if(!isExhaustive){throw new Error(`Cannot store arrays with missing entries`)}}const promises=Object.keys(childNodeValues).map((key=>{const keyOrIndex=isArray?parseInt(key):key;const childPath=acebase_core_1.PathInfo.getChildPath(path,keyOrIndex);const childValue=childNodeValues[keyOrIndex];return this._writeNode(childPath,childValue,{transaction:transaction,revision:revision,merge:false,currentValue:null})}));const p=this._storeNode(path,{type:mainNode.type,value:mainNode.value,revision:revision,revision_nr:1,created:Date.now(),modified:Date.now()},{transaction:transaction});if(p instanceof Promise){promises.push(p)}await Promise.all(promises)}}async _deleteNode(path,options){const pathInfo=acebase_core_1.PathInfo.get(path);this.logger.info(`Node "/${path}" is being deleted`.colorize(acebase_core_1.ColorStyle.cyan));const deletePaths=[path];let checkExecuted=false;const includeDescendantCheck=descPath=>{checkExecuted=true;if(!transaction.production&&!pathInfo.isAncestorOf(descPath)){this.throwImplementationError(`"${descPath}" is not a descendant of "${path}" - descendantsOf must only check and return paths that are descendants`)}return true};const addDescendant=descPath=>{if(!checkExecuted){this.throwImplementationError(`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.logger.info(`Nodes ${deletePaths.map((p=>`"/${p}"`)).join(",")} are being deleted`.colorize(acebase_core_1.ColorStyle.cyan));return transaction.removeMultiple(deletePaths)}getChildren(path,options={}){let 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()=>{const node=await this._readNode(path,{transaction:transaction});if(!node){throw new node_errors_1.NodeNotFoundError(`Node "/${path}" does not exist`)}if(![node_value_types_1.VALUE_TYPES.OBJECT,node_value_types_1.VALUE_TYPES.ARRAY].includes(node.type)){return}const isArray=node.type===node_value_types_1.VALUE_TYPES.ARRAY;const value=node.value;let keys=Object.keys(value).map((key=>isArray?parseInt(key):key));if(options.keyFilter){keys=keys.filter((key=>options.keyFilter.includes(key)))}const pathInfo=acebase_core_1.PathInfo.get(path);keys.length>0&&keys.every((key=>{const 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:new Date(node.created),modified:new Date(node.modified)});canceled=callback(info)===false;return!canceled}));if(canceled){return}let checkExecuted=false;const includeChildCheck=childPath=>{checkExecuted=true;if(!transaction.production&&!pathInfo.isParentOf(childPath)){this.throwImplementationError(`"${childPath}" is not a child of "${path}" - childrenOf must only check and return paths that are children`)}if(options.keyFilter){const key=acebase_core_1.PathInfo.get(childPath).key;return options.keyFilter.includes(key)}return true};const addChildNode=(childPath,node)=>{if(!checkExecuted){this.throwImplementationError(`childrenOf did not call checkCallback before addCallback`)}const key=acebase_core_1.PathInfo.get(childPath).key;const info=new CustomStorageNodeInfo({path:childPath,type:node.type,key:isArray?null:key,index:isArray?key:null,address:new node_address_1.NodeAddress(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.include.length>0||options.exclude&&options.exclude.length>0||options.child_objects===false;const pathInfo=acebase_core_1.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);(0,assert_1.assert)(lockPath===pathInfo.parentPath,`transaction.moveToParentPath() did not move to the right parent path of "${path}"`);const parentNode=await this._readNode(pathInfo.parentPath,{transaction:transaction});if(parentNode&&[node_value_types_1.VALUE_TYPES.OBJECT,node_value_types_1.VALUE_TYPES.ARRAY].includes(parentNode.type)&&pathInfo.key in parentNode.value){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 isArray=targetNode.type===node_value_types_1.VALUE_TYPES.ARRAY;const convertFilterArray=arr=>{const isNumber=key=>/^[0-9]+$/.test(key);return arr.map((path=>acebase_core_1.PathInfo.get(isArray&&isNumber(path)?`[${path}]`:path)))};const includeFilter=options.include?convertFilterArray(options.include):[];const excludeFilter=options.exclude?convertFilterArray(options.exclude):[];const applyFiltersOnInlineData=(descPath,node)=>{if([node_value_types_1.VALUE_TYPES.OBJECT,node_value_types_1.VALUE_TYPES.ARRAY].includes(node.type)&&includeFilter.length>0){const trailKeys=acebase_core_1.PathInfo.getPathKeys(descPath).slice(pathInfo.keys.length);const checkPathInfo=new acebase_core_1.PathInfo(trailKeys);const remove=[];const includes=includeFilter.filter((info=>info.isDescendantOf(checkPathInfo)));if(includes.length>0){const isArray=node.type===node_value_types_1.VALUE_TYPES.ARRAY;remove.push(...Object.keys(node.value).map((key=>isArray?+key:key)));for(const info of includes){const targetProp=info.keys[trailKeys.length];if(typeof targetProp==="string"&&(targetProp==="*"||targetProp.startsWith("$"))){remove.splice(0);break}const index=remove.indexOf(targetProp);index>=0&&remove.splice(index,1)}}const hasIncludeOnChild=includeFilter.some((info=>info.isChildOf(checkPathInfo)));const hasExcludeOnChild=excludeFilter.some((info=>info.isChildOf(checkPathInfo)));if(hasExcludeOnChild&&!hasIncludeOnChild){const excludes=excludeFilter.filter((info=>info.isChildOf(checkPathInfo)));for(let i=0;iinfo.equals(remove[i])))){remove.splice(i,1);i--}}}for(const key of remove){delete node.value[key]}}};applyFiltersOnInlineData(path,targetNode);let checkExecuted=false;const includeDescendantCheck=(descPath,metadata)=>{checkExecuted=true;if(!transaction.production&&!pathInfo.isAncestorOf(descPath)){this.throwImplementationError(`"${descPath}" is not a descendant of "${path}" - descendantsOf must only check and return paths that are descendants`)}if(!filtered){return true}const descPathKeys=acebase_core_1.PathInfo.getPathKeys(descPath);const trailKeys=descPathKeys.slice(pathInfo.keys.length);const checkPathInfo=new acebase_core_1.PathInfo(trailKeys);let include=(includeFilter.length>0?includeFilter.some((info=>checkPathInfo.isOnTrailOf(info))):true)&&(excludeFilter.length>0?!excludeFilter.some((info=>info.equals(checkPathInfo)||info.isAncestorOf(checkPathInfo))):true);if(include&&options.child_objects===false&&(pathInfo.isParentOf(descPath)&&[node_value_types_1.VALUE_TYPES.OBJECT,node_value_types_1.VALUE_TYPES.ARRAY].includes(metadata?metadata.type:-1)||acebase_core_1.PathInfo.getPathKeys(descPath).length>pathInfo.pathKeys.length+1)){include=false}return include};const descRows=[];const addDescendant=(descPath,node)=>{if(!checkExecuted){this.throwImplementationError("descendantsOf did not call checkCallback before addCallback")}if(options.child_objects===false&&[node_value_types_1.VALUE_TYPES.OBJECT,node_value_types_1.VALUE_TYPES.ARRAY].includes(node.type)){return true}applyFiltersOnInlineData(descPath,node);this._processReadNodeValue(node);const row=node;row.path=descPath;descRows.push(row);return true};await transaction.descendantsOf(path,{metadata:true,value:true},includeDescendantCheck,addDescendant);this.logger.info(`Read node "/${path}" and ${filtered?"(filtered) ":""}descendants from ${descRows.length+1} records`.colorize(acebase_core_1.ColorStyle.magenta));const result=targetNode;const objectToArray=obj=>{const arr=[];Object.keys(obj).forEach((key=>{const index=parseInt(key);arr[index]=obj[index]}));return arr};if(targetNode.type===node_value_types_1.VALUE_TYPES.ARRAY){result.value=objectToArray(result.value)}if(targetNode.type===node_value_types_1.VALUE_TYPES.OBJECT||targetNode.type===node_value_types_1.VALUE_TYPES.ARRAY){const targetPathKeys=acebase_core_1.PathInfo.getPathKeys(path);const value=targetNode.value;for(let i=0;i{if(childKey in parent[key]){this.throwImplementationError(`Custom storage merge error: child key "${childKey}" is in parent value already! Make sure the get/childrenOf/descendantsOf methods of the custom storage class return values that can be modified by AceBase without affecting the stored source`)}parent[key][childKey]=nodeValue[childKey]}))}}else{parent[key]=nodeValue}parent=parent[key]}}}else if(descRows.length>0){this.throwImplementationError(`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){(0,assert_1.assert)(Object.keys(result.value[key]).length===0);delete result.value[key]}}))}if(options.include){}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=acebase_core_1.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=acebase_core_1.PathInfo.get(path);const transaction=options.transaction||await this._customImplementation.getTransaction({path:path,write:false});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 node_address_1.NodeAddress(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);(0,assert_1.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&&[node_value_types_1.VALUE_TYPES.OBJECT,node_value_types_1.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.include_child_count){info.childCount=0;if([node_value_types_1.VALUE_TYPES.OBJECT,node_value_types_1.VALUE_TYPES.ARRAY].includes(info.valueType)&&info.address){info.childCount=node.value?Object.keys(node.value).length:0;info.childCount+=await transaction.getChildCount(path)}}if(!options.transaction){await transaction.commit()}return info}catch(err){if(!options.transaction){await transaction.rollback(err)}throw err}}async setNode(path,value,options={suppress_events:false,context:null}){if(this.settings.readOnly){throw new Error(`Database is opened in read-only mode`)}const pathInfo=acebase_core_1.PathInfo.get(path);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,suppress_events:options.suppress_events,context:options.context})}else if(typeof options.assert_revision!=="undefined"){const info=await this.getNodeInfo(path,{transaction:transaction});if(info.revision!==options.assert_revision){throw new node_errors_1.NodeRevisionError(`revision '${info.revision}' does not match requested revision '${options.assert_revision}'`)}if(info.address&&info.address.path===path&&value!==null&&!this.valueFitsInline(value)){await this._writeNodeWithTracking(path,value,{merge:false,transaction:transaction,suppress_events:options.suppress_events,context:options.context})}else{const lockPath=await transaction.moveToParentPath(pathInfo.parentPath);(0,assert_1.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,suppress_events:options.suppress_events,context:options.context})}}else{const lockPath=await transaction.moveToParentPath(pathInfo.parentPath);(0,assert_1.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,suppress_events:options.suppress_events,context:options.context})}if(!options.transaction){await transaction.commit()}}catch(err){if(!options.transaction){await transaction.rollback(err)}throw err}}async updateNode(path,updates,options={suppress_events:false,context:null}){if(this.settings.readOnly){throw new Error(`Database is opened in read-only mode`)}if(typeof updates!=="object"){throw new Error(`invalid updates argument`)}else if(Object.keys(updates).length===0){return}const transaction=options.transaction||await this._customImplementation.getTransaction({path:path,write:true});try{const nodeInfo=await this.getNodeInfo(path,{transaction:transaction});const pathInfo=acebase_core_1.PathInfo.get(path);if(nodeInfo.exists&&nodeInfo.address&&nodeInfo.address.path===path){await this._writeNodeWithTracking(path,updates,{transaction:transaction,merge:true,suppress_events:options.suppress_events,context:options.context})}else if(nodeInfo.exists){const pathInfo=acebase_core_1.PathInfo.get(path);const lockPath=await transaction.moveToParentPath(pathInfo.parentPath);(0,assert_1.assert)(lockPath===pathInfo.parentPath,`transaction.moveToParentPath() did not move to the right parent path of "${path}"`);await this._writeNodeWithTracking(pathInfo.parentPath,{[pathInfo.key]:updates},{transaction:transaction,merge:true,suppress_events:options.suppress_events,context:options.context})}else{const lockPath=await transaction.moveToParentPath(pathInfo.parentPath);(0,assert_1.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,suppress_events:options.suppress_events,context:options.context})}if(!options.transaction){await transaction.commit()}}catch(err){if(!options.transaction){await transaction.rollback(err)}throw err}}}exports.CustomStorage=CustomStorage},{"../../assert":4,"../../node-address":10,"../../node-errors":11,"../../node-info":12,"../../node-lock":13,"../../node-value-types":14,"../index":29,"./helpers":20,"acebase-core":46}],22:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.createIndexedDBInstance=void 0;const acebase_core_1=require("acebase-core");const __1=require("..");const __2=require("../../..");const settings_1=require("./settings");const transaction_1=require("./transaction");function createIndexedDBInstance(dbname,init={}){const settings=new settings_1.IndexedDBStorageSettings(init);const request=indexedDB.open(`${dbname}.acebase`,1);request.onupgradeneeded=e=>{const db=request.result;db.createObjectStore("nodes",{keyPath:"path"});db.createObjectStore("content")};let idb;const readyPromise=new Promise(((resolve,reject)=>{request.onsuccess=e=>{idb=request.result;resolve()};request.onerror=e=>{reject(e)}}));const cache=new acebase_core_1.SimpleCache(typeof settings.cacheSeconds==="number"?settings.cacheSeconds:60);const storageSettings=new __1.CustomStorageSettings({name:"IndexedDB",locking:true,removeVoidProperties:settings.removeVoidProperties,maxInlineValueSize:settings.maxInlineValueSize,lockTimeout:settings.lockTimeout,ready(){return readyPromise},async getTransaction(target){await readyPromise;const context={debug:false,db:idb,cache:cache,ipc:ipc};return new transaction_1.IndexedDBStorageTransaction(context,target)}});const db=new __2.AceBase(dbname,{logLevel:settings.logLevel,storage:storageSettings,sponsor:settings.sponsor});const ipc=db.api.storage.ipc;db.settings.ipcEvents=settings.multipleTabs===true;ipc.on("notification",(async notification=>{const message=notification.data;if(typeof message!=="object"){return}if(message.action==="cache.invalidate"){for(const path of message.paths){cache.remove(path)}}}));return db}exports.createIndexedDBInstance=createIndexedDBInstance},{"..":21,"../../..":6,"./settings":23,"./transaction":24,"acebase-core":46}],23:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.IndexedDBStorageSettings=void 0;const __1=require("../..");class IndexedDBStorageSettings extends __1.StorageSettings{constructor(settings){super(settings);this.multipleTabs=false;this.cacheSeconds=60;this.sponsor=false;if(typeof settings.logLevel==="string"){this.logLevel=settings.logLevel}if(typeof settings.multipleTabs==="boolean"){this.multipleTabs=settings.multipleTabs}if(typeof settings.cacheSeconds==="number"){this.cacheSeconds=settings.cacheSeconds}if(typeof settings.sponsor==="boolean"){this.sponsor=settings.sponsor}["type","ipc","path"].forEach((prop=>{if(prop in settings){console.warn(`${prop} setting is not supported for AceBase IndexedDBStorage`)}}))}}exports.IndexedDBStorageSettings=IndexedDBStorageSettings},{"../..":29}],24:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.IndexedDBStorageTransaction=void 0;const __1=require("..");function _requestToPromise(request){return new Promise(((resolve,reject)=>{request.onsuccess=event=>resolve(request.result||null);request.onerror=reject}))}class IndexedDBStorageTransaction extends __1.CustomStorageTransaction{constructor(context,target){super(target);this.context=context;this.production=true;this._pending=[]}_createTransaction(write=false){const tx=this.context.db.transaction(["nodes","content"],write?"readwrite":"readonly");return tx}_splitMetadata(node){const value=node.value;const copy=Object.assign({},node);delete copy.value;const metadata=copy;return{metadata:metadata,value:value}}async commit(){if(this._pending.length===0){return}const batch=this._pending.splice(0);this.context.ipc.sendNotification({action:"cache.invalidate",paths:batch.map((op=>op.path))});const tx=this._createTransaction(true);try{await new Promise(((resolve,reject)=>{let stop=false,processed=0;const handleError=err=>{stop=true;reject(err)};const handleSuccess=()=>{if(++processed===batch.length){resolve()}};batch.forEach(((op,i)=>{if(stop){return}let r1,r2;const path=op.path;if(op.action==="set"){const{metadata:metadata,value:value}=this._splitMetadata(op.node);const nodeInfo={path:path,metadata:metadata};r1=tx.objectStore("nodes").put(nodeInfo);r2=tx.objectStore("content").put(value,path);this.context.cache.set(path,op.node)}else if(op.action==="remove"){r1=tx.objectStore("content").delete(path);r2=tx.objectStore("nodes").delete(path);this.context.cache.set(path,null)}else{handleError(new Error(`Unknown pending operation "${op.action}" on path "${path}" `))}let succeeded=0;r1.onsuccess=r2.onsuccess=()=>{if(++succeeded===2){handleSuccess()}};r1.onerror=r2.onerror=handleError}))}));tx.commit&&tx.commit()}catch(err){console.error(err);tx.abort&&tx.abort();throw err}}async rollback(err){this._pending=[]}async get(path){if(this.context.cache.has(path)){const cache=this.context.cache.get(path);return cache}const tx=this._createTransaction(false);const r1=_requestToPromise(tx.objectStore("nodes").get(path));const r2=_requestToPromise(tx.objectStore("content").get(path));try{const results=await Promise.all([r1,r2]);tx.commit&&tx.commit();const info=results[0];if(!info){this.context.cache.set(path,null);return null}const node=info.metadata;node.value=results[1];this.context.cache.set(path,node);return node}catch(err){console.error(`IndexedDB get error`,err);tx.abort&&tx.abort();throw err}}set(path,node){this._pending.push({action:"set",path:path,node:node})}remove(path){this._pending.push({action:"remove",path:path})}async removeMultiple(paths){paths.forEach((path=>{this._pending.push({action:"remove",path:path})}))}childrenOf(path,include,checkCallback,addCallback){return this._getChildrenOf(path,Object.assign(Object.assign({},include),{descendants:false}),checkCallback,addCallback)}descendantsOf(path,include,checkCallback,addCallback){return this._getChildrenOf(path,Object.assign(Object.assign({},include),{descendants:true}),checkCallback,addCallback)}_getChildrenOf(path,include,checkCallback,addCallback){return new Promise(((resolve,reject)=>{const pathInfo=__1.CustomStorageHelpers.PathInfo.get(path);const tx=this._createTransaction(false);const store=tx.objectStore("nodes");const query=IDBKeyRange.lowerBound(path,true);const cursor=include.metadata?store.openCursor(query):store.openKeyCursor(query);cursor.onerror=e=>{var _a;(_a=tx.abort)===null||_a===void 0?void 0:_a.call(tx);reject(e)};cursor.onsuccess=async e=>{var _a,_b,_c;const otherPath=(_b=(_a=cursor.result)===null||_a===void 0?void 0:_a.key)!==null&&_b!==void 0?_b:null;let keepGoing=true;if(otherPath===null){keepGoing=false}else if(!pathInfo.isAncestorOf(otherPath)){keepGoing=false}else if(include.descendants||pathInfo.isParentOf(otherPath)){let node;if(include.metadata){const valueCursor=cursor;const data=valueCursor.result.value;node=data.metadata}const shouldAdd=checkCallback(otherPath,node);if(shouldAdd){if(include.value){if(this.context.cache.has(otherPath)){const cache=this.context.cache.get(otherPath);node.value=cache.value}else{const req=tx.objectStore("content").get(otherPath);node.value=await new Promise(((resolve,reject)=>{req.onerror=e=>{resolve(null)};req.onsuccess=e=>{resolve(req.result)}}));this.context.cache.set(otherPath,node.value===null?null:node)}}keepGoing=addCallback(otherPath,node)}}if(keepGoing){try{cursor.result.continue()}catch(err){keepGoing=false}}if(!keepGoing){(_c=tx.commit)===null||_c===void 0?void 0:_c.call(tx);resolve()}}}))}}exports.IndexedDBStorageTransaction=IndexedDBStorageTransaction},{"..":21}],25:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.createLocalStorageInstance=exports.LocalStorageTransaction=exports.LocalStorageSettings=void 0;const __1=require("..");const __2=require("../../..");const settings_1=require("./settings");Object.defineProperty(exports,"LocalStorageSettings",{enumerable:true,get:function(){return settings_1.LocalStorageSettings}});const transaction_1=require("./transaction");Object.defineProperty(exports,"LocalStorageTransaction",{enumerable:true,get:function(){return transaction_1.LocalStorageTransaction}});function createLocalStorageInstance(dbname,init={}){const settings=new settings_1.LocalStorageSettings(init);const ls=settings.provider?settings.provider:settings.temp?localStorage:sessionStorage;const storageSettings=new __1.CustomStorageSettings({name:"LocalStorage",locking:true,removeVoidProperties:settings.removeVoidProperties,maxInlineValueSize:settings.maxInlineValueSize,async ready(){},async getTransaction(target){const context={debug:true,dbname:dbname,localStorage:ls};const transaction=new transaction_1.LocalStorageTransaction(context,target);return transaction}});const db=new __2.AceBase(dbname,{logLevel:settings.logLevel,storage:storageSettings,sponsor:settings.sponsor});db.settings.ipcEvents=settings.multipleTabs===true;return db}exports.createLocalStorageInstance=createLocalStorageInstance},{"..":21,"../../..":6,"./settings":26,"./transaction":27}],26:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.LocalStorageSettings=void 0;const __1=require("../..");class LocalStorageSettings extends __1.StorageSettings{constructor(settings){super(settings);this.temp=false;this.multipleTabs=false;if(typeof settings.temp==="boolean"){this.temp=settings.temp}if(typeof settings.provider==="object"){this.provider=settings.provider}if(typeof settings.multipleTabs==="boolean"){this.multipleTabs=settings.multipleTabs}if(typeof settings.logLevel==="string"){this.logLevel=settings.logLevel}if(typeof settings.sponsor==="boolean"){this.sponsor=settings.sponsor}["type","ipc","path"].forEach((prop=>{if(prop in settings){console.warn(`${prop} setting is not supported for AceBase LocalStorage`)}}))}}exports.LocalStorageSettings=LocalStorageSettings},{"../..":29}],27:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.LocalStorageTransaction=void 0;const __1=require("..");class LocalStorageTransaction extends __1.CustomStorageTransaction{constructor(context,target){super(target);this.context=context;this._storageKeysPrefix=`${this.context.dbname}.acebase::`}async commit(){}async rollback(err){}async get(path){const json=this.context.localStorage.getItem(this.getStorageKeyForPath(path));const val=JSON.parse(json);return val}async set(path,val){const json=JSON.stringify(val);this.context.localStorage.setItem(this.getStorageKeyForPath(path),json)}async remove(path){this.context.localStorage.removeItem(this.getStorageKeyForPath(path))}async childrenOf(path,include,checkCallback,addCallback){const pathInfo=__1.CustomStorageHelpers.PathInfo.get(path);for(let i=0;i`notify_${event}`)));const NOOP=()=>{};class Storage extends acebase_core_1.SimpleEventEmitter{createTid(){return DEBUG_MODE?++this._lastTid:acebase_core_1.ID.generate()}constructor(name,settings,env){var _a;super();this.name=name;this.settings=settings;this._schemas=[];this._indexes=[];this._annoucedIndexes=new Map;this.indexes={get supported(){return index_js_3.pfs===null||index_js_3.pfs===void 0?void 0:index_js_3.pfs.hasFileSystem},create:(path,key,options={rebuild:false})=>{const context={storage:this,logger:this.logger,indexes:this._indexes,ipc:this.ipc};return(0,indexes_js_1.createIndex)(context,path,key,options)},get:(path,key=null)=>{if(path.includes("$")){const pathKeys=acebase_core_1.PathInfo.getPathKeys(path).map((key=>typeof key==="string"&&key.startsWith("$")?"*":key));path=new acebase_core_1.PathInfo(pathKeys).path}return this._indexes.filter((index=>index.path===path&&(key===null||key===index.key)))},getAll:(targetPath,options={parentPaths:true,childPaths:true})=>{const pathKeys=acebase_core_1.PathInfo.getPathKeys(targetPath);return this._indexes.filter((index=>{const indexKeys=acebase_core_1.PathInfo.getPathKeys(index.path+"/*");if(options.parentPaths&&indexKeys.every(((key,i)=>key==="*"||pathKeys[i]===key))&&[index.key].concat(...index.includeKeys).includes(pathKeys[indexKeys.length])){return true}else if(indexKeys.length[key,"*"].includes(indexKeys[i])))}))},list:()=>this._indexes.slice(),load:async()=>{this._indexes.splice(0);if(!index_js_3.pfs.hasFileSystem){return}let files=[];try{files=await index_js_3.pfs.readdir(`${this.settings.path}/${this.name}.acebase`)}catch(err){if(err.code!=="ENOENT"){this.logger.error(err)}}const promises=[];files.forEach((fileName=>{if(!fileName.endsWith(".idx")){return}const needsStoragePrefix=this.settings.type!=="data";const hasStoragePrefix=/^\[[a-z]+\]-/.test(fileName);if(!needsStoragePrefix&&!hasStoragePrefix||needsStoragePrefix&&fileName.startsWith(`[${this.settings.type}]-`)){const p=this.indexes.add(fileName);promises.push(p)}}));await Promise.all(promises)},add:async fileName=>{const existingIndex=this._indexes.find((index=>index.fileName===fileName));if(existingIndex){return existingIndex}else if(this._annoucedIndexes.has(fileName)){const index=await this._annoucedIndexes.get(fileName);return index}try{const indexPromise=index_js_2.DataIndex.readFromFile(this,fileName);this._annoucedIndexes.set(fileName,indexPromise);const index=await indexPromise;this._indexes.push(index);this._annoucedIndexes.delete(fileName);return index}catch(err){this.logger.error(err);return null}},delete:async fileName=>{const index=await this.indexes.remove(fileName);await index.delete();this.ipc.sendNotification({type:"index.deleted",fileName:index.fileName,path:index.path,keys:index.key})},remove:async fileName=>{const index=this._indexes.find((index=>index.fileName===fileName));if(!index){throw new Error(`Index ${fileName} not found`)}this._indexes.splice(this._indexes.indexOf(index),1);return index},close:async()=>{const promises=this.indexes.list().map((index=>index.close().catch((err=>this.logger.error(err)))));await Promise.all(promises)}};this._eventSubscriptions={};this.subscriptions={add:(path,type,callback)=>{if(SUPPORTED_EVENTS.indexOf(type)<0){throw new TypeError(`Invalid event type "${type}"`)}let pathSubs=this._eventSubscriptions[path];if(!pathSubs){pathSubs=this._eventSubscriptions[path]=[]}pathSubs.push({created:Date.now(),type:type,callback:callback});this.emit("subscribe",{path:path,event:type,callback:callback})},remove:(path,type,callback)=>{const pathSubs=this._eventSubscriptions[path];if(!pathSubs){return}const next=()=>pathSubs.findIndex((ps=>(type?ps.type===type:true)&&(callback?ps.callback===callback:true)));let i;while((i=next())>=0){pathSubs.splice(i,1)}this.emit("unsubscribe",{path:path,event:type,callback:callback})},hasValueSubscribersForPath(path){const valueNeeded=this.getValueSubscribersForPath(path);return!!valueNeeded},getValueSubscribersForPath:path=>{const pathInfo=new acebase_core_1.PathInfo(path);const valueSubscribers=[];Object.keys(this._eventSubscriptions).forEach((subscriptionPath=>{if(pathInfo.equals(subscriptionPath)||pathInfo.isDescendantOf(subscriptionPath)){const pathSubs=this._eventSubscriptions[subscriptionPath];const eventPath=acebase_core_1.PathInfo.fillVariables(subscriptionPath,path);pathSubs.filter((sub=>!sub.type.startsWith("notify_"))).forEach((sub=>{let dataPath=null;if(sub.type==="value"){dataPath=eventPath}else if(["mutated","mutations"].includes(sub.type)&&pathInfo.isDescendantOf(eventPath)){dataPath=path}else if(sub.type==="child_changed"&&path!==eventPath){const childKey=acebase_core_1.PathInfo.getPathKeys(path.slice(eventPath.length).replace(/^\//,""))[0];dataPath=acebase_core_1.PathInfo.getChildPath(eventPath,childKey)}else if(["child_added","child_removed"].includes(sub.type)&&pathInfo.isChildOf(eventPath)){const childKey=acebase_core_1.PathInfo.getPathKeys(path.slice(eventPath.length).replace(/^\//,""))[0];dataPath=acebase_core_1.PathInfo.getChildPath(eventPath,childKey)}if(dataPath!==null&&!valueSubscribers.some((s=>s.type===sub.type&&s.eventPath===eventPath))){valueSubscribers.push({type:sub.type,eventPath:eventPath,dataPath:dataPath,subscriptionPath:subscriptionPath})}}))}}));return valueSubscribers},getAllSubscribersForPath:path=>{const pathInfo=acebase_core_1.PathInfo.get(path);const subscribers=[];Object.keys(this._eventSubscriptions).forEach((subscriptionPath=>{if(pathInfo.isOnTrailOf(subscriptionPath)){const pathSubs=this._eventSubscriptions[subscriptionPath];const eventPath=acebase_core_1.PathInfo.fillVariables(subscriptionPath,path);pathSubs.forEach((sub=>{let dataPath=null;if(sub.type==="value"||sub.type==="notify_value"){dataPath=eventPath}else if(["child_changed","notify_child_changed"].includes(sub.type)){const childKey=path===eventPath||pathInfo.isAncestorOf(eventPath)?"*":acebase_core_1.PathInfo.getPathKeys(path.slice(eventPath.length).replace(/^\//,""))[0];dataPath=acebase_core_1.PathInfo.getChildPath(eventPath,childKey)}else if(["mutated","mutations","notify_mutated","notify_mutations"].includes(sub.type)){dataPath=path}else if(["child_added","child_removed","notify_child_added","notify_child_removed"].includes(sub.type)&&(pathInfo.isChildOf(eventPath)||path===eventPath||pathInfo.isAncestorOf(eventPath))){const childKey=path===eventPath||pathInfo.isAncestorOf(eventPath)?"*":acebase_core_1.PathInfo.getPathKeys(path.slice(eventPath.length).replace(/^\//,""))[0];dataPath=acebase_core_1.PathInfo.getChildPath(eventPath,childKey)}if(dataPath!==null&&!subscribers.some((s=>s.type===sub.type&&s.eventPath===eventPath&&s.subscriptionPath===subscriptionPath))){subscribers.push({type:sub.type,eventPath:eventPath,dataPath:dataPath,subscriptionPath:subscriptionPath})}}))}}));return subscribers},trigger:(event,path,dataPath,oldValue,newValue,context)=>{const pathSubscriptions=this._eventSubscriptions[path]||[];pathSubscriptions.filter((sub=>sub.type===event)).forEach((sub=>{sub.callback(null,dataPath,newValue,oldValue,context)}))}};this.logger=(_a=env.logger)!==null&&_a!==void 0?_a:new acebase_core_1.DebugLogger(env.logLevel,`[${name}${typeof settings.type==="string"&&settings.type!=="data"?`:${settings.type}`:""}]`);const ipcName=name+(typeof settings.type==="string"?`_${settings.type}`:"");const ipcSocketSettings=typeof settings.ipc==="object"&&settings.ipc!==null&&"role"in settings.ipc&&settings.ipc.role==="socket"?settings.ipc:null;if(ipcSocketSettings||settings.ipc==="socket"||settings.ipc instanceof index_js_1.NetIPCServer){const ipcSettings=Object.assign({ipcName:ipcName,server:settings.ipc instanceof index_js_1.NetIPCServer?settings.ipc:null},ipcSocketSettings&&{maxIdleTime:ipcSocketSettings.maxIdleTime,loggerPluginPath:ipcSocketSettings.loggerPluginPath});this.ipc=new index_js_1.IPCSocketPeer(this,ipcSettings)}else if(settings.ipc){const ipcClientSettings=settings.ipc;if(typeof ipcClientSettings.port!=="number"){throw new Error("IPC port number must be a number")}if(!["master","worker"].includes(ipcClientSettings.role)){throw new Error(`IPC client role must be either "master" or "worker", not "${ipcClientSettings.role}"`)}const ipcSettings=Object.assign({dbname:ipcName},ipcClientSettings);this.ipc=new index_js_1.RemoteIPCPeer(this,ipcSettings)}else{this.ipc=new index_js_1.IPCPeer(this,ipcName)}this.ipc.once("exit",(code=>{if(this.indexes.supported){this.indexes.close()}}));this.nodeLocker={lock:async(path,tid,write,comment)=>{const lock=await this.ipc.lock({path:path,tid:tid,write:write,comment:comment});return lock}};this._lastTid=0}async close(){await this.ipc.exit()}get path(){return`${this.settings.path}/${this.name}.acebase`}valueFitsInline(value){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;const eventPaths=valueSubscribers.map((sub=>({path:sub.dataPath,keys:acebase_core_1.PathInfo.getPathKeys(sub.dataPath)}))).sort(((a,b)=>{if(a.keys.lengthb.keys.length){return 1}return 0}));const first=eventPaths[0];topEventPath=first.path;if(valueSubscribers.filter((sub=>sub.dataPath===topEventPath)).every((sub=>sub.type==="mutated"||sub.type.startsWith("notify_")))){hasValueSubscribers=false}topEventPath=acebase_core_1.PathInfo.fillVariables(topEventPath,path)}const indexes=this.indexes.getAll(path,{childPaths:true,parentPaths:true}).map((index=>({index:index,keys:acebase_core_1.PathInfo.getPathKeys(index.path)}))).sort(((a,b)=>{if(a.keys.lengthb.keys.length){return 1}return 0})).map((obj=>obj.index));const keysFilter=[];if(indexes.length>0){indexes.sort(((a,b)=>{if(typeof a._pathKeys==="undefined"){a._pathKeys=acebase_core_1.PathInfo.getPathKeys(a.path)}if(typeof b._pathKeys==="undefined"){b._pathKeys=acebase_core_1.PathInfo.getPathKeys(b.path)}if(a._pathKeys.lengthb._pathKeys.length){return 1}return 0}));const topIndex=indexes[0];const topIndexPath=topIndex.path===path?path:acebase_core_1.PathInfo.fillVariables(`${topIndex.path}/*`,path);if(topIndexPath.lengthindex.path===topIndex.path)).forEach((index=>{const keys=[index.key].concat(index.includeKeys);keys.forEach((key=>!keysFilter.includes(key)&&keysFilter.push(key)))}))}}return{topEventPath:topEventPath,eventSubscriptions:eventSubscriptions,valueSubscribers:valueSubscribers,hasValueSubscribers:hasValueSubscribers,indexes:indexes,keysFilter:keysFilter}}async _writeNodeWithTracking(path,value,options={merge:false,waitForIndexUpdates:true,suppress_events:false,context:null,impact:null}){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 validation=this.validateSchema(path,value,{updates:options.merge});if(!validation.ok){throw new errors_js_1.SchemaValidationError(validation.reason)}const tid=options.tid;const transaction=options.transaction;let topEventData=null;const updateImpact=options.impact?options.impact:this.getUpdateImpact(path,options.suppress_events);const{topEventPath:topEventPath,eventSubscriptions:eventSubscriptions,hasValueSubscribers:hasValueSubscribers,indexes:indexes}=updateImpact;let{keysFilter:keysFilter}=updateImpact;const writeNode=()=>{if(typeof options._customWriteFunction==="function"){return options._customWriteFunction()}if(topEventData){const pathKeys=acebase_core_1.PathInfo.getPathKeys(path);const eventPathKeys=acebase_core_1.PathInfo.getPathKeys(topEventPath);const trailKeys=pathKeys.slice(eventPathKeys.length);let currentValue=topEventData;while(trailKeys.length>0&¤tValue!==null){const childKey=trailKeys.shift();currentValue=typeof currentValue==="object"&&childKey in currentValue?currentValue[childKey]:null}options.currentValue=currentValue}return this._writeNode(path,value,options)};const transactionLoggingEnabled=this.settings.transactions&&this.settings.transactions.log===true;if(eventSubscriptions.length===0&&indexes.length===0&&!transactionLoggingEnabled){return writeNode()}if(!hasValueSubscribers&&options.merge===true&&keysFilter.length===0){keysFilter=Object.keys(value);if(topEventPath!==path){const trailPath=path.slice(topEventPath.length);keysFilter=keysFilter.map((key=>`${trailPath}/${key}`))}}const eventNodeInfo=await this.getNodeInfo(topEventPath,{transaction:transaction,tid:tid});let currentValue=null;if(eventNodeInfo.exists){const valueOptions={transaction:transaction,tid:tid};if(keysFilter.length>0){valueOptions.include=keysFilter}if(topEventPath===""&&typeof valueOptions.include==="undefined"){this.logger.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. Use "mutated" or "mutations" events instead')}const node=await this.getNode(topEventPath,valueOptions);currentValue=node.value}topEventData=currentValue;const result=await writeNode()||{};let newTopEventData,modifiedData;if(path===topEventPath){if(options.merge){if(topEventData===null){newTopEventData=value instanceof Array?[]:{}}else{newTopEventData=topEventData instanceof Array?[]:{};Object.keys(topEventData).forEach((key=>{newTopEventData[key]=topEventData[key]}))}}else{newTopEventData=value}modifiedData=newTopEventData}else{const trailPath=path.slice(topEventPath.length).replace(/^\//,"");const trailKeys=acebase_core_1.PathInfo.getPathKeys(trailPath);if(topEventData===null){newTopEventData=typeof trailKeys[0]==="number"?[]:{}}else{newTopEventData=topEventData instanceof Array?[]:{};Object.keys(topEventData).forEach((key=>{newTopEventData[key]=topEventData[key]}))}modifiedData=newTopEventData;while(trailKeys.length>0){const childKey=trailKeys.shift();if(!options.merge&&trailKeys.length===0){modifiedData[childKey]=value}else{const original=modifiedData[childKey];const shallowCopy=typeof childKey==="number"?[...original]:Object.assign({},original);modifiedData[childKey]=shallowCopy}modifiedData=modifiedData[childKey]}}if(options.merge){Object.keys(value).forEach((key=>{modifiedData[key]=value[key]}))}const dataChanges=compareValues(topEventData,newTopEventData);if(dataChanges==="identical"){result.mutations=[];return result}function removeNulls(obj){if(obj===null||typeof obj!=="object"){return obj}Object.keys(obj).forEach((prop=>{const val=obj[prop];if(val===null){delete obj[prop];if(obj instanceof Array){obj.length--}}if(typeof val==="object"){removeNulls(val)}}))}removeNulls(newTopEventData);const indexUpdates=[];indexes.map((index=>({index:index,keys:acebase_core_1.PathInfo.getPathKeys(index.path)}))).sort(((a,b)=>{if(a.keys.lengthb.keys.length){return-1}return 0})).forEach((({index:index})=>{const pathKeys=acebase_core_1.PathInfo.getPathKeys(topEventPath);const indexPathKeys=acebase_core_1.PathInfo.getPathKeys(index.path+"/*");const trailKeys=indexPathKeys.slice(pathKeys.length);const oldValue=topEventData;const newValue=newTopEventData;if(trailKeys.length===0){(0,assert_js_1.assert)(pathKeys.length===indexPathKeys.length,"check logic");const p=this.ipc.isMaster?index.handleRecordUpdate(topEventPath,oldValue,newValue):this.ipc.sendRequest({type:"index.update",fileName:index.fileName,path:topEventPath,oldValue:oldValue,newValue:newValue});indexUpdates.push(p);return}const getAllIndexUpdates=(path,oldValue,newValue)=>{if(oldValue===null&&newValue===null){return[]}const pathKeys=acebase_core_1.PathInfo.getPathKeys(path);const indexPathKeys=acebase_core_1.PathInfo.getPathKeys(index.path+"/*");const trailKeys=indexPathKeys.slice(pathKeys.length);if(trailKeys.length===0){(0,assert_js_1.assert)(pathKeys.length===indexPathKeys.length,"check logic");return[{path:path,oldValue:oldValue,newValue:newValue}]}let results=[];let trailPath="";while(trailKeys.length>0){const subKey=trailKeys.shift();if(typeof subKey==="string"&&(subKey==="*"||subKey.startsWith("$"))){const 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 childPath=acebase_core_1.PathInfo.getChildPath(trailPath,key);const childValues=getChildValues(key,oldValue,newValue);const subTrailPath=acebase_core_1.PathInfo.getChildPath(path,childPath);const childResults=getAllIndexUpdates(subTrailPath,childValues.oldValue,childValues.newValue);results=results.concat(childResults)}));break}else{const values=getChildValues(subKey,oldValue,newValue);oldValue=values.oldValue;newValue=values.newValue;if(oldValue===null&&newValue===null){break}trailPath=acebase_core_1.PathInfo.getChildPath(trailPath,subKey)}}return results};const results=getAllIndexUpdates(topEventPath,oldValue,newValue);results.forEach((result=>{const p=this.ipc.isMaster?index.handleRecordUpdate(result.path,result.oldValue,result.newValue):this.ipc.sendRequest({type:"index.update",fileName:index.fileName,path:result.path,oldValue:result.oldValue,newValue: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==="mutated"){return}else if(type==="child_changed"&&(oldValue===null||newValue===null)){trigger=false}else if(type==="value"||type==="child_changed"){const 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}if(!trigger){return}const pathKeys=acebase_core_1.PathInfo.getPathKeys(sub.dataPath);variables.forEach((variable=>{const index=pathKeys.indexOf(variable.name);(0,assert_js_1.assert)(index>=0,`Variable "${variable.name}" not found in subscription dataPath "${sub.dataPath}"`);pathKeys[index]=variable.value}));const dataPath=pathKeys.reduce(((path,key)=>acebase_core_1.PathInfo.getChildPath(path,key)),"");this.subscriptions.trigger(sub.type,sub.subscriptionPath,dataPath,oldValue,newValue,options.context)};const prepareMutationEvents=(currentPath,oldValue,newValue,compareResult)=>{const batch=[];const result=compareResult||compareValues(oldValue,newValue);if(result==="identical"){return batch}else if(typeof result==="string"){batch.push({path:currentPath,oldValue:oldValue,newValue:newValue})}else{result.changed.forEach((info=>{const childPath=acebase_core_1.PathInfo.getChildPath(currentPath,info.key);const childValues=getChildValues(info.key,oldValue,newValue);const childBatch=prepareMutationEvents(childPath,childValues.oldValue,childValues.newValue,info.change);batch.push(...childBatch)}));result.added.forEach((key=>{const childPath=acebase_core_1.PathInfo.getChildPath(currentPath,key);batch.push({path:childPath,oldValue:null,newValue:newValue[key]})}));if(oldValue instanceof Array&&newValue instanceof Array){result.removed.sort(((a,b)=>a{const childPath=acebase_core_1.PathInfo.getChildPath(currentPath,key);batch.push({path:childPath,oldValue:oldValue[key],newValue:null})}))}return batch};if(transactionLoggingEnabled&&this.settings.type!=="transaction"){result.mutations=(()=>{const trailPath=path.slice(topEventPath.length).replace(/^\//,"");const trailKeys=acebase_core_1.PathInfo.getPathKeys(trailPath);let oldValue=topEventData,newValue=newTopEventData;while(trailKeys.length>0){const key=trailKeys.shift();({oldValue:oldValue,newValue:newValue}=getChildValues(key,oldValue,newValue))}const compareResults=compareValues(oldValue,newValue);const batch=prepareMutationEvents(path,oldValue,newValue,compareResults);const mutations=batch.map((m=>({target:acebase_core_1.PathInfo.getPathKeys(m.path.slice(path.length)),prev:m.oldValue,val:m.newValue})));return mutations})()}const triggerAllEvents=()=>{eventSubscriptions.filter((sub=>!["mutated","mutations","notify_mutated","notify_mutations"].includes(sub.type))).map((sub=>{const keys=acebase_core_1.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=[])=>{const trailPath=sub.dataPath.slice(currentPath.length).replace(/^\//,"");const trailKeys=acebase_core_1.PathInfo.getPathKeys(trailPath);while(trailKeys.length>0){const subKey=trailKeys.shift();if(typeof subKey==="string"&&(subKey==="*"||subKey[0]==="$")){const allKeys=oldValue===null?[]:Object.keys(oldValue).map((key=>oldValue instanceof Array?parseInt(key):key));newValue!==null&&Object.keys(newValue).forEach((key=>{const keyOrIndex=newValue instanceof Array?parseInt(key):key;!allKeys.includes(keyOrIndex)&&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(acebase_core_1.PathInfo.getChildPath(currentPath,subKey),childValues.oldValue,childValues.newValue,vars)}}));return}else{currentPath=acebase_core_1.PathInfo.getChildPath(currentPath,subKey);const childValues=getChildValues(subKey,oldValue,newValue);oldValue=childValues.oldValue;newValue=childValues.newValue}}callSubscriberWithValues(sub,oldValue,newValue,variables)};if(sub.type.startsWith("notify_")&&acebase_core_1.PathInfo.get(sub.eventPath).isAncestorOf(topEventPath)){const isOnParentPath=acebase_core_1.PathInfo.get(sub.eventPath).isParentOf(topEventPath);const trigger=sub.type==="notify_value"||sub.type==="notify_child_changed"&&(!isOnParentPath||!["added","removed"].includes(dataChanges))||sub.type==="notify_child_removed"&&dataChanges==="removed"&&isOnParentPath||sub.type==="notify_child_added"&&dataChanges==="added"&&isOnParentPath;trigger&&this.subscriptions.trigger(sub.type,sub.subscriptionPath,sub.dataPath,null,null,options.context)}else{process(topEventPath,topEventData,newTopEventData)}}));const mutationEvents=eventSubscriptions.filter((sub=>["mutated","mutations","notify_mutated","notify_mutations"].includes(sub.type)));mutationEvents.forEach((sub=>{const currentPath=topEventPath;const trailKeys=acebase_core_1.PathInfo.getPathKeys(sub.eventPath).slice(acebase_core_1.PathInfo.getPathKeys(currentPath).length);const events=[];const oldValue=topEventData;const newValue=newTopEventData;const processNextTrailKey=(target,currentTarget,oldValue,newValue,vars)=>{if(target.length===0){return events.push({target:currentTarget,oldValue:oldValue,newValue:newValue,vars:vars})}const subKey=target[0];const keys=new Set;const isWildcardKey=typeof subKey==="string"&&(subKey==="*"||subKey.startsWith("$"));if(isWildcardKey){if(oldValue!==null&&typeof oldValue==="object"){Object.keys(oldValue).forEach((key=>keys.add(key)))}if(newValue!==null&&typeof newValue==="object"){Object.keys(newValue).forEach((key=>keys.add(key)))}}else{keys.add(subKey)}for(const key of keys){const childValues=getChildValues(key,oldValue,newValue);oldValue=childValues.oldValue;newValue=childValues.newValue;processNextTrailKey(target.slice(1),currentTarget.concat(key),oldValue,newValue,isWildcardKey?vars.concat({name:subKey,value:key}):vars)}};processNextTrailKey(trailKeys,[],oldValue,newValue,[]);for(const event of events){const targetPath=acebase_core_1.PathInfo.get(currentPath).child(event.target).path;const batch=prepareMutationEvents(targetPath,event.oldValue,event.newValue);if(batch.length===0){continue}const isNotifyEvent=sub.type.startsWith("notify_");if(["mutated","notify_mutated"].includes(sub.type)){batch.forEach(((mutation,index)=>{const context=options.context;const prevVal=isNotifyEvent?null:mutation.oldValue;const newVal=isNotifyEvent?null:mutation.newValue;this.subscriptions.trigger(sub.type,sub.subscriptionPath,mutation.path,prevVal,newVal,context)}))}else if(["mutations","notify_mutations"].includes(sub.type)){const subscriptionPathKeys=acebase_core_1.PathInfo.getPathKeys(sub.subscriptionPath);const values=isNotifyEvent?null:batch.map((m=>({target:acebase_core_1.PathInfo.getPathKeys(m.path).slice(subscriptionPathKeys.length),prev:m.oldValue,val:m.newValue})));const dataPath=acebase_core_1.PathInfo.get(acebase_core_1.PathInfo.getPathKeys(targetPath).slice(0,subscriptionPathKeys.length)).path;this.subscriptions.trigger(sub.type,sub.subscriptionPath,dataPath,null,values,options.context)}}}))};if(options.waitForIndexUpdates===false){indexUpdates.splice(0)}await Promise.all(indexUpdates);defer(triggerAllEvents);return result}getChildren(path,options){throw new Error("This method must be implemented by subclass")}async getNodeValue(path,options={}){const node=await this.getNode(path,options);return node.value}getNode(path,options){throw new Error("This method must be implemented by subclass")}getNodeInfo(path,options){throw new Error("This method must be implemented by subclass")}setNode(path,value,options){throw new Error("This method must be implemented by subclass")}updateNode(path,updates,options){throw new Error("This method must be implemented by subclass")}async transactNode(path,callback,options={no_lock:false,suppress_events:false,context:null}){const useFakeLock=options&&options.no_lock===true;const tid=this.createTid();const lock=useFakeLock?{tid:tid,release:NOOP}:await this.nodeLocker.lock(path,tid,true,"transactNode");try{let changed=false;const changeCallback=()=>{changed=true};if(useFakeLock){this.subscriptions.add(path,"notify_value",changeCallback)}const node=await this.getNode(path,{tid:tid});const checkRevision=node.revision;let newValue;try{newValue=callback(node.value);if(newValue instanceof Promise){newValue=await newValue.catch((err=>{this.logger.error(`Error in transaction callback: ${err.message}`)}))}}catch(err){this.logger.error(`Error in transaction callback: ${err.message}`)}if(typeof newValue==="undefined"){return}if(useFakeLock){this.subscriptions.remove(path,"notify_value",changeCallback)}if(changed){throw new node_errors_js_1.NodeRevisionError("Node changed")}const cursor=await this.setNode(path,newValue,{assert_revision:checkRevision,tid:lock.tid,suppress_events:options.suppress_events,context:options.context});return cursor}catch(err){if(err instanceof node_errors_js_1.NodeRevisionError){console.warn(`node value changed, running again. Error: ${err.message}`);return this.transactNode(path,callback,options)}else{throw err}}finally{lock.release()}}async matchNode(path,criteria,options){var _a;const tid=(_a=options===null||options===void 0?void 0:options.tid)!==null&&_a!==void 0?_a:acebase_core_1.ID.generate();const checkNode=async(path,criteria)=>{if(criteria.length===0){return Promise.resolve(true)}const criteriaKeys=criteria.reduce(((keys,cr)=>{let key=cr.key;if(typeof key==="string"&&key.includes("/")){key=key.slice(0,key.indexOf("/"))}if(keys.indexOf(key)<0){keys.push(key)}return keys}),[]);const unseenKeys=criteriaKeys.slice();let isMatch=true;const delayedMatchPromises=[];try{await this.getChildren(path,{tid:tid,keyFilter:criteriaKeys}).next((childInfo=>{var _a;const keyOrIndex=(_a=childInfo.key)!==null&&_a!==void 0?_a:childInfo.index;unseenKeys.includes(keyOrIndex)&&unseenKeys.splice(unseenKeys.indexOf(childInfo.key),1);const keyCriteria=criteria.filter((cr=>cr.key===keyOrIndex)).map((cr=>({op:cr.op,compare:cr.compare})));const keyResult=keyCriteria.length>0?checkChild(childInfo,keyCriteria):{isMatch:true,promises:[]};isMatch=keyResult.isMatch;if(isMatch){delayedMatchPromises.push(...keyResult.promises);const childCriteria=criteria.filter((cr=>typeof cr.key==="string"&&cr.key.startsWith(`${typeof keyOrIndex==="number"?`[${keyOrIndex}]`:keyOrIndex}/`))).map((cr=>{const key=cr.key.slice(cr.key.indexOf("/")+1);return{key:key,op:cr.op,compare:cr.compare}}));if(childCriteria.length>0){const childPath=acebase_core_1.PathInfo.getChildPath(path,childInfo.key);const childPromise=checkNode(childPath,childCriteria).then((isMatch=>({isMatch:isMatch})));delayedMatchPromises.push(childPromise)}}if(!isMatch||unseenKeys.length===0){return false}}));if(isMatch){const results=await Promise.all(delayedMatchPromises);isMatch=results.every((res=>res.isMatch))}if(!isMatch){return false}isMatch=unseenKeys.every((keyOrIndex=>{const childInfo=new node_info_js_1.NodeInfo(Object.assign(Object.assign(Object.assign({},typeof keyOrIndex==="number"&&{index:keyOrIndex}),typeof keyOrIndex==="string"&&{key:keyOrIndex}),{exists:false}));const childCriteria=criteria.filter((cr=>typeof cr.key==="string"&&cr.key.startsWith(`${typeof keyOrIndex==="number"?`[${keyOrIndex}]`:keyOrIndex}/`))).map((cr=>({op:cr.op,compare:cr.compare})));if(childCriteria.length>0&&!checkChild(childInfo,childCriteria).isMatch){return false}const keyCriteria=criteria.filter((cr=>cr.key===keyOrIndex)).map((cr=>({op:cr.op,compare:cr.compare})));if(keyCriteria.length===0){return true}const result=checkChild(childInfo,keyCriteria);return result.isMatch}));return isMatch}catch(err){this.logger.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((f.op==="contains"||f.op==="!contains")&&f.compare instanceof Array&&f.compare.length===0){proceed=true}else if(!child.exists){proceed=false}else{if(child.address){if(child.valueType===node_value_types_js_1.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=>({key:child.key,isMatch:isMatch})));promises.push(p);proceed=true}else if(child.valueType===node_value_types_js_1.VALUE_TYPES.ARRAY&&["contains","!contains"].indexOf(f.op)>=0){const p=this.getNode(child.path,{tid:tid}).then((({value: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===node_value_types_js_1.VALUE_TYPES.STRING){const p=this.getNode(child.path,{tid:tid}).then((node=>({key:child.key,isMatch:this.test(node.value,f.op,f.compare)})));promises.push(p);proceed=true}else{proceed=false}}else if(child.type===node_value_types_js_1.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===node_value_types_js_1.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{let 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}async exportNode(path,writeFn,options={format:"json",type_safe:true}){if((options===null||options===void 0?void 0:options.format)!=="json"){throw new Error("Only json output is currently supported")}const write=typeof writeFn!=="function"?writeFn.write.bind(writeFn):writeFn;const stringifyValue=(type,val)=>{const escape=str=>str.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/\r/g,"\\r").replace(/\n/g,"\\n").replace(/\t/g,"\\t").replace(/[\u0000-\u001f]/g,(ch=>`\\u${ch.charCodeAt(0).toString(16).padStart(4,"0")}`));if(type===node_value_types_js_1.VALUE_TYPES.DATETIME){val=`"${val.toISOString()}"`;if(options.type_safe){val=`{".type":"date",".val":${val}}`}}else if(type===node_value_types_js_1.VALUE_TYPES.STRING){val=`"${escape(val)}"`}else if(type===node_value_types_js_1.VALUE_TYPES.ARRAY){val="[]"}else if(type===node_value_types_js_1.VALUE_TYPES.OBJECT){val="{}"}else if(type===node_value_types_js_1.VALUE_TYPES.BINARY){val=`"${escape(acebase_core_1.ascii85.encode(val))}"`;if(options.type_safe){val=`{".type":"binary",".val":${val}}`}}else if(type===node_value_types_js_1.VALUE_TYPES.REFERENCE){val=`"${val.path}"`;if(options.type_safe){val=`{".type":"reference",".val":${val}}`}}else if(type===node_value_types_js_1.VALUE_TYPES.BIGINT){val=`"${val}"`;if(options.type_safe){val=`{".type":"bigint",".val":${val}}`}}return val};let objStart="",objEnd="";const nodeInfo=await this.getNodeInfo(path);if(!nodeInfo.exists){return write("null")}else if(nodeInfo.type===node_value_types_js_1.VALUE_TYPES.OBJECT){objStart="{";objEnd="}"}else if(nodeInfo.type===node_value_types_js_1.VALUE_TYPES.ARRAY){objStart="[";objEnd="]"}else{const node=await this.getNode(path);const val=stringifyValue(nodeInfo.type,node.value);return write(val)}if(objStart){const p=write(objStart);if(p instanceof Promise){await p}}let output="",outputCount=0;const pending=[];await this.getChildren(path).next((childInfo=>{if(childInfo.address){pending.push(childInfo)}else{if(outputCount++>0){output+=","}if(typeof childInfo.key==="string"){output+=`"${childInfo.key}":`}output+=stringifyValue(childInfo.type,childInfo.value)}}));if(output){const p=write(output);if(p instanceof Promise){await p}}while(pending.length>0){const childInfo=pending.shift();let output=outputCount++>0?",":"";const key=typeof childInfo.index==="number"?childInfo.index:childInfo.key;if(typeof key==="string"){output+=`"${key}":`}if(output){const p=write(output);if(p instanceof Promise){await p}}await this.exportNode(acebase_core_1.PathInfo.getChildPath(path,key),write,options)}if(objEnd){const p=write(objEnd);if(p instanceof Promise){await p}}}async importNode(path,read,options={format:"json",method:"set"}){const chunkSize=256*1024;const maxQueueBytes=1024*1024;const state={data:"",index:0,offset:0,queue:[],queueStartByte:0,timesFlushed:0,get processedBytes(){return this.offset+this.index}};const readNextChunk=async(append=false)=>{let data=await read(chunkSize);if(data===null){if(state.data){throw new Error(`Unexpected EOF at index ${state.offset+state.data.length}`)}else{throw new Error("Unable to read data from stream")}}else if(typeof data==="object"){data=acebase_core_1.Utils.decodeString(data)}if(append){state.data+=data}else{state.offset+=state.data.length;state.data=data;state.index=0}};const readBytes=async length=>{let str="";if(state.index+length>=state.data.length){str=state.data.slice(state.index);length-=str.length;await readNextChunk()}str+=state.data.slice(state.index,state.index+length);state.index+=length;return str};const assertBytes=async length=>{if(state.index+length>state.data.length){await readNextChunk(true)}if(state.index+length>state.data.length){throw new Error("Not enough data available from stream")}};const consumeToken=async token=>{const str=await readBytes(token.length);if(str!==token){throw new Error(`Unexpected character "${str[0]}" at index ${state.offset+state.index}, expected "${token}"`)}};const consumeSpaces=async()=>{const spaces=[" ","\t","\r","\n"];while(true){if(state.index>=state.data.length){await readNextChunk()}if(spaces.includes(state.data[state.index])){state.index++}else{break}}};const peekBytes=async length=>{await assertBytes(length);const index=state.index;return state.data.slice(index,index+length)};const peekValueType=async()=>{await consumeSpaces();const ch=await peekBytes(1);switch(ch){case'"':return"string";case"{":return"object";case"[":return"array";case"n":return"null";case"u":return"undefined";case"t":case"f":return"boolean";default:{if(ch==="-"||ch>="0"&&ch<="9"){return"number"}throw new Error(`Unknown value at index ${state.offset+state.index}`)}}};const readString=async()=>{await consumeToken('"');let str="";let i=state.index;while(state.data[i]!=='"'||state.data[i-1]==="\\"){i++;if(i>=state.data.length){str+=state.data.slice(state.index);await readNextChunk();i=0}}str+=state.data.slice(state.index,i);state.index=i+1;return unescape(str)};const readBoolean=async()=>{if(state.data[state.index]==="t"){await consumeToken("true")}else if(state.data[state.index]==="f"){await consumeToken("false")}throw new Error(`Expected true or false at index ${state.offset+state.index}`)};const readNumber=async()=>{let str="";let i=state.index;const nrChars=["-","0","1","2","3","4","5","6","7","8","9",".","e","b","f","x","o","n"];while(nrChars.includes(state.data[i])){i++;if(i>=state.data.length){str+=state.data.slice(state.index);await readNextChunk();i=0}}str+=state.data.slice(state.index,i);state.index=i;const nr=str.endsWith("n")?BigInt(str.slice(0,-1)):str.includes(".")?parseFloat(str):parseInt(str);return nr};const readValue=async()=>{await consumeSpaces();const type=await peekValueType();const value=await(()=>{switch(type){case"string":return readString();case"object":return{};case"array":return[];case"number":return readNumber();case"null":return null;case"undefined":return undefined;case"boolean":return readBoolean()}})();return{type:type,value:value}};const unescape=str=>str.replace(/\\n/g,"\n").replace(/\\"/g,'"');const getTypeSafeValue=(path,obj)=>{const type=obj[".type"];let val=obj[".val"];switch(type){case"Date":case"date":{val=new Date(val);break}case"Buffer":case"binary":{val=unescape(val);if(val.startsWith("<~")){val=acebase_core_1.ascii85.decode(val)}else{throw new Error(`Import error: Unexpected encoding for value for value at path "/${path}"`)}break}case"PathReference":case"reference":{val=new acebase_core_1.PathReference(val);break}case"bigint":{val=BigInt(val);break}default:throw new Error(`Import error: Unsupported type "${type}" for value at path "/${path}"`)}return val};const context={acebase_import_id:acebase_core_1.ID.generate()};const childOptions={suppress_events:options.suppress_events,context:context};const enqueue=async(target,value)=>{state.queue.push({target:target,value:value});if(state.processedBytes>=state.queueStartByte+maxQueueBytes){const operations=state.queue.reduce(((updates,item)=>{if(item.target.path===path){updates.push(Object.assign({op:options.method==="set"&&state.timesFlushed===0?"set":"update"},item))}else{const parent=updates.find((other=>other.target.isParentOf(item.target)));if(parent){parent.value[item.target.key]=item.value}else{updates.push(Object.assign({op:options.method==="merge"?"update":"set"},item))}}return updates}),[]);state.queueStartByte=state.processedBytes;state.queue=[];state.timesFlushed++}if(target.path===path){}};const importObject=async target=>{await consumeToken("{");await consumeSpaces();const nextChar=await peekBytes(1);if(nextChar==="}"){state.index++;return this.setNode(target.path,{},childOptions)}let childCount=0;let obj={};let flushedBefore=false;const flushObject=async()=>{let p;if(!flushedBefore){flushedBefore=true;p=this.setNode(target.path,obj,childOptions)}else if(Object.keys(obj).length>0){p=this.updateNode(target.path,obj,childOptions)}obj={};if(p){await p}};const promises=[];while(true){await consumeSpaces();const property=await readString();await consumeSpaces();await consumeToken(":");await consumeSpaces();const{value:value,type:type}=await readValue();obj[property]=value;childCount++;if(["object","array"].includes(type)){promises.push(flushObject());if(type==="object"){await importObject(target.child(property))}else{await importArray(target.child(property))}}await consumeSpaces();const nextChar=await peekBytes(1);if(nextChar==="}"){state.index++;break}await consumeToken(",")}const isTypedValue=childCount===2&&".type"in obj&&".val"in obj;if(isTypedValue){const val=getTypeSafeValue(target.path,obj);return this.setNode(target.path,val,childOptions)}promises.push(flushObject());await Promise.all(promises)};const importArray=async target=>{await consumeToken("[");await consumeSpaces();const nextChar=await peekBytes(1);if(nextChar==="]"){state.index++;return this.setNode(target.path,[],childOptions)}let flushedBefore=false;let arr=[];let updates={};const flushArray=async()=>{let p;if(!flushedBefore){flushedBefore=true;p=this.setNode(target.path,arr,childOptions);arr=null}else if(Object.keys(updates).length>0){p=this.updateNode(target.path,updates,childOptions);updates={}}if(p){await p}};const pushChild=(value,index)=>{if(flushedBefore){updates[index]=value}else{arr.push(value)}};const promises=[];let index=0;while(true){await consumeSpaces();const{value:value,type:type}=await readValue();pushChild(value,index);if(["object","array"].includes(type)){promises.push(flushArray());if(type==="object"){await importObject(target.child(index))}else{await importArray(target.child(index))}}await consumeSpaces();const nextChar=await peekBytes(1);if(nextChar==="]"){state.index++;break}await consumeToken(",");index++}promises.push(flushArray());await Promise.all(promises)};const start=async()=>{const{value:value,type:type}=await readValue();if(["object","array"].includes(type)){const target=acebase_core_1.PathInfo.get(path);if(type==="object"){await importObject(target)}else{await importArray(target)}}else{await this.setNode(path,value,childOptions)}};return start()}setSchema(path,schema,warnOnly=false){if(typeof schema==="undefined"){throw new TypeError("schema argument must be given")}if(schema===null){const i=this._schemas.findIndex((s=>s.path===path));i>=0&&this._schemas.splice(i,1);return}const definition=new acebase_core_1.SchemaDefinition(schema,{warnOnly:warnOnly,warnCallback:message=>this.logger.warn(message)});const item=this._schemas.find((s=>s.path===path));if(item){item.schema=definition}else{this._schemas.push({path:path,schema:definition});this._schemas.sort(((a,b)=>{const ka=acebase_core_1.PathInfo.getPathKeys(a.path),kb=acebase_core_1.PathInfo.getPathKeys(b.path);if(ka.length===kb.length){return 0}return ka.lengthitem.path===path));return item?{path:path,schema:item.schema.source,text:item.schema.text}:null}getSchemas(){return this._schemas.map((item=>({path:item.path,schema:item.schema.source,text:item.schema.text})))}validateSchema(path,value,options={updates:false}){let result={ok:true};const pathInfo=acebase_core_1.PathInfo.get(path);this._schemas.filter((s=>pathInfo.isOnTrailOf(s.path))).every((s=>{if(pathInfo.isDescendantOf(s.path)){const ancestorPath=acebase_core_1.PathInfo.fillVariables(s.path,path);const trailKeys=pathInfo.keys.slice(acebase_core_1.PathInfo.getPathKeys(s.path).length);result=s.schema.check(ancestorPath,value,options.updates,trailKeys);return result.ok}const trailKeys=acebase_core_1.PathInfo.getPathKeys(s.path).slice(pathInfo.keys.length);if(options.updates===true&&trailKeys.length>0&&!(trailKeys[0]in value)){return result.ok}const partial=options.updates===true&&trailKeys.length===0;const check=(path,value,trailKeys)=>{if(trailKeys.length===0){return s.schema.check(path,value,partial)}else if(value===null){return{ok:true}}const key=trailKeys[0];if(typeof key==="string"&&(key==="*"||key[0]==="$")){if(value===null||typeof value!=="object"){return{ok:true}}let result;Object.keys(value).every((childKey=>{const childPath=acebase_core_1.PathInfo.getChildPath(path,childKey);const childValue=value[childKey];result=check(childPath,childValue,trailKeys.slice(1));return result.ok}));return result}else{const childPath=acebase_core_1.PathInfo.getChildPath(path,key);const childValue=value[key];return check(childPath,childValue,trailKeys.slice(1))}};result=check(path,value,trailKeys);return result.ok}));return result}}exports.Storage=Storage},{"../assert.js":4,"../data-index/index.js":7,"../ipc/index.js":8,"../node-errors.js":11,"../node-info.js":12,"../node-value-types.js":14,"../promise-fs/index.js":16,"./errors.js":28,"./indexes.js":30,"acebase-core":46}],35:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.AceBaseBase=exports.AceBaseBaseSettings=void 0;const simple_event_emitter_1=require("./simple-event-emitter");const data_reference_1=require("./data-reference");const type_mappings_1=require("./type-mappings");const optional_observable_1=require("./optional-observable");const debug_1=require("./debug");const simple_colors_1=require("./simple-colors");class AceBaseBaseSettings{constructor(options){this.logLevel="log";this.logColors=true;this.info="realtime database";this.sponsor=false;if(typeof options!=="object"){options={}}if(typeof options.logger==="object"){this.logger=options.logger}if(typeof options.logLevel==="string"){this.logLevel=options.logLevel}if(typeof options.logColors==="boolean"){this.logColors=options.logColors}if(typeof options.info==="string"){this.info=options.info}if(typeof options.sponsor==="boolean"){this.sponsor=options.sponsor}}}exports.AceBaseBaseSettings=AceBaseBaseSettings;class AceBaseBase extends simple_event_emitter_1.SimpleEventEmitter{constructor(dbname,options={}){var _a;super();this._ready=false;options=new AceBaseBaseSettings(options);this.name=dbname;const legacyLogger=new debug_1.DebugLogger(options.logLevel,`[${dbname}]`);this.debug=legacyLogger;this.logger=(_a=options.logger)!==null&&_a!==void 0?_a:legacyLogger;(0,simple_colors_1.SetColorsEnabled)(options.logColors);const logoStyle=[simple_colors_1.ColorStyle.magenta,simple_colors_1.ColorStyle.bold];const logo=" ___ ______ "+"\n"+" / _ \\ | ___ \\ "+"\n"+" / /_\\ \\ ___ ___| |_/ / __ _ ___ ___ "+"\n"+" | _ |/ __/ _ \\ ___ \\/ _` / __|/ _ \\"+"\n"+" | | | | (_| __/ |_/ / (_| \\__ \\ __/"+"\n"+" \\_| |_/\\___\\___\\____/ \\__,_|___/\\___|";const info=options.info?"".padStart(40-options.info.length," ")+options.info+"\n":"";if(!options.sponsor){legacyLogger.write(logo.colorize(logoStyle));info&&legacyLogger.write(info.colorize(simple_colors_1.ColorStyle.magenta))}this.types=new type_mappings_1.TypeMappings(this);this.once("ready",(()=>{this._ready=true}))}async ready(callback){if(!this._ready){await new Promise((resolve=>this.on("ready",resolve)))}callback===null||callback===void 0?void 0:callback()}get isReady(){return this._ready}setObservable(ObservableImpl){(0,optional_observable_1.setObservable)(ObservableImpl)}ref(path){return new data_reference_1.DataReference(this,path)}get root(){return this.ref("")}query(path){const ref=new data_reference_1.DataReference(this,path);return new data_reference_1.DataReferenceQuery(ref)}get indexes(){return{get:()=>this.api.getIndexes(),create:(path,key,options)=>this.api.createIndex(path,key,options),delete:async filePath=>this.api.deleteIndex(filePath)}}get schema(){return{get:path=>this.api.getSchema(path),set:(path,schema,warnOnly=false)=>this.api.setSchema(path,schema,warnOnly),all:()=>this.api.getSchemas(),check:(path,value,isUpdate)=>this.api.validateSchema(path,value,isUpdate)}}}exports.AceBaseBase=AceBaseBase},{"./data-reference":42,"./debug":44,"./optional-observable":48,"./simple-colors":55,"./simple-event-emitter":56,"./type-mappings":60}],36:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.Api=void 0;const simple_event_emitter_1=require("./simple-event-emitter");class NotImplementedError extends Error{constructor(name){super(`${name} is not implemented`)}}class Api extends simple_event_emitter_1.SimpleEventEmitter{constructor(){super()}stats(options){throw new NotImplementedError("stats")}subscribe(path,event,callback,settings){throw new NotImplementedError("subscribe")}unsubscribe(path,event,callback){throw new NotImplementedError("unsubscribe")}update(path,updates,options){throw new NotImplementedError("update")}set(path,value,options){throw new NotImplementedError("set")}get(path,options){throw new NotImplementedError("get")}transaction(path,callback,options){throw new NotImplementedError("transaction")}exists(path){throw new NotImplementedError("exists")}query(path,query,options){throw new NotImplementedError("query")}reflect(path,type,args){throw new NotImplementedError("reflect")}export(path,write,options){throw new NotImplementedError("export")}import(path,read,options){throw new NotImplementedError("import")}createIndex(path,key,options){throw new NotImplementedError("createIndex")}getIndexes(){throw new NotImplementedError("getIndexes")}deleteIndex(filePath){throw new NotImplementedError("deleteIndex")}setSchema(path,schema,warnOnly){throw new NotImplementedError("setSchema")}getSchema(path){throw new NotImplementedError("getSchema")}getSchemas(){throw new NotImplementedError("getSchemas")}validateSchema(path,value,isUpdate){throw new NotImplementedError("validateSchema")}getMutations(filter){throw new NotImplementedError("getMutations")}getChanges(filter){throw new NotImplementedError("getChanges")}}exports.Api=Api},{"./simple-event-emitter":56}],37:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.ascii85=void 0;function c(input,length,result){const b=[0,0,0,0,0];for(let i=0;i";return ret}exports.ascii85={encode:function(arr){if(arr instanceof ArrayBuffer){arr=new Uint8Array(arr,0,arr.byteLength)}return encode(arr)},decode:function(input){if(!input.startsWith("<~")||!input.endsWith("~>")){throw new Error("Invalid input string")}input=input.substr(2,input.length-4);const n=input.length,r=[],b=[0,0,0,0,0];let t,x,y,d;for(let i=0;i>>=8;y=t&255;t>>>=8;r.push(t>>>8,t&255,y,x);for(let j=d;j<5;++j,r.pop()){}i+=4}const data=new Uint8Array(r);return data.buffer.slice(data.byteOffset,data.byteOffset+data.byteLength)}}},{}],38:[function(require,module,exports){"use strict";var _a,_b;Object.defineProperty(exports,"__esModule",{value:true});const pad_1=require("../pad");const env=typeof window==="object"?window:self,globalCount=Object.keys(env).length,mimeTypesLength=(_b=(_a=navigator.mimeTypes)===null||_a===void 0?void 0:_a.length)!==null&&_b!==void 0?_b:0,clientId=(0,pad_1.default)((mimeTypesLength+navigator.userAgent.length).toString(36)+globalCount.toString(36),4);function fingerprint(){return clientId}exports.default=fingerprint},{"../pad":40}],39:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});const fingerprint_1=require("./fingerprint");const pad_1=require("./pad");let c=0;const blockSize=4,base=36,discreteValues=Math.pow(base,blockSize);function randomBlock(){return(0,pad_1.default)((Math.random()*discreteValues<<0).toString(base),blockSize)}function safeCounter(){c=ct2[i]===key))}static isAncestor(ancestor,other){return ancestor.lengthother[i]===key))}static isDescendant(descendant,other){return descendant.length>other.length&&other.every(((key,i)=>descendant[i]===key))}}const isProxy=Symbol("isProxy");class LiveDataProxy{static async create(ref,options){var _a;ref=new data_reference_1.DataReference(ref.db,ref.path);let cache,loaded=false;let latestCursor=options===null||options===void 0?void 0:options.cursor;let proxy;const proxyId=id_1.ID.generate();const clientSubscriptions=[];const clientEventEmitter=new simple_event_emitter_1.SimpleEventEmitter;clientEventEmitter.on("cursor",(cursor=>latestCursor=cursor));clientEventEmitter.on("error",(err=>{console.error(err.message,err.details)}));const applyChange=(keys,newValue)=>{if(keys.length===0){cache=newValue;return true}const allowCreation=false;if(allowCreation){cache=typeof keys[0]==="number"?[]:{}}let target=cache;const trailKeys=keys.slice();while(trailKeys.length>1){const key=trailKeys.shift();if(!(key in target)){if(allowCreation){target[key]=typeof key==="number"?[]:{}}else{return false}}target=target[key]}const prop=trailKeys.shift();if(newValue===null){target instanceof Array?target.splice(prop,1):delete target[prop]}else{target[prop]=newValue}return true};const syncFallback=async()=>{if(!loaded){return}await reload()};const subscription=ref.on("mutations",{syncFallback:syncFallback}).subscribe((async snap=>{var _a;if(!loaded){return}const context=snap.context();const isRemote=((_a=context.acebase_proxy)===null||_a===void 0?void 0:_a.id)!==proxyId;if(!isRemote){return}const mutations=snap.val(false);const proceed=mutations.every((mutation=>{if(!applyChange(mutation.target,mutation.val)){return false}const changeRef=mutation.target.reduce(((ref,key)=>ref.child(key)),ref);const changeSnap=new data_snapshot_1.DataSnapshot(changeRef,mutation.val,false,mutation.prev,snap.context());clientEventEmitter.emit("mutation",{snapshot:changeSnap,isRemote:isRemote});return true}));if(proceed){clientEventEmitter.emit("cursor",context.acebase_cursor);localMutationsEmitter.emit("mutations",{origin:"remote",snap:snap})}else{console.warn(`Cached value of live data proxy on "${ref.path}" appears outdated, will be reloaded`);await reload()}}));let processPromise=Promise.resolve();const mutationQueue=[];const transactions=[];const pushLocalMutations=async()=>{const mutations=[];for(let i=0,m=mutationQueue[0];iRelativeNodeTarget.areEqual(t.target,m.target)||RelativeNodeTarget.isAncestor(t.target,m.target)))){mutationQueue.splice(i,1);i--;mutations.push(m)}}if(mutations.length===0){return}mutations.forEach((mutation=>{mutation.value=(0,utils_1.cloneObject)(getTargetValue(cache,mutation.target))}));process_1.default.nextTick((()=>{const context={acebase_proxy:{id:proxyId,source:"update"}};mutations.forEach((mutation=>{const mutationRef=mutation.target.reduce(((ref,key)=>ref.child(key)),ref);const mutationSnap=new data_snapshot_1.DataSnapshot(mutationRef,mutation.value,false,mutation.previous,context);clientEventEmitter.emit("mutation",{snapshot:mutationSnap,isRemote:false})}));const snap=new data_snapshot_1.MutationsDataSnapshot(ref,mutations.map((m=>({target:m.target,val:m.value,prev:m.previous}))),context);localMutationsEmitter.emit("mutations",{origin:"local",snap:snap})}));processPromise=mutations.reduce(((mutations,m,i,arr)=>{if(!arr.some((other=>RelativeNodeTarget.isAncestor(other.target,m.target)))){mutations.push(m)}return mutations}),[]).reduce(((updates,m)=>{const target=m.target;if(target.length===0){updates.push({ref:ref,target:target,value:cache,type:"set",previous:m.previous})}else{const parentTarget=target.slice(0,-1);const key=target.slice(-1)[0];const parentRef=parentTarget.reduce(((ref,key)=>ref.child(key)),ref);const parentUpdate=updates.find((update=>update.ref.path===parentRef.path));const cacheValue=getTargetValue(cache,target);const prevValue=m.previous;if(parentUpdate){parentUpdate.value[key]=cacheValue;parentUpdate.previous[key]=prevValue}else{updates.push({ref:parentRef,target:parentTarget,value:{[key]:cacheValue},type:"update",previous:{[key]:prevValue}})}}return updates}),[]).reduce((async(promise,update)=>{const context={acebase_proxy:{id:proxyId,source:update.type}};await promise;await update.ref.context(context)[update.type](update.value).catch((async err=>{if(options===null||options===void 0?void 0:options.shouldRollback){const rollback=await options.shouldRollback(err,{type:update.type,ref:update.ref,value:update.value,previous:update.previous});if(rollback===false){return}}clientEventEmitter.emit("error",{source:"update",message:`Error processing update of "/${ref.path}"`,details:err});const context={acebase_proxy:{id:proxyId,source:"update-rollback"}};const mutations=[];if(update.type==="set"){setTargetValue(cache,update.target,update.previous);const mutationSnap=new data_snapshot_1.DataSnapshot(update.ref,update.previous,false,update.value,context);clientEventEmitter.emit("mutation",{snapshot:mutationSnap,isRemote:false});mutations.push({target:update.target,val:update.previous,prev:update.value})}else{Object.keys(update.previous).forEach((key=>{setTargetValue(cache,update.target.concat(key),update.previous[key]);const mutationSnap=new data_snapshot_1.DataSnapshot(update.ref.child(key),update.previous[key],false,update.value[key],context);clientEventEmitter.emit("mutation",{snapshot:mutationSnap,isRemote:false});mutations.push({target:update.target.concat(key),val:update.previous[key],prev:update.value[key]})}))}mutations.forEach((m=>{const mutationRef=m.target.reduce(((ref,key)=>ref.child(key)),ref);const mutationSnap=new data_snapshot_1.DataSnapshot(mutationRef,m.val,false,m.prev,context);clientEventEmitter.emit("mutation",{snapshot:mutationSnap,isRemote:false})}));const snap=new data_snapshot_1.MutationsDataSnapshot(update.ref,mutations,context);localMutationsEmitter.emit("mutations",{origin:"local",snap:snap})}));if(update.ref.cursor){clientEventEmitter.emit("cursor",update.ref.cursor)}}),processPromise);await processPromise};let syncInProgress=false;const syncPromises=[];const syncCompleted=()=>{let resolve;const promise=new Promise((rs=>resolve=rs));syncPromises.push({resolve:resolve});return promise};let processQueueTimeout=null;const scheduleSync=()=>{if(!processQueueTimeout){processQueueTimeout=setTimeout((async()=>{syncInProgress=true;processQueueTimeout=null;await pushLocalMutations();syncInProgress=false;syncPromises.splice(0).forEach((p=>p.resolve()))}),0)}};const flagOverwritten=target=>{if(!mutationQueue.find((m=>RelativeNodeTarget.areEqual(m.target,target)))){mutationQueue.push({target:target,previous:(0,utils_1.cloneObject)(getTargetValue(cache,target))})}scheduleSync()};const localMutationsEmitter=new simple_event_emitter_1.SimpleEventEmitter;const addOnChangeHandler=(target,callback)=>{const isObject=val=>val!==null&&typeof val==="object";const mutationsHandler=async details=>{var _a;const{snap:snap,origin:origin}=details;const context=snap.context();const causedByOurProxy=((_a=context.acebase_proxy)===null||_a===void 0?void 0:_a.id)===proxyId;if(details.origin==="remote"&&causedByOurProxy){console.error("DEV ISSUE: mutationsHandler was called from remote event originating from our own proxy");return}const mutations=snap.val(false).filter((mutation=>mutation.target.slice(0,target.length).every(((key,i)=>target[i]===key))));if(mutations.length===0){return}let newValue,previousValue;const singleMutation=mutations.find((m=>m.target.length<=target.length));if(singleMutation){const trailKeys=target.slice(singleMutation.target.length);newValue=trailKeys.reduce(((val,key)=>!isObject(val)||!(key in val)?null:val[key]),singleMutation.val);previousValue=trailKeys.reduce(((val,key)=>!isObject(val)||!(key in val)?null:val[key]),singleMutation.prev)}else{const currentValue=getTargetValue(cache,target);newValue=(0,utils_1.cloneObject)(currentValue);previousValue=(0,utils_1.cloneObject)(newValue);mutations.forEach((mutation=>{const trailKeys=mutation.target.slice(target.length);for(let i=0,val=newValue,prev=previousValue;i{let keepSubscription=true;try{keepSubscription=false!==callback(Object.freeze(newValue),Object.freeze(previousValue),!causedByOurProxy,context)}catch(err){clientEventEmitter.emit("error",{source:origin==="remote"?"remote_update":"local_update",message:"Error running subscription callback",details:err})}if(keepSubscription===false){stop()}}))};localMutationsEmitter.on("mutations",mutationsHandler);const stop=()=>{localMutationsEmitter.off("mutations",mutationsHandler);clientSubscriptions.splice(clientSubscriptions.findIndex((cs=>cs.stop===stop)),1)};clientSubscriptions.push({target:target,stop:stop});return{stop:stop}};const handleFlag=(flag,target,args)=>{if(flag==="write"){return flagOverwritten(target)}else if(flag==="onChange"){return addOnChangeHandler(target,args.callback)}else if(flag==="subscribe"||flag==="observe"){const subscribe=subscriber=>{const currentValue=getTargetValue(cache,target);subscriber.next(currentValue);const subscription=addOnChangeHandler(target,(value=>{subscriber.next(value)}));return function unsubscribe(){subscription.stop()}};if(flag==="subscribe"){return subscribe}const Observable=(0,optional_observable_1.getObservable)();return new Observable(subscribe)}else if(flag==="transaction"){const hasConflictingTransaction=transactions.some((t=>RelativeNodeTarget.areEqual(target,t.target)||RelativeNodeTarget.isAncestor(target,t.target)||RelativeNodeTarget.isDescendant(target,t.target)));if(hasConflictingTransaction){return Promise.reject(new Error("Cannot start transaction because it conflicts with another transaction"))}return new Promise((async resolve=>{const hasPendingMutations=mutationQueue.some((m=>RelativeNodeTarget.areEqual(target,m.target)||RelativeNodeTarget.isAncestor(target,m.target)));if(hasPendingMutations){if(!syncInProgress){scheduleSync()}await syncCompleted()}const tx={target:target,status:"started",transaction:null};transactions.push(tx);tx.transaction={get status(){return tx.status},get completed(){return tx.status!=="started"},get mutations(){return mutationQueue.filter((m=>RelativeNodeTarget.areEqual(tx.target,m.target)||RelativeNodeTarget.isAncestor(tx.target,m.target)))},get hasMutations(){return this.mutations.length>0},async commit(){if(this.completed){throw new Error(`Transaction has completed already (status '${tx.status}')`)}tx.status="finished";transactions.splice(transactions.indexOf(tx),1);if(syncInProgress){await syncCompleted()}scheduleSync();await syncCompleted()},rollback(){if(this.completed){throw new Error(`Transaction has completed already (status '${tx.status}')`)}tx.status="canceled";const mutations=[];for(let i=0;i{if(m.target.length===0){cache=m.previous}else{setTargetValue(cache,m.target,m.previous)}}));transactions.splice(transactions.indexOf(tx),1)}};resolve(tx.transaction)}))}};const snap=await ref.get({cache_mode:"allow",cache_cursor:options===null||options===void 0?void 0:options.cursor});if(snap.context().acebase_origin!=="cache"){clientEventEmitter.emit("cursor",(_a=ref.cursor)!==null&&_a!==void 0?_a:null)}loaded=true;cache=snap.val();if(cache===null&&typeof(options===null||options===void 0?void 0:options.defaultValue)!=="undefined"){cache=options.defaultValue;const context={acebase_proxy:{id:proxyId,source:"default"}};await ref.context(context).set(cache)}proxy=createProxy({root:{ref:ref,get cache(){return cache}},target:[],id:proxyId,flag:handleFlag});const assertProxyAvailable=()=>{if(proxy===null){throw new Error("Proxy was destroyed")}};const reload=async()=>{assertProxyAvailable();mutationQueue.splice(0);const snap=await ref.get({allow_cache:false});const oldVal=cache,newVal=snap.val();cache=newVal;const mutations=(0,utils_1.getMutations)(oldVal,newVal);if(mutations.length===0){return}const context=snap.context();context.acebase_proxy={id:proxyId,source:"reload"};mutations.forEach((m=>{const targetRef=getTargetRef(ref,m.target);const newSnap=new data_snapshot_1.DataSnapshot(targetRef,m.val,m.val===null,m.prev,context);clientEventEmitter.emit("mutation",{snapshot:newSnap,isRemote:true})}));const mutationsSnap=new data_snapshot_1.MutationsDataSnapshot(ref,mutations,context);localMutationsEmitter.emit("mutations",{origin:"local",snap:mutationsSnap})};return{async destroy(){await processPromise;const promises=[subscription.stop(),...clientSubscriptions.map((cs=>cs.stop()))];await Promise.all(promises);["cursor","mutation","error"].forEach((event=>clientEventEmitter.off(event)));cache=null;proxy=null},stop(){this.destroy()},get value(){assertProxyAvailable();return proxy},get hasValue(){assertProxyAvailable();return cache!==null},set value(val){assertProxyAvailable();if(val!==null&&typeof val==="object"&&val[isProxy]){val=val.valueOf()}flagOverwritten([]);cache=val},get ref(){return ref},get cursor(){return latestCursor},reload:reload,onMutation(callback){assertProxyAvailable();clientEventEmitter.off("mutation");clientEventEmitter.on("mutation",(({snapshot:snapshot,isRemote:isRemote})=>{try{callback(snapshot,isRemote)}catch(err){clientEventEmitter.emit("error",{source:"mutation_callback",message:"Error in dataproxy onMutation callback",details:err})}}))},onError(callback){assertProxyAvailable();clientEventEmitter.off("error");clientEventEmitter.on("error",(err=>{try{callback(err)}catch(err){console.error(`Error in dataproxy onError callback: ${err.message}`)}}))},on(event,callback){clientEventEmitter.on(event,callback)},off(event,callback){clientEventEmitter.off(event,callback)}}}}exports.LiveDataProxy=LiveDataProxy;function getTargetValue(obj,target){let val=obj;for(const key of target){val=typeof val==="object"&&val!==null&&key in val?val[key]:null}return val}function setTargetValue(obj,target,value){if(target.length===0){throw new Error("Cannot update root target, caller must do that itself!")}const targetObject=target.slice(0,-1).reduce(((obj,key)=>obj[key]),obj);const prop=target.slice(-1)[0];if(value===null||typeof value==="undefined"){targetObject instanceof Array?targetObject.splice(prop,1):delete targetObject[prop]}else{targetObject[prop]=value}}function getTargetRef(ref,target){const path=path_info_1.PathInfo.get(ref.path).childPath(target);return new data_reference_1.DataReference(ref.db,path)}function createProxy(context){const targetRef=getTargetRef(context.root.ref,context.target);const childProxies=[];const handler={get(target,prop,receiver){target=getTargetValue(context.root.cache,context.target);if(typeof prop==="symbol"){if(prop.toString()===Symbol.iterator.toString()){prop="values"}else if(prop.toString()===isProxy.toString()){return true}else{return Reflect.get(target,prop,receiver)}}if(prop==="valueOf"){return function valueOf(){return target}}if(target===null||typeof target!=="object"){throw new Error(`Cannot read property "${prop}" of ${target}. Value of path "/${targetRef.path}" is not an object (anymore)`)}if(target instanceof Array&&typeof prop==="string"&&/^[0-9]+$/.test(prop)){prop=parseInt(prop)}const value=target[prop];if(value===null){delete target[prop];return}const childProxy=childProxies.find((proxy=>proxy.prop===prop));if(childProxy){if(childProxy.typeof===typeof value){return childProxy.value}childProxies.splice(childProxies.indexOf(childProxy),1)}const proxifyChildValue=prop=>{const value=target[prop];const childProxy=childProxies.find((child=>child.prop===prop));if(childProxy){if(childProxy.typeof===typeof value){return childProxy.value}childProxies.splice(childProxies.indexOf(childProxy),1)}if(typeof value!=="object"){return value}const newChildProxy=createProxy({root:context.root,target:context.target.concat(prop),id:context.id,flag:context.flag});childProxies.push({typeof:typeof value,prop:prop,value:newChildProxy});return newChildProxy};const unproxyValue=value=>value!==null&&typeof value==="object"&&value[isProxy]?value.getTarget():value;if(["string","number","boolean"].includes(typeof value)||value instanceof Date||value instanceof path_reference_1.PathReference||value instanceof ArrayBuffer||typeof value==="object"&&"buffer"in value){return value}const isArray=target instanceof Array;if(prop==="toString"){return function toString(){return`[LiveDataProxy for "${targetRef.path}"]`}}if(typeof value==="undefined"){if(prop==="push"){return function push(item){const childRef=targetRef.push();context.flag("write",context.target.concat(childRef.key));target[childRef.key]=item;return childRef.key}}if(prop==="getTarget"){return function(warn=true){warn&&console.warn("Use getTarget with caution - any changes will not be synchronized!");return target}}if(prop==="getRef"){return function getRef(){const ref=getTargetRef(context.root.ref,context.target);return ref}}if(prop==="forEach"){return function forEach(callback){const keys=Object.keys(target);let stop=false;for(let i=0;!stop&&iproxifyChildValue(key)));if(sortFn){arr.sort(sortFn)}return arr}}if(prop==="onChanged"){return function onChanged(callback){return context.flag("onChange",context.target,{callback:callback})}}if(prop==="subscribe"){return function subscribe(){return context.flag("subscribe",context.target)}}if(prop==="getObservable"){return function getObservable(){return context.flag("observe",context.target)}}if(prop==="getOrderedCollection"){return function getOrderedCollection(orderProperty,orderIncrement){return new OrderedCollectionProxy(this,orderProperty,orderIncrement)}}if(prop==="startTransaction"){return function startTransaction(){return context.flag("transaction",context.target)}}if(prop==="remove"&&!isArray){return function remove(){if(context.target.length===0){throw new Error("Can't remove proxy root value")}const parent=getTargetValue(context.root.cache,context.target.slice(0,-1));const key=context.target.slice(-1)[0];context.flag("write",context.target);delete parent[key]}}return}else if(typeof value==="function"){if(isArray){const writeArray=action=>{context.flag("write",context.target);return action()};const cleanArrayValues=values=>values.map((value=>{value=unproxyValue(value);removeVoidProperties(value);return value}));if(prop==="push"){return function push(...items){items=cleanArrayValues(items);return writeArray((()=>target.push(...items)))}}if(prop==="pop"){return function pop(){return writeArray((()=>target.pop()))}}if(prop==="splice"){return function splice(start,deleteCount,...items){items=cleanArrayValues(items);return writeArray((()=>target.splice(start,deleteCount,...items)))}}if(prop==="shift"){return function shift(){return writeArray((()=>target.shift()))}}if(prop==="unshift"){return function unshift(...items){items=cleanArrayValues(items);return writeArray((()=>target.unshift(...items)))}}if(prop==="sort"){return function sort(compareFn){return writeArray((()=>target.sort(compareFn)))}}if(prop==="reverse"){return function reverse(){return writeArray((()=>target.reverse()))}}if(["indexOf","lastIndexOf"].includes(prop)){return function indexOf(item,start){if(item!==null&&typeof item==="object"&&item[isProxy]){item=item.getTarget(false)}return target[prop](item,start)}}if(["forEach","every","some","filter","map"].includes(prop)){return function iterate(callback){return target[prop](((value,i)=>callback(proxifyChildValue(i),i,proxy)))}}if(["reduce","reduceRight"].includes(prop)){return function reduce(callback,initialValue){return target[prop](((prev,value,i)=>callback(prev,proxifyChildValue(i),i,proxy)),initialValue)}}if(["find","findIndex"].includes(prop)){return function find(callback){let value=target[prop](((value,i)=>callback(proxifyChildValue(i),i,proxy)));if(prop==="find"&&value){const index=target.indexOf(value);value=proxifyChildValue(index)}return value}}if(["values","entries","keys"].includes(prop)){return function*generator(){for(let i=0;itypeof key==="number"))){context.flag("write",context.target.slice(0,context.target.findIndex((key=>typeof key==="number"))))}else if(target instanceof Array){context.flag("write",context.target)}else{context.flag("write",context.target.concat(prop))}if(value===null){delete target[prop]}else{removeVoidProperties(value);target[prop]=value}return true},deleteProperty(target,prop){target=getTargetValue(context.root.cache,context.target);if(target===null){throw new Error(`Cannot delete property ${prop.toString()} of null`)}if(typeof prop==="symbol"){return Reflect.deleteProperty(target,prop)}if(!(prop in target)){return true}context.flag("write",context.target.concat(prop));delete target[prop];return true},ownKeys(target){target=getTargetValue(context.root.cache,context.target);return Reflect.ownKeys(target)},has(target,prop){target=getTargetValue(context.root.cache,context.target);return Reflect.has(target,prop)},getOwnPropertyDescriptor(target,prop){target=getTargetValue(context.root.cache,context.target);const descriptor=Reflect.getOwnPropertyDescriptor(target,prop);if(descriptor){descriptor.configurable=true}return descriptor},getPrototypeOf(target){target=getTargetValue(context.root.cache,context.target);return Reflect.getPrototypeOf(target)}};const proxy=new Proxy({},handler);return proxy}function removeVoidProperties(obj){if(typeof obj!=="object"){return}Object.keys(obj).forEach((key=>{const val=obj[key];if(val===null||typeof val==="undefined"){delete obj[key]}else if(typeof val==="object"){removeVoidProperties(val)}}))}function proxyAccess(proxiedValue){if(typeof proxiedValue!=="object"||!proxiedValue[isProxy]){throw new Error("Given value is not proxied. Make sure you are referencing the value through the live data proxy.")}return proxiedValue}exports.proxyAccess=proxyAccess;class OrderedCollectionProxy{constructor(collection,orderProperty="order",orderIncrement=10){this.collection=collection;this.orderProperty=orderProperty;this.orderIncrement=orderIncrement;if(typeof collection!=="object"||!collection[isProxy]){throw new Error("Collection is not proxied")}if(collection.valueOf()instanceof Array){throw new Error("Collection is an array, not an object collection")}if(!Object.keys(collection).every((key=>typeof collection[key]==="object"))){throw new Error("Collection has non-object children")}const ok=Object.keys(collection).every((key=>typeof collection[key][orderProperty]==="number"));if(!ok){const keys=Object.keys(collection);for(let i=0;i{const subscription=this.getObservable().subscribe((()=>{const newArray=this.getArray();subscriber.next(newArray)}));return function unsubscribe(){subscription.unsubscribe()}}))}getArray(){const arr=proxyAccess(this.collection).toArray(((a,b)=>a[this.orderProperty]-b[this.orderProperty]));return arr}add(item,index,from){const arr=this.getArray();let minOrder=Number.POSITIVE_INFINITY,maxOrder=Number.NEGATIVE_INFINITY;for(let i=0;ithis.collection[key]===item));if(!fromKey){throw new Error("item not found in collection")}if(from===index){return{key:fromKey,index:index}}if(Math.abs(from-index)===1){const otherItem=arr[index];const otherOrder=otherItem[this.orderProperty];otherItem[this.orderProperty]=item[this.orderProperty];item[this.orderProperty]=otherOrder;return{key:fromKey,index:index}}else{arr.splice(from,1)}}if(typeof index!=="number"||index>=arr.length){index=arr.length;item[this.orderProperty]=arr.length==0?0:maxOrder+this.orderIncrement}else if(index===0){item[this.orderProperty]=arr.length==0?0:minOrder-this.orderIncrement}else{const orders=arr.map((item=>item[this.orderProperty]));const gap=orders[index]-orders[index-1];if(gap>1){item[this.orderProperty]=orders[index]-Math.floor(gap/2)}else{arr.splice(index,0,item);for(let i=0;ithis.collection[key]===item));if(!key){throw new Error("Cannot find target object to delete")}this.collection[key]=null;return{key:key,index:index}}move(fromIndex,toIndex){const arr=this.getArray();return this.add(arr[fromIndex],toIndex,fromIndex)}sort(sortFn){const arr=this.getArray();arr.sort(sortFn);for(let i=0;i{newContext[key]=context[key]}))}this[_private].context=newContext;return this}else if(typeof context==="undefined"){console.warn("Use snap.context() instead of snap.ref.context() to get updating context in event callbacks");return currentContext}else{throw new Error("Invalid context argument")}}get cursor(){return this[_private].cursor}set cursor(value){var _a;this[_private].cursor=value;(_a=this.onCursor)===null||_a===void 0?void 0:_a.call(this,value)}get path(){return this[_private].path}get key(){const key=this[_private].key;return typeof key==="number"?`[${key}]`:key}get index(){const key=this[_private].key;if(typeof key!=="number"){throw new Error(`"${key}" is not a number`)}return key}get parent(){const currentPath=path_info_1.PathInfo.fillVariables2(this.path,this.vars);const info=path_info_1.PathInfo.get(currentPath);if(info.parentPath===null){return null}return new DataReference(this.db,info.parentPath).context(this[_private].context)}get vars(){return this[_private].vars}child(childPath){childPath=typeof childPath==="number"?childPath:childPath.replace(/^\/|\/$/g,"");const currentPath=path_info_1.PathInfo.fillVariables2(this.path,this.vars);const targetPath=path_info_1.PathInfo.getChildPath(currentPath,childPath);return new DataReference(this.db,targetPath).context(this[_private].context)}async set(value,onComplete){try{if(this.isWildcardPath){throw new Error(`Cannot set the value of wildcard path "/${this.path}"`)}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 undefined value in "/${this.path}"`)}if(!this.db.isReady){await this.db.ready()}value=this.db.types.serialize(this.path,value);const{cursor:cursor}=await this.db.api.set(this.path,value,{context:this[_private].context});this.cursor=cursor;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,this)}catch(err){console.error("Error in onComplete callback:",err)}}else{throw err}}return this}async update(updates,onComplete){try{if(this.isWildcardPath){throw new Error(`Cannot update the value of wildcard path "/${this.path}"`)}if(!this.db.isReady){await this.db.ready()}if(typeof updates!=="object"||updates instanceof Array||updates instanceof ArrayBuffer||updates instanceof Date){await this.set(updates)}else if(Object.keys(updates).length===0){console.warn(`update called on path "/${this.path}", but there is nothing to update`)}else{updates=this.db.types.serialize(this.path,updates);const{cursor:cursor}=await this.db.api.update(this.path,updates,{context:this[_private].context});this.cursor=cursor}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,this)}catch(err){console.error("Error in onComplete callback:",err)}}else{throw err}}return this}async transaction(callback){if(this.isWildcardPath){throw new Error(`Cannot start a transaction on wildcard path "/${this.path}"`)}if(!this.db.isReady){await this.db.ready()}let throwError;const cb=currentValue=>{currentValue=this.db.types.deserialize(this.path,currentValue);const snap=new data_snapshot_1.DataSnapshot(this,currentValue);let newValue;try{newValue=callback(snap)}catch(err){throwError=err;return}if(newValue instanceof Promise){return newValue.then((val=>this.db.types.serialize(this.path,val))).catch((err=>{throwError=err;return}))}else{return this.db.types.serialize(this.path,newValue)}};const{cursor:cursor}=await this.db.api.transaction(this.path,cb,{context:this[_private].context});this.cursor=cursor;if(throwError){throw throwError}return this}on(event,callback,cancelCallback){if(this.path===""&&["value","child_changed"].includes(event)){console.warn("WARNING: Listening for value and child_changed events on the root node is a bad practice. These events require loading of all data (value event), or potentially lots of data (child_changed event) each time they are fired")}let eventPublisher=null;const eventStream=new subscription_1.EventStream((publisher=>{eventPublisher=publisher}));const cb={event:event,stream:eventStream,userCallback:typeof callback==="function"&&callback,ourCallback:(err,path,newValue,oldValue,eventContext)=>{if(err){this.db.logger.error(`Error getting data for event ${event} on path "${path}"`,err);return}const ref=this.db.ref(path);ref[_private].vars=path_info_1.PathInfo.extractVariables(this.path,path);let callbackObject;if(event.startsWith("notify_")){callbackObject=ref.context(eventContext||{})}else{const values={previous:this.db.types.deserialize(path,oldValue),current:this.db.types.deserialize(path,newValue)};if(event==="child_removed"){callbackObject=new data_snapshot_1.DataSnapshot(ref,values.previous,true,values.previous,eventContext)}else if(event==="mutations"){callbackObject=new data_snapshot_1.MutationsDataSnapshot(ref,values.current,eventContext)}else{const isRemoved=event==="mutated"&&values.current===null;callbackObject=new data_snapshot_1.DataSnapshot(ref,values.current,isRemoved,values.previous,eventContext)}}eventPublisher.publish(callbackObject);if(eventContext===null||eventContext===void 0?void 0:eventContext.acebase_cursor){this.cursor=eventContext.acebase_cursor}}};this[_private].callbacks.push(cb);const subscribe=()=>{if(typeof callback==="function"){eventStream.subscribe(callback,((activated,cancelReason)=>{if(!activated){cancelCallback&&cancelCallback(cancelReason)}}))}const advancedOptions=typeof callback==="object"?callback:{newOnly:!callback};if(typeof advancedOptions.newOnly!=="boolean"){advancedOptions.newOnly=false}if(this.isWildcardPath){advancedOptions.newOnly=true}const cancelSubscription=err=>{const callbacks=this[_private].callbacks;callbacks.splice(callbacks.indexOf(cb),1);this.db.api.unsubscribe(this.path,event,cb.ourCallback);this.db.logger.error(`Subscription "${event}" on path "/${this.path}" canceled because of an error: ${err.message}`);eventPublisher.cancel(err.message)};const authorized=this.db.api.subscribe(this.path,event,cb.ourCallback,{newOnly:advancedOptions.newOnly,cancelCallback:cancelSubscription,syncFallback:advancedOptions.syncFallback});const allSubscriptionsStoppedCallback=()=>{const callbacks=this[_private].callbacks;callbacks.splice(callbacks.indexOf(cb),1);return this.db.api.unsubscribe(this.path,event,cb.ourCallback)};if(authorized instanceof Promise){authorized.then((()=>{eventPublisher.start(allSubscriptionsStoppedCallback)})).catch(cancelSubscription)}else{eventPublisher.start(allSubscriptionsStoppedCallback)}if(!advancedOptions.newOnly){if(event==="value"){this.get((snap=>{eventPublisher.publish(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=>{const childSnap=new data_snapshot_1.DataSnapshot(this.child(key),val[key]);eventPublisher.publish(childSnap)}))}))}else if(event==="notify_child_added"){const step=100,limit=step;let skip=0;const more=async()=>{const children=await this.db.api.reflect(this.path,"children",{limit:limit,skip:skip});children.list.forEach((child=>{const childRef=this.child(child.key);eventPublisher.publish(childRef)}));if(children.more){skip+=step;more()}};more()}}};if(this.db.isReady){subscribe()}else{this.db.ready(subscribe)}return eventStream}off(event,callback){const subscriptions=this[_private].callbacks;const stopSubs=subscriptions.filter((sub=>(!event||sub.event===event)&&(!callback||sub.userCallback===callback)));if(stopSubs.length===0){this.db.logger.warn(`Can't find event subscriptions to stop (path: "${this.path}", event: ${event||"(any)"}, callback: ${callback})`)}stopSubs.forEach((sub=>{sub.stream.stop()}));return this}get(optionsOrCallback,callback){if(!this.db.isReady){const promise=this.db.ready().then((()=>this.get(optionsOrCallback,callback)));return typeof optionsOrCallback!=="function"&&typeof callback!=="function"?promise:undefined}callback=typeof optionsOrCallback==="function"?optionsOrCallback:typeof callback==="function"?callback:undefined;if(this.isWildcardPath){const error=new Error(`Cannot get value of wildcard path "/${this.path}". Use .query() instead`);if(typeof callback==="function"){throw error}return Promise.reject(error)}const options=new DataRetrievalOptions(typeof optionsOrCallback==="object"?optionsOrCallback:{cache_mode:"allow"});const promise=this.db.api.get(this.path,options).then((result=>{var _a;const isNewApiResult="context"in result&&"value"in result;if(!isNewApiResult){console.warn("AceBase api.get method returned an old response value. Update your acebase or acebase-client package");result={value:result,context:{}}}const value=this.db.types.deserialize(this.path,result.value);const snapshot=new data_snapshot_1.DataSnapshot(this,value,undefined,undefined,result.context);if((_a=result.context)===null||_a===void 0?void 0:_a.acebase_cursor){this.cursor=result.context.acebase_cursor}return snapshot}));if(callback){promise.then(callback).catch((err=>{console.error("Uncaught error:",err)}));return}else{return promise}}once(event,options){if(event==="value"&&!this.isWildcardPath){return this.get(options)}return new Promise((resolve=>{const callback=snap=>{this.off(event,callback);resolve(snap)};this.on(event,callback)}))}push(value,onComplete){if(this.isWildcardPath){const error=new Error(`Cannot push to wildcard path "/${this.path}"`);if(typeof value==="undefined"||typeof onComplete==="function"){throw error}return Promise.reject(error)}const id=id_1.ID.generate();const ref=this.child(id);ref[_private].pushed=true;if(typeof value!=="undefined"){return ref.set(value,onComplete).then((()=>ref))}else{return ref}}async remove(){if(this.isWildcardPath){throw new Error(`Cannot remove wildcard path "/${this.path}". Use query().remove instead`)}if(this.parent===null){throw new Error("Cannot remove the root node")}return this.set(null)}async exists(){if(this.isWildcardPath){throw new Error(`Cannot check wildcard path "/${this.path}" existence`)}if(!this.db.isReady){await this.db.ready()}return this.db.api.exists(this.path)}get isWildcardPath(){return this.path.indexOf("*")>=0||this.path.indexOf("$")>=0}query(){return new DataReferenceQuery(this)}async count(){const info=await this.reflect("info",{child_count:true});return info.children.count}async reflect(type,args){if(this.isWildcardPath){throw new Error(`Cannot reflect on wildcard path "/${this.path}"`)}if(!this.db.isReady){await this.db.ready()}return this.db.api.reflect(this.path,type,args)}async export(write,options={format:"json",type_safe:true}){if(this.isWildcardPath){throw new Error(`Cannot export wildcard path "/${this.path}"`)}if(!this.db.isReady){await this.db.ready()}const writeFn=typeof write==="function"?write:write.write.bind(write);return this.db.api.export(this.path,writeFn,options)}async import(read,options={format:"json",suppress_events:false}){if(this.isWildcardPath){throw new Error(`Cannot import to wildcard path "/${this.path}"`)}if(!this.db.isReady){await this.db.ready()}return this.db.api.import(this.path,read,options)}proxy(options){const isOptionsArg=typeof options==="object"&&(typeof options.cursor!=="undefined"||typeof options.defaultValue!=="undefined");if(typeof options!=="undefined"&&!isOptionsArg){this.db.logger.warn("Warning: live data proxy is being initialized with a deprecated method signature. Use ref.proxy(options) instead of ref.proxy(defaultValue)");options={defaultValue:options}}return data_proxy_1.LiveDataProxy.create(this,options)}observe(options){if(options){throw new Error("observe does not support data retrieval options yet")}if(this.isWildcardPath){throw new Error(`Cannot observe wildcard path "/${this.path}"`)}const Observable=(0,optional_observable_1.getObservable)();return new Observable((observer=>{let cache,resolved=false;let promise=this.get(options).then((snap=>{resolved=true;cache=snap.val();observer.next(cache)}));const updateCache=snap=>{if(!resolved){promise=promise.then((()=>updateCache(snap)));return}const mutatedPath=snap.ref.path;if(mutatedPath===this.path){cache=snap.val();return observer.next(cache)}const trailKeys=path_info_1.PathInfo.getPathKeys(mutatedPath).slice(path_info_1.PathInfo.getPathKeys(this.path).length);let target=cache;while(trailKeys.length>1){const key=trailKeys.shift();if(!(key in target)){target[key]=typeof trailKeys[0]==="number"?[]:{}}target=target[key]}const prop=trailKeys.shift();const newValue=snap.val();if(newValue===null){target instanceof Array&&typeof prop==="number"?target.splice(prop,1):delete target[prop]}else{target[prop]=newValue}observer.next(cache)};this.on("mutated",updateCache);return()=>{this.off("mutated",updateCache)}}))}async forEach(callbackOrOptions,callback){let options;if(typeof callbackOrOptions==="function"){callback=callbackOrOptions}else{options=callbackOrOptions}if(typeof callback!=="function"){throw new TypeError("No callback function given")}const info=await this.reflect("children",{limit:0,skip:0});const summary={canceled:false,total:info.list.length,processed:0};for(let i=0;ithis.get(optionsOrCallback,callback)));return typeof optionsOrCallback!=="function"&&typeof callback!=="function"?promise:undefined}callback=typeof optionsOrCallback==="function"?optionsOrCallback:typeof callback==="function"?callback:undefined;const options=new QueryDataRetrievalOptions(typeof optionsOrCallback==="object"?optionsOrCallback:{snapshots:true,cache_mode:"allow"});options.allow_cache=options.cache_mode!=="bypass";options.eventHandler=ev=>{if(!this[_private].events[ev.name]){return false}const listeners=this[_private].events[ev.name];if(typeof listeners!=="object"||listeners.length===0){return false}if(["add","change","remove"].includes(ev.name)){const eventData={name:ev.name,ref:new DataReference(this.ref.db,ev.path)};if(options.snapshots&&ev.name!=="remove"){const val=db.types.deserialize(ev.path,ev.value);eventData.snapshot=new data_snapshot_1.DataSnapshot(eventData.ref,val,false)}ev=eventData}listeners.forEach((callback=>{var _a,_b;try{callback(ev)}catch(err){this.ref.db.logger.error(`Error executing "${ev.name}" event handler of realtime query on path "${this.ref.path}": ${(_b=(_a=err===null||err===void 0?void 0:err.stack)!==null&&_a!==void 0?_a:err===null||err===void 0?void 0:err.message)!==null&&_b!==void 0?_b:err}`)}}))};options.monitor={add:false,change:false,remove:false};if(this[_private].events){if(this[_private].events["add"]&&this[_private].events["add"].length>0){options.monitor.add=true}if(this[_private].events["change"]&&this[_private].events["change"].length>0){options.monitor.change=true}if(this[_private].events["remove"]&&this[_private].events["remove"].length>0){options.monitor.remove=true}}this.stop();const db=this.ref.db;return db.api.query(this.ref.path,this[_private],options).catch((err=>{throw new Error(err)})).then((res=>{const{stop:stop}=res;let{results:results,context:context}=res;this.stop=async()=>{await stop()};if(!("results"in res&&"context"in res)){console.warn("Query results missing context. Update your acebase and/or acebase-client packages");results=res,context={}}if(options.snapshots){const snaps=results.map((result=>{const val=db.types.deserialize(result.path,result.val);return new data_snapshot_1.DataSnapshot(db.ref(result.path),val,false,undefined,context)}));return DataSnapshotsArray.from(snaps)}else{const refs=results.map((path=>db.ref(path)));return DataReferencesArray.from(refs)}})).then((results=>{callback&&callback(results);return results}))}async stop(){}getRefs(callback){return this.get({snapshots:false},callback)}find(){return this.get({snapshots:false})}async count(){const refs=await this.find();return refs.length}async exists(){const originalTake=this[_private].take;const p=this.take(1).find();this.take(originalTake);const refs=await p;return refs.length!==0}async remove(callback){const refs=await this.find();const parentUpdates=refs.reduce(((parents,ref)=>{const parent=parents[ref.parent.path];if(!parent){parents[ref.parent.path]=[ref]}else{parent.push(ref)}return parents}),{});const db=this.ref.db;const promises=Object.keys(parentUpdates).map((async parentPath=>{const updates=refs.reduce(((updates,ref)=>{updates[ref.key]=null;return updates}),{});const ref=db.ref(parentPath);try{await ref.update(updates);return{ref:ref,success:true}}catch(error){return{ref:ref,success:false,error:error}}}));const results=await Promise.all(promises);callback&&callback(results);return results}on(event,callback){if(!this[_private].events[event]){this[_private].events[event]=[]}this[_private].events[event].push(callback);return this}off(event,callback){if(typeof event==="undefined"){this[_private].events={};return this}if(!this[_private].events[event]){return this}if(typeof callback==="undefined"){delete this[_private].events[event];return this}const index=this[_private].events[event].indexOf(callback);if(!~index){return this}this[_private].events[event].splice(index,1);return this}async forEach(callbackOrOptions,callback){let options;if(typeof callbackOrOptions==="function"){callback=callbackOrOptions}else{options=callbackOrOptions}if(typeof callback!=="function"){throw new TypeError("No callback function given")}const refs=await this.find();const summary={canceled:false,total:refs.length,processed:0};for(let i=0;iarr[i]=snap));return arr}getValues(){return this.map((snap=>snap.val()))}}exports.DataSnapshotsArray=DataSnapshotsArray;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))}}exports.DataReferencesArray=DataReferencesArray},{"./data-proxy":41,"./data-snapshot":43,"./id":45,"./optional-observable":48,"./path-info":50,"./subscription":58}],43:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.MutationsDataSnapshot=exports.DataSnapshot=void 0;const path_info_1=require("./path-info");function getChild(snapshot,path,previous=false){if(!snapshot.exists()){return null}let child=previous?snapshot.previous():snapshot.val();if(typeof path==="number"){return child[path]}path_info_1.PathInfo.getPathKeys(path).every((key=>{child=child[key];return typeof child!=="undefined"}));return child||null}function getChildren(snapshot){if(!snapshot.exists()){return[]}const 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{exists(){return false}constructor(ref,value,isRemoved=false,prevValue,context){this.ref=ref;this.val=()=>value;this.previous=()=>prevValue;this.exists=()=>{if(isRemoved){return false}return value!==null&&typeof value!=="undefined"};this.context=()=>context||{}}static for(ref,value){return new DataSnapshot(ref,value)}child(path){const val=getChild(this,path,false);const prev=getChild(this,path,true);return new DataSnapshot(this.ref.child(path),val,false,prev)}hasChild(path){return getChild(this,path)!==null}hasChildren(){return getChildren(this).length>0}numChildren(){return getChildren(this).length}forEach(callback){const value=this.val();const prev=this.previous();return getChildren(this).every((key=>{const snap=new DataSnapshot(this.ref.child(key),value[key],false,prev[key]);return callback(snap)}))}get key(){return this.ref.key}}exports.DataSnapshot=DataSnapshot;class MutationsDataSnapshot extends DataSnapshot{constructor(ref,mutations,context){super(ref,mutations,false,undefined,context);this.previous=()=>{throw new Error("Iterate values to get previous values for each mutation")};this.val=(warn=true)=>{if(warn){console.warn("Unless you know what you are doing, it is best not to use the value of a mutations snapshot directly. Use child methods and forEach to iterate the mutations instead")}return mutations}}forEach(callback){const mutations=this.val(false);return mutations.every((mutation=>{const ref=mutation.target.reduce(((ref,key)=>ref.child(key)),this.ref);const snap=new DataSnapshot(ref,mutation.val,false,mutation.prev);return callback(snap)}))}child(index){if(typeof index!=="number"){throw new Error("child index must be a number")}const mutation=this.val(false)[index];const ref=mutation.target.reduce(((ref,key)=>ref.child(key)),this.ref);return new DataSnapshot(ref,mutation.val,false,mutation.prev)}}exports.MutationsDataSnapshot=MutationsDataSnapshot},{"./path-info":50}],44:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.DebugLogger=void 0;const process_1=require("./process");const noop=()=>{};class DebugLogger{constructor(level="log",prefix=""){this.level=level;this.prefix=prefix;this.setLevel(level)}setLevel(level){const prefix=this.prefix?this.prefix+" %s":"";this.verbose=["verbose"].includes(level)?prefix?console.log.bind(console,prefix):console.log.bind(console):noop;this.trace=["verbose"].includes(level)?prefix?console.log.bind(console,prefix):console.log.bind(console):noop;this.debug=["verbose"].includes(level)?prefix?console.log.bind(console,prefix):console.log.bind(console):noop;this.log=["verbose","log"].includes(level)?prefix?console.log.bind(console,prefix):console.log.bind(console):noop;this.info=["verbose","log"].includes(level)?prefix?console.log.bind(console,prefix):console.log.bind(console):noop;this.warn=["verbose","log","warn"].includes(level)?prefix?console.warn.bind(console,prefix):console.warn.bind(console):noop;this.error=["verbose","log","warn","error"].includes(level)?prefix?console.error.bind(console,prefix):console.error.bind(console):noop;this.fatal=["verbose","log","warn","error"].includes(level)?prefix?console.error.bind(console,prefix):console.error.bind(console):noop;this.write=text=>{const isRunKit=typeof process_1.default!=="undefined"&&process_1.default.env&&typeof process_1.default.env.RUNKIT_ENDPOINT_PATH==="string";if(text&&isRunKit){text.split("\n").forEach((line=>console.log(line)))}else{console.log(text)}}}}exports.DebugLogger=DebugLogger},{"./process":52}],45:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.ID=void 0;const cuid_1=require("./cuid");let timeBias=0;class ID{static set timeBias(bias){if(typeof bias!=="number"){return}timeBias=bias}static generate(){return(0,cuid_1.default)(timeBias).slice(1)}}exports.ID=ID},{"./cuid":39}],46:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.ObjectCollection=exports.PartialArray=exports.SimpleObservable=exports.SchemaDefinition=exports.Colorize=exports.ColorStyle=exports.SimpleEventEmitter=exports.SimpleCache=exports.ascii85=exports.PathInfo=exports.Utils=exports.TypeMappings=exports.Transport=exports.EventSubscription=exports.EventPublisher=exports.EventStream=exports.PathReference=exports.ID=exports.DebugLogger=exports.OrderedCollectionProxy=exports.proxyAccess=exports.MutationsDataSnapshot=exports.DataSnapshot=exports.DataReferencesArray=exports.DataSnapshotsArray=exports.QueryDataRetrievalOptions=exports.DataRetrievalOptions=exports.DataReferenceQuery=exports.DataReference=exports.Api=exports.AceBaseBaseSettings=exports.AceBaseBase=void 0;var acebase_base_1=require("./acebase-base");Object.defineProperty(exports,"AceBaseBase",{enumerable:true,get:function(){return acebase_base_1.AceBaseBase}});Object.defineProperty(exports,"AceBaseBaseSettings",{enumerable:true,get:function(){return acebase_base_1.AceBaseBaseSettings}});var api_1=require("./api");Object.defineProperty(exports,"Api",{enumerable:true,get:function(){return api_1.Api}});var data_reference_1=require("./data-reference");Object.defineProperty(exports,"DataReference",{enumerable:true,get:function(){return data_reference_1.DataReference}});Object.defineProperty(exports,"DataReferenceQuery",{enumerable:true,get:function(){return data_reference_1.DataReferenceQuery}});Object.defineProperty(exports,"DataRetrievalOptions",{enumerable:true,get:function(){return data_reference_1.DataRetrievalOptions}});Object.defineProperty(exports,"QueryDataRetrievalOptions",{enumerable:true,get:function(){return data_reference_1.QueryDataRetrievalOptions}});Object.defineProperty(exports,"DataSnapshotsArray",{enumerable:true,get:function(){return data_reference_1.DataSnapshotsArray}});Object.defineProperty(exports,"DataReferencesArray",{enumerable:true,get:function(){return data_reference_1.DataReferencesArray}});var data_snapshot_1=require("./data-snapshot");Object.defineProperty(exports,"DataSnapshot",{enumerable:true,get:function(){return data_snapshot_1.DataSnapshot}});Object.defineProperty(exports,"MutationsDataSnapshot",{enumerable:true,get:function(){return data_snapshot_1.MutationsDataSnapshot}});var data_proxy_1=require("./data-proxy");Object.defineProperty(exports,"proxyAccess",{enumerable:true,get:function(){return data_proxy_1.proxyAccess}});Object.defineProperty(exports,"OrderedCollectionProxy",{enumerable:true,get:function(){return data_proxy_1.OrderedCollectionProxy}});var debug_1=require("./debug");Object.defineProperty(exports,"DebugLogger",{enumerable:true,get:function(){return debug_1.DebugLogger}});var id_1=require("./id");Object.defineProperty(exports,"ID",{enumerable:true,get:function(){return id_1.ID}});var path_reference_1=require("./path-reference");Object.defineProperty(exports,"PathReference",{enumerable:true,get:function(){return path_reference_1.PathReference}});var subscription_1=require("./subscription");Object.defineProperty(exports,"EventStream",{enumerable:true,get:function(){return subscription_1.EventStream}});Object.defineProperty(exports,"EventPublisher",{enumerable:true,get:function(){return subscription_1.EventPublisher}});Object.defineProperty(exports,"EventSubscription",{enumerable:true,get:function(){return subscription_1.EventSubscription}});exports.Transport=require("./transport");var type_mappings_1=require("./type-mappings");Object.defineProperty(exports,"TypeMappings",{enumerable:true,get:function(){return type_mappings_1.TypeMappings}});exports.Utils=require("./utils");var path_info_1=require("./path-info");Object.defineProperty(exports,"PathInfo",{enumerable:true,get:function(){return path_info_1.PathInfo}});var ascii85_1=require("./ascii85");Object.defineProperty(exports,"ascii85",{enumerable:true,get:function(){return ascii85_1.ascii85}});var simple_cache_1=require("./simple-cache");Object.defineProperty(exports,"SimpleCache",{enumerable:true,get:function(){return simple_cache_1.SimpleCache}});var simple_event_emitter_1=require("./simple-event-emitter");Object.defineProperty(exports,"SimpleEventEmitter",{enumerable:true,get:function(){return simple_event_emitter_1.SimpleEventEmitter}});var simple_colors_1=require("./simple-colors");Object.defineProperty(exports,"ColorStyle",{enumerable:true,get:function(){return simple_colors_1.ColorStyle}});Object.defineProperty(exports,"Colorize",{enumerable:true,get:function(){return simple_colors_1.Colorize}});var schema_1=require("./schema");Object.defineProperty(exports,"SchemaDefinition",{enumerable:true,get:function(){return schema_1.SchemaDefinition}});var simple_observable_1=require("./simple-observable");Object.defineProperty(exports,"SimpleObservable",{enumerable:true,get:function(){return simple_observable_1.SimpleObservable}});var partial_array_1=require("./partial-array");Object.defineProperty(exports,"PartialArray",{enumerable:true,get:function(){return partial_array_1.PartialArray}});const object_collection_1=require("./object-collection");Object.defineProperty(exports,"ObjectCollection",{enumerable:true,get:function(){return object_collection_1.ObjectCollection}})},{"./acebase-base":35,"./api":36,"./ascii85":37,"./data-proxy":41,"./data-reference":42,"./data-snapshot":43,"./debug":44,"./id":45,"./object-collection":47,"./partial-array":49,"./path-info":50,"./path-reference":51,"./schema":53,"./simple-cache":54,"./simple-colors":55,"./simple-event-emitter":56,"./simple-observable":57,"./subscription":58,"./transport":59,"./type-mappings":60,"./utils":61}],47:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.ObjectCollection=void 0;const id_1=require("./id");class ObjectCollection{static from(array){const collection={};array.forEach((child=>{collection[id_1.ID.generate()]=child}));return collection}}exports.ObjectCollection=ObjectCollection},{"./id":45}],48:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.setObservable=exports.getObservable=void 0;const simple_observable_1=require("./simple-observable");const utils_1=require("./utils");let _shimRequested=false;let _observable;(async()=>{const global=(0,utils_1.getGlobalObject)();if(typeof global.Observable!=="undefined"){_observable=global.Observable;return}try{const{Observable:Observable}=await Promise.resolve().then((()=>require("rxjs")));_observable=Observable}catch(_a){_observable=simple_observable_1.SimpleObservable}})();function getObservable(){if(_observable===simple_observable_1.SimpleObservable&&!_shimRequested){console.warn("Using AceBase's simple Observable implementation because rxjs is not available. "+'Add it to your project with "npm install rxjs", add it to AceBase using db.setObservable(Observable), '+'or call db.setObservable("shim") to suppress this warning')}if(_observable){return _observable}throw new Error("RxJS Observable could not be loaded. ")}exports.getObservable=getObservable;function setObservable(Observable){if(Observable==="shim"){_observable=simple_observable_1.SimpleObservable;_shimRequested=true}else{_observable=Observable}}exports.setObservable=setObservable},{"./simple-observable":57,"./utils":61,rxjs:62}],49:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.PartialArray=void 0;class PartialArray{constructor(sparseArray){if(sparseArray instanceof Array){for(let i=0;ikey.startsWith("[")?parseInt(key.slice(1,-1)):key))}class PathInfo{static get(path){return new PathInfo(path)}static getChildPath(path,childKey){return PathInfo.get(path).child(childKey).path}static getPathKeys(path){return getPathKeys(path)}constructor(path){if(typeof path==="string"){this.keys=getPathKeys(path)}else if(path instanceof Array){this.keys=path}this.path=this.keys.reduce(((path,key,i)=>i===0?`${key}`:typeof key==="string"?`${path}/${key}`:`${path}[${key}]`),"")}get key(){return this.keys.length===0?null:this.keys.slice(-1)[0]}get parent(){if(this.keys.length==0){return null}const parentKeys=this.keys.slice(0,-1);return new PathInfo(parentKeys)}get parentPath(){return this.keys.length===0?null:this.parent.path}child(childKey){if(typeof childKey==="string"){if(childKey.length===0){throw new Error(`child key for path "${this.path}" cannot be empty`)}const keys=getPathKeys(childKey);keys.forEach((key=>{if(typeof key!=="string"){return}if(/[\x00-\x08\x0b\x0c\x0e-\x1f/[\]\\]/.test(key)){throw new Error(`Invalid child key "${key}" for path "${this.path}". Keys cannot contain control characters or any of the following characters: \\ / [ ]`)}if(key.length>128){throw new Error(`child key "${key}" for path "${this.path}" is too long. Max key length is 128`)}if(key.length===0){throw new Error(`child key for path "${this.path}" cannot be empty`)}}));childKey=keys}return new PathInfo(this.keys.concat(childKey))}childPath(childKey){return this.child(childKey).path}get pathKeys(){return this.keys}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);const 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 "${varPath}" 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}const pathKeys=getPathKeys(varPath);let n=0;const targetPath=pathKeys.reduce(((path,key)=>{if(typeof key==="string"&&(key==="*"||key.startsWith("$"))){return PathInfo.getChildPath(path,vars[n++])}else{return PathInfo.getChildPath(path,key)}}),"");return targetPath}equals(otherPath){const other=otherPath instanceof PathInfo?otherPath:new PathInfo(otherPath);if(this.path===other.path){return true}if(this.keys.length!==other.keys.length){return false}return this.keys.every(((key,index)=>{const otherKey=other.keys[index];return otherKey===key||typeof otherKey==="string"&&(otherKey==="*"||otherKey[0]==="$")||typeof key==="string"&&(key==="*"||key[0]==="$")}))}isAncestorOf(descendantPath){const descendant=descendantPath instanceof PathInfo?descendantPath:new PathInfo(descendantPath);if(descendant.path===""||this.path===descendant.path){return false}if(this.path===""){return true}if(this.keys.length>=descendant.keys.length){return false}return this.keys.every(((key,index)=>{const otherKey=descendant.keys[index];return otherKey===key||typeof otherKey==="string"&&(otherKey==="*"||otherKey[0]==="$")||typeof key==="string"&&(key==="*"||key[0]==="$")}))}isDescendantOf(ancestorPath){const ancestor=ancestorPath instanceof PathInfo?ancestorPath:new PathInfo(ancestorPath);if(this.path===""||this.path===ancestor.path){return false}if(ancestorPath===""){return true}if(ancestor.keys.length>=this.keys.length){return false}return ancestor.keys.every(((key,index)=>{const otherKey=this.keys[index];return otherKey===key||typeof otherKey==="string"&&(otherKey==="*"||otherKey[0]==="$")||typeof key==="string"&&(key==="*"||key[0]==="$")}))}isOnTrailOf(otherPath){const other=otherPath instanceof PathInfo?otherPath:new PathInfo(otherPath);if(this.path.length===0||other.path.length===0){return true}if(this.path===other.path){return true}return this.pathKeys.every(((key,index)=>{if(index>=other.keys.length){return true}const otherKey=other.keys[index];return otherKey===key||typeof otherKey==="string"&&(otherKey==="*"||otherKey[0]==="$")||typeof key==="string"&&(key==="*"||key[0]==="$")}))}isChildOf(otherPath){const other=otherPath instanceof PathInfo?otherPath:new PathInfo(otherPath);if(this.path===""){return false}return this.parent.equals(other)}isParentOf(otherPath){const other=otherPath instanceof PathInfo?otherPath:new PathInfo(otherPath);if(other.path===""){return false}return this.equals(other.parent)}}exports.PathInfo=PathInfo},{}],51:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.PathReference=void 0;class PathReference{constructor(path){this.path=path}}exports.PathReference=PathReference},{}],52:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.default={nextTick(fn){setTimeout(fn,0)}}},{}],53:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.SchemaDefinition=void 0;function parse(definition){let pos=0;function consumeSpaces(){let c;while(c=definition[pos],[" ","\r","\n","\t"].includes(c)){pos++}}function consumeCharacter(c){if(definition[pos]!==c){throw new Error(`Unexpected character at position ${pos}. Expected: '${c}', found '${definition[pos]}'`)}pos++}function readProperty(){consumeSpaces();const prop={name:"",optional:false,wildcard:false};let c;while(c=definition[pos],c==="_"||c==="$"||c>="a"&&c<="z"||c>="A"&&c<="Z"||prop.name.length>0&&c>="0"&&c<="9"||prop.name.length===0&&c==="*"){prop.name+=c;pos++}if(prop.name.length===0){throw new Error(`Property name expected at position ${pos}, found: ${definition.slice(pos,pos+10)}..`)}if(definition[pos]==="?"){prop.optional=true;pos++}if(prop.name==="*"||prop.name[0]==="$"){prop.optional=true;prop.wildcard=true}consumeSpaces();consumeCharacter(":");return prop}function readType(){consumeSpaces();let type={typeOf:"any"},c;let name="";while(c=definition[pos],c>="a"&&c<="z"||c>="A"&&c<="Z"){name+=c;pos++}if(name.length===0){if(definition[pos]==="*"){consumeCharacter("*");type.typeOf="any"}else if(["'",'"',"`"].includes(definition[pos])){type.typeOf="string";type.value="";const quote=definition[pos];consumeCharacter(quote);while(c=definition[pos],c&&c!==quote){type.value+=c;pos++}consumeCharacter(quote)}else if(definition[pos]>="0"&&definition[pos]<="9"){type.typeOf="number";let nr="";while(c=definition[pos],c==="."||c==="n"||c>="0"&&c<="9"){nr+=c;pos++}if(nr.endsWith("n")){type.value=BigInt(nr)}else if(nr.includes(".")){type.value=parseFloat(nr)}else{type.value=parseInt(nr)}}else if(definition[pos]==="{"){consumeCharacter("{");type.typeOf="object";type.instanceOf=Object;type.children=[];while(true){const prop=readProperty();const types=readTypes();type.children.push({name:prop.name,optional:prop.optional,wildcard:prop.wildcard,types:types});consumeSpaces();if(definition[pos]===";"||definition[pos]===","){consumeCharacter(definition[pos]);consumeSpaces()}if(definition[pos]==="}"){break}}consumeCharacter("}")}else if(definition[pos]==="/"){consumeCharacter("/");let pattern="",flags="";while(c=definition[pos],c!=="/"||pattern.endsWith("\\")){pattern+=c;pos++}consumeCharacter("/");while(c=definition[pos],["g","i","m","s","u","y","d"].includes(c)){flags+=c;pos++}type.typeOf="string";type.matches=new RegExp(pattern,flags)}else{throw new Error(`Expected a type definition at position ${pos}, found character '${definition[pos]}'`)}}else if(["string","number","boolean","bigint","undefined","String","Number","Boolean","BigInt"].includes(name)){type.typeOf=name.toLowerCase()}else if(name==="Object"||name==="object"){type.typeOf="object";type.instanceOf=Object}else if(name==="Date"){type.typeOf="object";type.instanceOf=Date}else if(name==="Binary"||name==="binary"){type.typeOf="object";type.instanceOf=ArrayBuffer}else if(name==="any"){type.typeOf="any"}else if(name==="null"){type.typeOf="object";type.value=null}else if(name==="Array"){consumeCharacter("<");type.typeOf="object";type.instanceOf=Array;type.genericTypes=readTypes();consumeCharacter(">")}else if(["true","false"].includes(name)){type.typeOf="boolean";type.value=name==="true"}else{throw new Error(`Unknown type at position ${pos}: "${type}"`)}consumeSpaces();while(definition[pos]==="["){consumeCharacter("[");consumeCharacter("]");type={typeOf:"object",instanceOf:Array,genericTypes:[type]}}return type}function readTypes(){consumeSpaces();const types=[readType()];while(definition[pos]==="|"){consumeCharacter("|");types.push(readType());consumeSpaces()}return types}return readType()}function checkObject(path,properties,obj,partial){const invalidProperties=properties.find((prop=>prop.name==="*"||prop.name[0]==="$"))?[]:Object.keys(obj).filter((key=>![null,undefined].includes(obj[key])&&!properties.find((prop=>prop.name===key))));if(invalidProperties.length>0){return{ok:false,reason:`Object at path "${path}" cannot have propert${invalidProperties.length===1?"y":"ies"} ${invalidProperties.map((p=>`"${p}"`)).join(", ")}`}}function checkProperty(property){const hasValue=![null,undefined].includes(obj[property.name]);if(!property.optional&&(partial?obj[property.name]===null:!hasValue)){return{ok:false,reason:`Property at path "${path}/${property.name}" is not optional`}}if(hasValue&&property.types.length===1){return checkType(`${path}/${property.name}`,property.types[0],obj[property.name],false)}if(hasValue&&!property.types.some((type=>checkType(`${path}/${property.name}`,type,obj[property.name],false).ok))){return{ok:false,reason:`Property at path "${path}/${property.name}" does not match any of ${property.types.length} allowed types`}}return{ok:true}}const namedProperties=properties.filter((prop=>!prop.wildcard));const failedProperty=namedProperties.find((prop=>!checkProperty(prop).ok));if(failedProperty){const reason=checkProperty(failedProperty).reason;return{ok:false,reason:reason}}const wildcardProperty=properties.find((prop=>prop.wildcard));if(!wildcardProperty){return{ok:true}}const wildcardChildKeys=Object.keys(obj).filter((key=>!namedProperties.find((prop=>prop.name===key))));let result={ok:true};for(let i=0;i0){if(type.typeOf!=="object"){return{ok:false,reason:`path "${path}" must be typeof ${type.typeOf}`}}if(!type.children){return ok}const childKey=trailKeys[0];let property=type.children.find((prop=>prop.name===childKey));if(!property){property=type.children.find((prop=>prop.name==="*"||prop.name[0]==="$"))}if(!property){return{ok:false,reason:`Object at path "${path}" cannot have property "${childKey}"`}}if(property.optional&&value===null&&trailKeys.length===1){return ok}let result;property.types.some((type=>{const childPath=typeof childKey==="number"?`${path}[${childKey}]`:`${path}/${childKey}`;result=checkType(childPath,type,value,partial,trailKeys.slice(1));return result.ok}));return result}if(value===null){return ok}if(type.instanceOf===Object&&(typeof value!=="object"||value instanceof Array||value instanceof Date)){return{ok:false,reason:`path "${path}" must be an object collection`}}if(type.instanceOf&&(typeof value!=="object"||value.constructor!==type.instanceOf)){return{ok:false,reason:`path "${path}" must be an instance of ${type.instanceOf.name}`}}if("value"in type&&value!==type.value){return{ok:false,reason:`path "${path}" must be value: ${type.value}`}}if(typeof value!==type.typeOf){return{ok:false,reason:`path "${path}" must be typeof ${type.typeOf}`}}if(type.instanceOf===Array&&type.genericTypes&&!value.every((v=>type.genericTypes.some((t=>checkType(path,t,v,false).ok))))){return{ok:false,reason:`every array value of path "${path}" must match one of the specified types`}}if(type.typeOf==="object"&&type.children){return checkObject(path,type.children,value,partial)}if(type.matches&&!type.matches.test(value)){return{ok:false,reason:`path "${path}" must match regular expression /${type.matches.source}/${type.matches.flags}`}}return ok}function getConstructorType(val){switch(val){case String:return"string";case Number:return"number";case Boolean:return"boolean";case Date:return"Date";case BigInt:return"bigint";case Array:throw new Error("Schema error: Array cannot be used without a type. Use string[] or Array instead");default:throw new Error(`Schema error: unknown type used: ${val.name}`)}}class SchemaDefinition{constructor(definition,handling={warnOnly:false}){this.handling=handling;this.source=definition;if(typeof definition==="object"){const toTS=obj=>"{"+Object.keys(obj).map((key=>{let val=obj[key];if(val===undefined){val="undefined"}else if(val instanceof RegExp){val=`/${val.source}/${val.flags}`}else if(typeof val==="object"){val=toTS(val)}else if(typeof val==="function"){val=getConstructorType(val)}else if(!["string","number","boolean","bigint"].includes(typeof val)){throw new Error(`Type definition for key "${key}" must be a string, number, boolean, bigint, object, regular expression, or one of these classes: String, Number, Boolean, Date, BigInt`)}return`${key}:${val}`})).join(",")+"}";this.text=toTS(definition)}else if(typeof definition==="string"){this.text=definition}else{throw new Error("Type definiton must be a string or an object")}this.type=parse(this.text)}check(path,value,partial,trailKeys){const result=checkType(path,this.type,value,partial,trailKeys);if(!result.ok&&this.handling.warnOnly){result.warning=`${partial?"Partial schema":"Schema"} check on path "${path}"${trailKeys?` for child "${trailKeys.join("/")}"`:""} failed: ${result.reason}`;result.ok=true;this.handling.warnCallback(result.warning)}return result}}exports.SchemaDefinition=SchemaDefinition},{}],54:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.SimpleCache=void 0;const utils_1=require("./utils");const calculateExpiryTime=expirySeconds=>expirySeconds>0?Date.now()+expirySeconds*1e3:Infinity;class SimpleCache{get size(){return this.cache.size}constructor(options){var _a;this.enabled=true;if(typeof options==="number"){options={expirySeconds:options}}options.cloneValues=options.cloneValues!==false;if(typeof options.expirySeconds!=="number"&&typeof options.maxEntries!=="number"){throw new Error("Either expirySeconds or maxEntries must be specified")}this.options=options;this.cache=new Map;const interval=setInterval((()=>{this.cleanUp()}),60*1e3);(_a=interval.unref)===null||_a===void 0?void 0:_a.call(interval)}has(key){if(!this.enabled){return false}return this.cache.has(key)}get(key){if(!this.enabled){return null}const entry=this.cache.get(key);if(!entry){return null}entry.expires=calculateExpiryTime(this.options.expirySeconds);entry.accessed=Date.now();return this.options.cloneValues?(0,utils_1.cloneObject)(entry.value):entry.value}set(key,value){if(this.options.maxEntries>0&&this.cache.size>=this.options.maxEntries&&!this.cache.has(key)){let oldest=null;const now=Date.now();for(const[key,entry]of this.cache.entries()){if(entry.expires<=now){this.cache.delete(key);oldest=null;break}if(!oldest||entry.accessed{if(entry.expires<=now){this.cache.delete(key)}}))}}exports.SimpleCache=SimpleCache},{"./utils":61}],55:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.Colorize=exports.SetColorsEnabled=exports.ColorsSupported=exports.ColorStyle=void 0;const process_1=require("./process");const FontCode={bold:1,dim:2,italic:3,underline:4,inverse:7,hidden:8,strikethrough:94};const ColorCode={black:30,red:31,green:32,yellow:33,blue:34,magenta:35,cyan:36,white:37,grey:90,brightRed:91};const BgColorCode={bgBlack:40,bgRed:41,bgGreen:42,bgYellow:43,bgBlue:44,bgMagenta:45,bgCyan:46,bgWhite:47,bgGrey:100,bgBrightRed:101};const ResetCode={all:0,color:39,background:49,bold:22,dim:22,italic:23,underline:24,inverse:27,hidden:28,strikethrough:29};var ColorStyle;(function(ColorStyle){ColorStyle["reset"]="reset";ColorStyle["bold"]="bold";ColorStyle["dim"]="dim";ColorStyle["italic"]="italic";ColorStyle["underline"]="underline";ColorStyle["inverse"]="inverse";ColorStyle["hidden"]="hidden";ColorStyle["strikethrough"]="strikethrough";ColorStyle["black"]="black";ColorStyle["red"]="red";ColorStyle["green"]="green";ColorStyle["yellow"]="yellow";ColorStyle["blue"]="blue";ColorStyle["magenta"]="magenta";ColorStyle["cyan"]="cyan";ColorStyle["grey"]="grey";ColorStyle["bgBlack"]="bgBlack";ColorStyle["bgRed"]="bgRed";ColorStyle["bgGreen"]="bgGreen";ColorStyle["bgYellow"]="bgYellow";ColorStyle["bgBlue"]="bgBlue";ColorStyle["bgMagenta"]="bgMagenta";ColorStyle["bgCyan"]="bgCyan";ColorStyle["bgWhite"]="bgWhite";ColorStyle["bgGrey"]="bgGrey"})(ColorStyle=exports.ColorStyle||(exports.ColorStyle={}));function ColorsSupported(){if(typeof process_1.default==="undefined"||!process_1.default.stdout||!process_1.default.env||!process_1.default.platform||process_1.default.platform==="browser"){return false}if(process_1.default.platform==="win32"){return true}const env=process_1.default.env;if(env.COLORTERM){return true}if(env.TERM==="dumb"){return false}if(env.CI||env.TEAMCITY_VERSION){return!!env.TRAVIS}if(["iTerm.app","HyperTerm","Hyper","MacTerm","Apple_Terminal","vscode"].includes(env.TERM_PROGRAM)){return true}if(/^xterm-256|^screen|^xterm|^vt100|color|ansi|cygwin|linux/i.test(env.TERM)){return true}return false}exports.ColorsSupported=ColorsSupported;let _enabled=ColorsSupported();function SetColorsEnabled(enabled){_enabled=ColorsSupported()&&enabled}exports.SetColorsEnabled=SetColorsEnabled;function Colorize(str,style){if(!_enabled){return str}const openCodes=[],closeCodes=[];const addStyle=style=>{if(style===ColorStyle.reset){openCodes.push(ResetCode.all)}else if(style in FontCode){openCodes.push(FontCode[style]);closeCodes.push(ResetCode[style])}else if(style in ColorCode){openCodes.push(ColorCode[style]);closeCodes.push(ResetCode.color)}else if(style in BgColorCode){openCodes.push(BgColorCode[style]);closeCodes.push(ResetCode.background)}};if(style instanceof Array){style.forEach(addStyle)}else{addStyle(style)}const open=openCodes.map((code=>"["+code+"m")).join("");const close=closeCodes.map((code=>"["+code+"m")).join("");return str.split("\n").map((line=>open+line+close)).join("\n")}exports.Colorize=Colorize;String.prototype.colorize=function(style){return Colorize(this,style)}},{"./process":52}],56:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.SimpleEventEmitter=void 0;function runCallback(callback,data){try{callback(data)}catch(err){console.error("Error in subscription callback",err)}}const _subscriptions=Symbol("subscriptions");const _oneTimeEvents=Symbol("oneTimeEvents");class SimpleEventEmitter{constructor(){this[_subscriptions]=[];this[_oneTimeEvents]=new Map}on(event,callback){if(this[_oneTimeEvents].has(event)){return runCallback(callback,this[_oneTimeEvents].get(event))}this[_subscriptions].push({event:event,callback:callback,once:false});return this}off(event,callback){this[_subscriptions]=this[_subscriptions].filter((s=>s.event!==event||callback&&s.callback!==callback));return this}once(event,callback){return new Promise((resolve=>{const ourCallback=data=>{resolve(data);callback===null||callback===void 0?void 0:callback(data)};if(this[_oneTimeEvents].has(event)){runCallback(ourCallback,this[_oneTimeEvents].get(event))}else{this[_subscriptions].push({event:event,callback:ourCallback,once:true})}}))}emit(event,data){if(this[_oneTimeEvents].has(event)){throw new Error(`Event "${event}" was supposed to be emitted only once`)}for(let i=0;i{eventEmitter.emit(event,data)}))}pipeOnce(event,eventEmitter){this.once(event,(data=>{eventEmitter.emitOnce(event,data)}))}}exports.SimpleEventEmitter=SimpleEventEmitter},{}],57:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.SimpleObservable=void 0;class SimpleObservable{constructor(create){this._active=false;this._subscribers=[];this._create=create}subscribe(subscriber){if(!this._active){const next=value=>{this._subscribers.forEach((s=>{try{s(value)}catch(err){console.error("Error in subscriber callback:",err)}}))};const observer={next:next};this._cleanup=this._create(observer);this._active=true}this._subscribers.push(subscriber);const unsubscribe=()=>{this._subscribers.splice(this._subscribers.indexOf(subscriber),1);if(this._subscribers.length===0){this._active=false;this._cleanup()}};const subscription={unsubscribe:unsubscribe};return subscription}}exports.SimpleObservable=SimpleObservable},{}],58:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.EventStream=exports.EventPublisher=exports.EventSubscription=void 0;class EventSubscription{constructor(stop){this.stop=stop;this._internal={state:"init",activatePromises:[]}}activated(callback){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))}const noop=()=>{};this._internal.activatePromises.push({resolve:resolve,reject:callback?noop: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)}}}}exports.EventSubscription=EventSubscription;class EventPublisher{constructor(publish,start,cancel){this.publish=publish;this.start=start;this.cancel=cancel}}exports.EventPublisher=EventPublisher;class EventStream{constructor(eventPublisherCallback){const subscribers=[];let noMoreSubscribersCallback;let activationState;const STATE_STOPPED="stopped (no more subscribers)";this.subscribe=(callback,activationCallback)=>{if(typeof callback!=="function"){throw new TypeError("callback must be a function")}else if(activationState===STATE_STOPPED){throw new Error("stream can't be used anymore because all subscribers were stopped")}const sub={callback:callback,activationCallback:function(activated,cancelReason){activationCallback===null||activationCallback===void 0?void 0:activationCallback(activated,cancelReason);this.subscription._setActivationState(activated,cancelReason)},subscription:new EventSubscription((function stop(){subscribers.splice(subscribers.indexOf(this),1);return checkActiveSubscribers()}))};subscribers.push(sub);if(typeof activationState!=="undefined"){if(activationState===true){activationCallback===null||activationCallback===void 0?void 0:activationCallback(true);sub.subscription._setActivationState(true)}else if(typeof activationState==="string"){activationCallback===null||activationCallback===void 0?void 0:activationCallback(false,activationState);sub.subscription._setActivationState(false,activationState)}}return sub.subscription};const checkActiveSubscribers=()=>{let ret;if(subscribers.length===0){ret=noMoreSubscribersCallback===null||noMoreSubscribersCallback===void 0?void 0:noMoreSubscribersCallback();activationState=STATE_STOPPED}return Promise.resolve(ret)};this.unsubscribe=callback=>{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){console.error(`Error running subscriber callback: ${err.message}`)}}));if(subscribers.length===0){checkActiveSubscribers()}return subscribers.length>0};const start=allSubscriptionsStoppedCallback=>{activationState=true;noMoreSubscribersCallback=allSubscriptionsStoppedCallback;subscribers.forEach((sub=>{var _a;(_a=sub.activationCallback)===null||_a===void 0?void 0:_a.call(sub,true)}))};const cancel=reason=>{activationState=reason;subscribers.forEach((sub=>{var _a;(_a=sub.activationCallback)===null||_a===void 0?void 0:_a.call(sub,false,reason||new Error("unknown reason"))}));subscribers.splice(0)};const publisher=new EventPublisher(publish,start,cancel);eventPublisherCallback(publisher)}}exports.EventStream=EventStream},{}],59:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.deserialize2=exports.serialize2=exports.serialize=exports.detectSerializeVersion=exports.deserialize=void 0;const path_reference_1=require("./path-reference");const utils_1=require("./utils");const ascii85_1=require("./ascii85");const path_info_1=require("./path-info");const partial_array_1=require("./partial-array");const deserialize=data=>{if(data.map===null||typeof data.map==="undefined"){if(typeof data.val==="undefined"){throw new Error("serialized value must have a val property")}return data.val}const deserializeValue=(type,val)=>{if(type==="date"){return new Date(val)}else if(type==="binary"){return ascii85_1.ascii85.decode(val)}else if(type==="reference"){return new path_reference_1.PathReference(val)}else if(type==="regexp"){return new RegExp(val.pattern,val.flags)}else if(type==="array"){return new partial_array_1.PartialArray(val)}else if(type==="bigint"){return BigInt(val)}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_info_1.PathInfo.getPathKeys(path);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};exports.deserialize=deserialize;const detectSerializeVersion=data=>{if(typeof data!=="object"||data===null){return 2}if("map"in data&&"val"in data){return 1}else if("val"in data){if(Object.keys(data).length>1){return 2}return 1}return 2};exports.detectSerializeVersion=detectSerializeVersion;const serialize=obj=>{var _a;if(obj===null||typeof obj!=="object"||obj instanceof Date||obj instanceof ArrayBuffer||obj instanceof path_reference_1.PathReference||obj instanceof RegExp){const ser=(0,exports.serialize)({value:obj});return{map:(_a=ser.map)===null||_a===void 0?void 0:_a.value,val:ser.val.value}}obj=(0,utils_1.cloneObject)(obj);const process=(obj,mappings,prefix)=>{if(obj instanceof partial_array_1.PartialArray){mappings[prefix]="array"}Object.keys(obj).forEach((key=>{const val=obj[key];const path=prefix.length===0?key:`${prefix}/${key}`;if(typeof val==="bigint"){obj[key]=val.toString();mappings[path]="bigint"}else if(val instanceof Date){obj[key]=val.toISOString();mappings[path]="date"}else if(val instanceof ArrayBuffer){obj[key]=ascii85_1.ascii85.encode(val);mappings[path]="binary"}else if(val instanceof path_reference_1.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,"");const serialized={val:obj};if(Object.keys(mappings).length>0){serialized.map=mappings}return serialized};exports.serialize=serialize;const serialize2=obj=>{const getSerializedValue=val=>{if(typeof val==="bigint"){return{".type":"bigint",".val":val.toString()}}else if(val instanceof Date){return{".type":"date",".val":val.toISOString()}}else if(val instanceof ArrayBuffer){return{".type":"binary",".val":ascii85_1.ascii85.encode(val)}}else if(val instanceof path_reference_1.PathReference){return{".type":"reference",".val":val.path}}else if(val instanceof RegExp){return{".type":"regexp",".val":`/${val.source}/${val.flags}`}}else if(typeof val==="object"&&val!==null){if(val instanceof Array){const copy=[];for(let i=0;i{if(typeof data!=="object"||data===null){return data}if(typeof data[".type"]==="undefined"){if(data instanceof Array){const copy=[];const arr=data;for(let i=0;i{const mkeys=path_info_1.PathInfo.getPathKeys(mpath);if(mkeys.length!==keys.length){return false}return mkeys.every(((mkey,index)=>{if(mkey==="*"||typeof mkey==="string"&&mkey[0]==="$"){return true}return mkey===keys[index]}))}));const mapping=mappings[mappedPath];return mapping}function map(mappings,path){const targetPath=path_info_1.PathInfo.get(path).parentPath;if(targetPath===null){return}return get(mappings,targetPath)}function mapDeep(mappings,entryPath){entryPath=entryPath.replace(/^\/|\/$/g,"");const pathInfo=path_info_1.PathInfo.get(entryPath);const startPath=pathInfo.parentPath;const keys=startPath?path_info_1.PathInfo.getPathKeys(startPath):[];const matches=Object.keys(mappings).reduce(((m,mpath)=>{const mkeys=path_info_1.PathInfo.getPathKeys(mpath);if(mkeys.length{if(index>=keys.length){return false}else if(mkey==="*"||typeof mkey==="string"&&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}function process(db,mappings,path,obj,action){if(obj===null||typeof obj!=="object"){return obj}const keys=path_info_1.PathInfo.getPathKeys(path);const m=mapDeep(mappings,path);const changes=[];m.sort(((a,b)=>path_info_1.PathInfo.getPathKeys(a.path).length>path_info_1.PathInfo.getPathKeys(b.path).length?-1:1));m.forEach((mapping=>{const mkeys=path_info_1.PathInfo.getPathKeys(mapping.path);mkeys.push("*");const mTrailKeys=mkeys.slice(keys.length);if(mTrailKeys.length===0){const vars=path_info_1.PathInfo.extractVariables(mapping.path,path);const ref=new data_reference_1.DataReference(db,path,vars);if(action==="serialize"){obj=mapping.type.serialize(obj,ref)}else if(action==="deserialize"){const snap=new data_snapshot_1.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==="*"||typeof key==="string"&&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=path_info_1.PathInfo.getChildPath(parentPath,child.key);const vars=path_info_1.PathInfo.extractVariables(mapping.path,childPath);const ref=new data_reference_1.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 data_snapshot_1.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=(0,utils_1.cloneObject)(obj);if(changes.length>0){changes.forEach((change=>{change.parent[change.key]=change.original}))}}return obj}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")}}exports.TypeMappings=TypeMappings},{"./data-reference":42,"./data-snapshot":43,"./path-info":50,"./utils":61}],61:[function(require,module,exports){(function(global,Buffer){(function(){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.getGlobalObject=exports.defer=exports.getChildValues=exports.getMutations=exports.compareValues=exports.ObjectDifferences=exports.valuesAreEqual=exports.cloneObject=exports.concatTypedArrays=exports.decodeString=exports.encodeString=exports.bytesToBigint=exports.bigintToBytes=exports.bytesToNumber=exports.numberToBytes=void 0;const path_reference_1=require("./path-reference");const process_1=require("./process");const partial_array_1=require("./partial-array");function numberToBytes(number){const bytes=new Uint8Array(8);const view=new DataView(bytes.buffer);view.setFloat64(0,number);return new Array(...bytes)}exports.numberToBytes=numberToBytes;function bytesToNumber(bytes){const length=Array.isArray(bytes)?bytes.length:bytes.byteLength;if(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}exports.bytesToNumber=bytesToNumber;const hasBigIntSupport=(()=>{try{return typeof BigInt(0)==="bigint"}catch(err){return false}})();const noBigIntError="BigInt is not supported on this platform";const bigIntFunctions={bigintToBytes(number){throw new Error(noBigIntError)},bytesToBigint(bytes){throw new Error(noBigIntError)}};if(hasBigIntSupport){const big={zero:BigInt(0),one:BigInt(1),two:BigInt(2),eight:BigInt(8),ff:BigInt(255)};bigIntFunctions.bigintToBytes=function bigintToBytes(number){if(typeof number!=="bigint"){throw new Error("number must be a bigint")}const bytes=[];const negative=number>big.eight}while(number!==(negative?-big.one:big.zero));bytes.reverse();if(negative?bytes[0]<128:bytes[0]>=128){bytes.unshift(negative?255:0)}return bytes};bigIntFunctions.bytesToBigint=function bytesToBigint(bytes){const negative=bytes[0]>=128;let number=big.zero;for(let b of bytes){if(negative){b=~b&255}number=(number<128){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)}}exports.encodeString=encodeString;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 Array){buffer=Uint8Array.from(buffer)}if(!(buffer instanceof Buffer)&&"buffer"in buffer&&buffer.buffer instanceof ArrayBuffer){const typedArray=buffer;buffer=Buffer.from(typedArray.buffer,typedArray.byteOffset,typedArray.byteLength)}if(!(buffer instanceof Buffer)){throw new Error("Unsupported buffer argument")}return buffer.toString("utf-8")}else{if(!(buffer instanceof Uint8Array)&&"buffer"in buffer&&buffer["buffer"]instanceof ArrayBuffer){const typedArray=buffer;buffer=new Uint8Array(typedArray.buffer,typedArray.byteOffset,typedArray.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")}}}exports.decodeString=decodeString;function concatTypedArrays(a,b){const c=new a.constructor(a.length+b.length);c.set(a);c.set(b,a.length);return c}exports.concatTypedArrays=concatTypedArrays;function cloneObject(original,stack){var _a;if(((_a=original===null||original===void 0?void 0:original.constructor)===null||_a===void 0?void 0:_a.name)==="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 path_reference_1.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 path_reference_1.PathReference||val instanceof RegExp){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=original instanceof Array?[]:original instanceof partial_array_1.PartialArray?new partial_array_1.PartialArray:{};Object.keys(original).forEach((key=>{const val=original[key];if(typeof val==="function"){return}clone[key]=cloneValue(val)}));return clone}exports.cloneObject=cloneObject;const isTypedArray=val=>typeof val==="object"&&["ArrayBuffer","Buffer","Uint8Array","Uint16Array","Uint32Array","Int8Array","Int16Array","Int32Array"].includes(val.constructor.name);function valuesAreEqual(val1,val2){if(val1===val2){return true}if(typeof val1!==typeof val2){return false}if(typeof val1==="object"||typeof val2==="object"){if(val1===null||val2===null){return false}if(val1 instanceof path_reference_1.PathReference||val2 instanceof path_reference_1.PathReference){return val1 instanceof path_reference_1.PathReference&&val2 instanceof path_reference_1.PathReference&&val1.path===val2.path}if(val1 instanceof Date||val2 instanceof Date){return val1 instanceof Date&&val2 instanceof Date&&val1.getTime()===val2.getTime()}if(val1 instanceof Array||val2 instanceof Array){return val1 instanceof Array&&val2 instanceof Array&&val1.length===val2.length&&val1.every(((item,i)=>valuesAreEqual(val1[i],val2[i])))}if(isTypedArray(val1)||isTypedArray(val2)){if(!isTypedArray(val1)||!isTypedArray(val2)||val1.byteLength===val2.byteLength){return false}const typed1=val1 instanceof ArrayBuffer?new Uint8Array(val1):new Uint8Array(val1.buffer,val1.byteOffset,val1.byteLength),typed2=val2 instanceof ArrayBuffer?new Uint8Array(val2):new Uint8Array(val2.buffer,val2.byteOffset,val2.byteLength);return typed1.every(((val,i)=>typed2[i]===val))}const keys1=Object.keys(val1),keys2=Object.keys(val2);return keys1.length===keys2.length&&keys1.every((key=>keys2.includes(key)))&&keys1.every((key=>valuesAreEqual(val1[key],val2[key])))}return false}exports.valuesAreEqual=valuesAreEqual;class ObjectDifferences{constructor(added,removed,changed){this.added=added;this.removed=removed;this.changed=changed}forChild(key){if(this.added.includes(key)){return"added"}if(this.removed.includes(key)){return"removed"}const changed=this.changed.find((ch=>ch.key===key));return changed?changed.change:"identical"}}exports.ObjectDifferences=ObjectDifferences;function compareValues(oldVal,newVal,sortedResults=false){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(isTypedArray(oldVal)||isTypedArray(newVal)){if(!isTypedArray(oldVal)||!isTypedArray(newVal)){return"changed"}const typed1=oldVal instanceof Uint8Array?oldVal:oldVal instanceof ArrayBuffer?new Uint8Array(oldVal):new Uint8Array(oldVal.buffer,oldVal.byteOffset,oldVal.byteLength);const typed2=newVal instanceof Uint8Array?newVal:newVal instanceof ArrayBuffer?new Uint8Array(newVal):new Uint8Array(newVal.buffer,newVal.byteOffset,newVal.byteLength);return typed1.byteLength===typed2.byteLength&&typed1.every(((val,i)=>typed2[i]===val))?"identical":"changed"}else if(oldVal instanceof Date||newVal instanceof Date){return oldVal instanceof Date&&newVal instanceof Date&&oldVal.getTime()===newVal.getTime()?"identical":"changed"}else if(oldVal instanceof path_reference_1.PathReference||newVal instanceof path_reference_1.PathReference){return oldVal instanceof path_reference_1.PathReference&&newVal instanceof path_reference_1.PathReference&&oldVal.path===newVal.path?"identical":"changed"}else if(typeof oldVal==="object"){const isArray=oldVal instanceof Array;const getKeys=obj=>{let keys=Object.keys(obj).filter((key=>!voids.includes(obj[key])));if(isArray){keys=keys.map((v=>parseInt(v)))}return keys};const oldKeys=getKeys(oldVal);const newKeys=getKeys(newVal);const removedKeys=oldKeys.filter((key=>!newKeys.includes(key)));const addedKeys=newKeys.filter((key=>!oldKeys.includes(key)));const changedKeys=newKeys.reduce(((changed,key)=>{if(oldKeys.includes(key)){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 new ObjectDifferences(addedKeys,removedKeys,sortedResults?changedKeys.sort(((a,b)=>a.key{switch(compareResult){case"identical":return[];case"changed":return[{target:target,prev:prev,val:val}];case"added":return[{target:target,prev:null,val:val}];case"removed":return[{target:target,prev:prev,val:null}];default:{let changes=[];compareResult.added.forEach((key=>changes.push({target:target.concat(key),prev:null,val:val[key]})));compareResult.removed.forEach((key=>changes.push({target:target.concat(key),prev:prev[key],val:null})));compareResult.changed.forEach((item=>{const childChanges=process(target.concat(item.key),item.change,prev[item.key],val[item.key]);changes=changes.concat(childChanges)}));return changes}}};const compareResult=compareValues(oldVal,newVal,sortedResults);return process([],compareResult,oldVal,newVal)}exports.getMutations=getMutations;function 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}}exports.getChildValues=getChildValues;function defer(fn){process_1.default.nextTick(fn)}exports.defer=defer;function getGlobalObject(){var _a;if(typeof globalThis!=="undefined"){return globalThis}if(typeof global!=="undefined"){return global}if(typeof window!=="undefined"){return window}if(typeof self!=="undefined"){return self}return(_a=function(){return this}())!==null&&_a!==void 0?_a:Function("return this")()}exports.getGlobalObject=getGlobalObject}).call(this)}).call(this,typeof global!=="undefined"?global:typeof self!=="undefined"?self:typeof window!=="undefined"?window:{},require("buffer").Buffer)},{"./partial-array":49,"./path-reference":51,"./process":52,buffer:62}],62:[function(require,module,exports){},{}]},{},[6])(6)})); +(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{this._ready=true}))}async ready(callback){if(!this._ready){await new Promise((resolve=>this.on("ready",resolve)))}callback===null||callback===void 0?void 0:callback()}get isReady(){return this._ready}setObservable(ObservableImpl){(0,optional_observable_1.setObservable)(ObservableImpl)}ref(path){return new data_reference_1.DataReference(this,path)}get root(){return this.ref("")}query(path){const ref=new data_reference_1.DataReference(this,path);return new data_reference_1.DataReferenceQuery(ref)}get indexes(){return{get:()=>this.api.getIndexes(),create:(path,key,options)=>this.api.createIndex(path,key,options),delete:async filePath=>this.api.deleteIndex(filePath)}}get schema(){return{get:path=>this.api.getSchema(path),set:(path,schema,warnOnly=false)=>this.api.setSchema(path,schema,warnOnly),all:()=>this.api.getSchemas(),check:(path,value,isUpdate)=>this.api.validateSchema(path,value,isUpdate)}}}exports.AceBaseBase=AceBaseBase},{"./data-reference":8,"./debug":10,"./optional-observable":14,"./simple-colors":21,"./simple-event-emitter":22,"./type-mappings":26}],2:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.Api=void 0;const simple_event_emitter_1=require("./simple-event-emitter");class NotImplementedError extends Error{constructor(name){super(`${name} is not implemented`)}}class Api extends simple_event_emitter_1.SimpleEventEmitter{constructor(){super()}stats(options){throw new NotImplementedError("stats")}subscribe(path,event,callback,settings){throw new NotImplementedError("subscribe")}unsubscribe(path,event,callback){throw new NotImplementedError("unsubscribe")}update(path,updates,options){throw new NotImplementedError("update")}set(path,value,options){throw new NotImplementedError("set")}get(path,options){throw new NotImplementedError("get")}transaction(path,callback,options){throw new NotImplementedError("transaction")}exists(path){throw new NotImplementedError("exists")}query(path,query,options){throw new NotImplementedError("query")}reflect(path,type,args){throw new NotImplementedError("reflect")}export(path,write,options){throw new NotImplementedError("export")}import(path,read,options){throw new NotImplementedError("import")}createIndex(path,key,options){throw new NotImplementedError("createIndex")}getIndexes(){throw new NotImplementedError("getIndexes")}deleteIndex(filePath){throw new NotImplementedError("deleteIndex")}setSchema(path,schema,warnOnly){throw new NotImplementedError("setSchema")}getSchema(path){throw new NotImplementedError("getSchema")}getSchemas(){throw new NotImplementedError("getSchemas")}validateSchema(path,value,isUpdate){throw new NotImplementedError("validateSchema")}getMutations(filter){throw new NotImplementedError("getMutations")}getChanges(filter){throw new NotImplementedError("getChanges")}}exports.Api=Api},{"./simple-event-emitter":22}],3:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.ascii85=void 0;function c(input,length,result){const b=[0,0,0,0,0];for(let i=0;i";return ret}exports.ascii85={encode:function(arr){if(arr instanceof ArrayBuffer){arr=new Uint8Array(arr,0,arr.byteLength)}return encode(arr)},decode:function(input){if(!input.startsWith("<~")||!input.endsWith("~>")){throw new Error("Invalid input string")}input=input.substr(2,input.length-4);const n=input.length,r=[],b=[0,0,0,0,0];let t,x,y,d;for(let i=0;i>>=8;y=t&255;t>>>=8;r.push(t>>>8,t&255,y,x);for(let j=d;j<5;++j,r.pop()){}i+=4}const data=new Uint8Array(r);return data.buffer.slice(data.byteOffset,data.byteOffset+data.byteLength)}}},{}],4:[function(require,module,exports){"use strict";var _a,_b;Object.defineProperty(exports,"__esModule",{value:true});const pad_1=require("../pad");const env=typeof window==="object"?window:self,globalCount=Object.keys(env).length,mimeTypesLength=(_b=(_a=navigator.mimeTypes)===null||_a===void 0?void 0:_a.length)!==null&&_b!==void 0?_b:0,clientId=(0,pad_1.default)((mimeTypesLength+navigator.userAgent.length).toString(36)+globalCount.toString(36),4);function fingerprint(){return clientId}exports.default=fingerprint},{"../pad":6}],5:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});const fingerprint_1=require("./fingerprint");const pad_1=require("./pad");let c=0;const blockSize=4,base=36,discreteValues=Math.pow(base,blockSize);function randomBlock(){return(0,pad_1.default)((Math.random()*discreteValues<<0).toString(base),blockSize)}function safeCounter(){c=ct2[i]===key))}static isAncestor(ancestor,other){return ancestor.lengthother[i]===key))}static isDescendant(descendant,other){return descendant.length>other.length&&other.every(((key,i)=>descendant[i]===key))}}const isProxy=Symbol("isProxy");class LiveDataProxy{static async create(ref,options){var _a;ref=new data_reference_1.DataReference(ref.db,ref.path);let cache,loaded=false;let latestCursor=options===null||options===void 0?void 0:options.cursor;let proxy;const proxyId=id_1.ID.generate();const clientSubscriptions=[];const clientEventEmitter=new simple_event_emitter_1.SimpleEventEmitter;clientEventEmitter.on("cursor",(cursor=>latestCursor=cursor));clientEventEmitter.on("error",(err=>{console.error(err.message,err.details)}));const applyChange=(keys,newValue)=>{if(keys.length===0){cache=newValue;return true}const allowCreation=false;if(allowCreation){cache=typeof keys[0]==="number"?[]:{}}let target=cache;const trailKeys=keys.slice();while(trailKeys.length>1){const key=trailKeys.shift();if(!(key in target)){if(allowCreation){target[key]=typeof key==="number"?[]:{}}else{return false}}target=target[key]}const prop=trailKeys.shift();if(newValue===null){target instanceof Array?target.splice(prop,1):delete target[prop]}else{target[prop]=newValue}return true};const syncFallback=async()=>{if(!loaded){return}await reload()};const subscription=ref.on("mutations",{syncFallback:syncFallback}).subscribe((async snap=>{var _a;if(!loaded){return}const context=snap.context();const isRemote=((_a=context.acebase_proxy)===null||_a===void 0?void 0:_a.id)!==proxyId;if(!isRemote){return}const mutations=snap.val(false);const proceed=mutations.every((mutation=>{if(!applyChange(mutation.target,mutation.val)){return false}const changeRef=mutation.target.reduce(((ref,key)=>ref.child(key)),ref);const changeSnap=new data_snapshot_1.DataSnapshot(changeRef,mutation.val,false,mutation.prev,snap.context());clientEventEmitter.emit("mutation",{snapshot:changeSnap,isRemote:isRemote});return true}));if(proceed){clientEventEmitter.emit("cursor",context.acebase_cursor);localMutationsEmitter.emit("mutations",{origin:"remote",snap:snap})}else{console.warn(`Cached value of live data proxy on "${ref.path}" appears outdated, will be reloaded`);await reload()}}));let processPromise=Promise.resolve();const mutationQueue=[];const transactions=[];const pushLocalMutations=async()=>{const mutations=[];for(let i=0,m=mutationQueue[0];iRelativeNodeTarget.areEqual(t.target,m.target)||RelativeNodeTarget.isAncestor(t.target,m.target)))){mutationQueue.splice(i,1);i--;mutations.push(m)}}if(mutations.length===0){return}mutations.forEach((mutation=>{mutation.value=(0,utils_1.cloneObject)(getTargetValue(cache,mutation.target))}));process_1.default.nextTick((()=>{const context={acebase_proxy:{id:proxyId,source:"update"}};mutations.forEach((mutation=>{const mutationRef=mutation.target.reduce(((ref,key)=>ref.child(key)),ref);const mutationSnap=new data_snapshot_1.DataSnapshot(mutationRef,mutation.value,false,mutation.previous,context);clientEventEmitter.emit("mutation",{snapshot:mutationSnap,isRemote:false})}));const snap=new data_snapshot_1.MutationsDataSnapshot(ref,mutations.map((m=>({target:m.target,val:m.value,prev:m.previous}))),context);localMutationsEmitter.emit("mutations",{origin:"local",snap:snap})}));processPromise=mutations.reduce(((mutations,m,i,arr)=>{if(!arr.some((other=>RelativeNodeTarget.isAncestor(other.target,m.target)))){mutations.push(m)}return mutations}),[]).reduce(((updates,m)=>{const target=m.target;if(target.length===0){updates.push({ref:ref,target:target,value:cache,type:"set",previous:m.previous})}else{const parentTarget=target.slice(0,-1);const key=target.slice(-1)[0];const parentRef=parentTarget.reduce(((ref,key)=>ref.child(key)),ref);const parentUpdate=updates.find((update=>update.ref.path===parentRef.path));const cacheValue=getTargetValue(cache,target);const prevValue=m.previous;if(parentUpdate){parentUpdate.value[key]=cacheValue;parentUpdate.previous[key]=prevValue}else{updates.push({ref:parentRef,target:parentTarget,value:{[key]:cacheValue},type:"update",previous:{[key]:prevValue}})}}return updates}),[]).reduce((async(promise,update)=>{const context={acebase_proxy:{id:proxyId,source:update.type}};await promise;await update.ref.context(context)[update.type](update.value).catch((async err=>{if(options===null||options===void 0?void 0:options.shouldRollback){const rollback=await options.shouldRollback(err,{type:update.type,ref:update.ref,value:update.value,previous:update.previous});if(rollback===false){return}}clientEventEmitter.emit("error",{source:"update",message:`Error processing update of "/${ref.path}"`,details:err});const context={acebase_proxy:{id:proxyId,source:"update-rollback"}};const mutations=[];if(update.type==="set"){setTargetValue(cache,update.target,update.previous);const mutationSnap=new data_snapshot_1.DataSnapshot(update.ref,update.previous,false,update.value,context);clientEventEmitter.emit("mutation",{snapshot:mutationSnap,isRemote:false});mutations.push({target:update.target,val:update.previous,prev:update.value})}else{Object.keys(update.previous).forEach((key=>{setTargetValue(cache,update.target.concat(key),update.previous[key]);const mutationSnap=new data_snapshot_1.DataSnapshot(update.ref.child(key),update.previous[key],false,update.value[key],context);clientEventEmitter.emit("mutation",{snapshot:mutationSnap,isRemote:false});mutations.push({target:update.target.concat(key),val:update.previous[key],prev:update.value[key]})}))}mutations.forEach((m=>{const mutationRef=m.target.reduce(((ref,key)=>ref.child(key)),ref);const mutationSnap=new data_snapshot_1.DataSnapshot(mutationRef,m.val,false,m.prev,context);clientEventEmitter.emit("mutation",{snapshot:mutationSnap,isRemote:false})}));const snap=new data_snapshot_1.MutationsDataSnapshot(update.ref,mutations,context);localMutationsEmitter.emit("mutations",{origin:"local",snap:snap})}));if(update.ref.cursor){clientEventEmitter.emit("cursor",update.ref.cursor)}}),processPromise);await processPromise};let syncInProgress=false;const syncPromises=[];const syncCompleted=()=>{let resolve;const promise=new Promise((rs=>resolve=rs));syncPromises.push({resolve:resolve});return promise};let processQueueTimeout=null;const scheduleSync=()=>{if(!processQueueTimeout){processQueueTimeout=setTimeout((async()=>{syncInProgress=true;processQueueTimeout=null;await pushLocalMutations();syncInProgress=false;syncPromises.splice(0).forEach((p=>p.resolve()))}),0)}};const flagOverwritten=target=>{if(!mutationQueue.find((m=>RelativeNodeTarget.areEqual(m.target,target)))){mutationQueue.push({target:target,previous:(0,utils_1.cloneObject)(getTargetValue(cache,target))})}scheduleSync()};const localMutationsEmitter=new simple_event_emitter_1.SimpleEventEmitter;const addOnChangeHandler=(target,callback)=>{const isObject=val=>val!==null&&typeof val==="object";const mutationsHandler=async details=>{var _a;const{snap:snap,origin:origin}=details;const context=snap.context();const causedByOurProxy=((_a=context.acebase_proxy)===null||_a===void 0?void 0:_a.id)===proxyId;if(details.origin==="remote"&&causedByOurProxy){console.error("DEV ISSUE: mutationsHandler was called from remote event originating from our own proxy");return}const mutations=snap.val(false).filter((mutation=>mutation.target.slice(0,target.length).every(((key,i)=>target[i]===key))));if(mutations.length===0){return}let newValue,previousValue;const singleMutation=mutations.find((m=>m.target.length<=target.length));if(singleMutation){const trailKeys=target.slice(singleMutation.target.length);newValue=trailKeys.reduce(((val,key)=>!isObject(val)||!(key in val)?null:val[key]),singleMutation.val);previousValue=trailKeys.reduce(((val,key)=>!isObject(val)||!(key in val)?null:val[key]),singleMutation.prev)}else{const currentValue=getTargetValue(cache,target);newValue=(0,utils_1.cloneObject)(currentValue);previousValue=(0,utils_1.cloneObject)(newValue);mutations.forEach((mutation=>{const trailKeys=mutation.target.slice(target.length);for(let i=0,val=newValue,prev=previousValue;i{let keepSubscription=true;try{keepSubscription=false!==callback(Object.freeze(newValue),Object.freeze(previousValue),!causedByOurProxy,context)}catch(err){clientEventEmitter.emit("error",{source:origin==="remote"?"remote_update":"local_update",message:"Error running subscription callback",details:err})}if(keepSubscription===false){stop()}}))};localMutationsEmitter.on("mutations",mutationsHandler);const stop=()=>{localMutationsEmitter.off("mutations",mutationsHandler);clientSubscriptions.splice(clientSubscriptions.findIndex((cs=>cs.stop===stop)),1)};clientSubscriptions.push({target:target,stop:stop});return{stop:stop}};const handleFlag=(flag,target,args)=>{if(flag==="write"){return flagOverwritten(target)}else if(flag==="onChange"){return addOnChangeHandler(target,args.callback)}else if(flag==="subscribe"||flag==="observe"){const subscribe=subscriber=>{const currentValue=getTargetValue(cache,target);subscriber.next(currentValue);const subscription=addOnChangeHandler(target,(value=>{subscriber.next(value)}));return function unsubscribe(){subscription.stop()}};if(flag==="subscribe"){return subscribe}const Observable=(0,optional_observable_1.getObservable)();return new Observable(subscribe)}else if(flag==="transaction"){const hasConflictingTransaction=transactions.some((t=>RelativeNodeTarget.areEqual(target,t.target)||RelativeNodeTarget.isAncestor(target,t.target)||RelativeNodeTarget.isDescendant(target,t.target)));if(hasConflictingTransaction){return Promise.reject(new Error("Cannot start transaction because it conflicts with another transaction"))}return new Promise((async resolve=>{const hasPendingMutations=mutationQueue.some((m=>RelativeNodeTarget.areEqual(target,m.target)||RelativeNodeTarget.isAncestor(target,m.target)));if(hasPendingMutations){if(!syncInProgress){scheduleSync()}await syncCompleted()}const tx={target:target,status:"started",transaction:null};transactions.push(tx);tx.transaction={get status(){return tx.status},get completed(){return tx.status!=="started"},get mutations(){return mutationQueue.filter((m=>RelativeNodeTarget.areEqual(tx.target,m.target)||RelativeNodeTarget.isAncestor(tx.target,m.target)))},get hasMutations(){return this.mutations.length>0},async commit(){if(this.completed){throw new Error(`Transaction has completed already (status '${tx.status}')`)}tx.status="finished";transactions.splice(transactions.indexOf(tx),1);if(syncInProgress){await syncCompleted()}scheduleSync();await syncCompleted()},rollback(){if(this.completed){throw new Error(`Transaction has completed already (status '${tx.status}')`)}tx.status="canceled";const mutations=[];for(let i=0;i{if(m.target.length===0){cache=m.previous}else{setTargetValue(cache,m.target,m.previous)}}));transactions.splice(transactions.indexOf(tx),1)}};resolve(tx.transaction)}))}};const snap=await ref.get({cache_mode:"allow",cache_cursor:options===null||options===void 0?void 0:options.cursor});if(snap.context().acebase_origin!=="cache"){clientEventEmitter.emit("cursor",(_a=ref.cursor)!==null&&_a!==void 0?_a:null)}loaded=true;cache=snap.val();if(cache===null&&typeof(options===null||options===void 0?void 0:options.defaultValue)!=="undefined"){cache=options.defaultValue;const context={acebase_proxy:{id:proxyId,source:"default"}};await ref.context(context).set(cache)}proxy=createProxy({root:{ref:ref,get cache(){return cache}},target:[],id:proxyId,flag:handleFlag});const assertProxyAvailable=()=>{if(proxy===null){throw new Error("Proxy was destroyed")}};const reload=async()=>{assertProxyAvailable();mutationQueue.splice(0);const snap=await ref.get({allow_cache:false});const oldVal=cache,newVal=snap.val();cache=newVal;const mutations=(0,utils_1.getMutations)(oldVal,newVal);if(mutations.length===0){return}const context=snap.context();context.acebase_proxy={id:proxyId,source:"reload"};mutations.forEach((m=>{const targetRef=getTargetRef(ref,m.target);const newSnap=new data_snapshot_1.DataSnapshot(targetRef,m.val,m.val===null,m.prev,context);clientEventEmitter.emit("mutation",{snapshot:newSnap,isRemote:true})}));const mutationsSnap=new data_snapshot_1.MutationsDataSnapshot(ref,mutations,context);localMutationsEmitter.emit("mutations",{origin:"local",snap:mutationsSnap})};return{async destroy(){await processPromise;const promises=[subscription.stop(),...clientSubscriptions.map((cs=>cs.stop()))];await Promise.all(promises);["cursor","mutation","error"].forEach((event=>clientEventEmitter.off(event)));cache=null;proxy=null},stop(){this.destroy()},get value(){assertProxyAvailable();return proxy},get hasValue(){assertProxyAvailable();return cache!==null},set value(val){assertProxyAvailable();if(val!==null&&typeof val==="object"&&val[isProxy]){val=val.valueOf()}flagOverwritten([]);cache=val},get ref(){return ref},get cursor(){return latestCursor},reload:reload,onMutation(callback){assertProxyAvailable();clientEventEmitter.off("mutation");clientEventEmitter.on("mutation",(({snapshot:snapshot,isRemote:isRemote})=>{try{callback(snapshot,isRemote)}catch(err){clientEventEmitter.emit("error",{source:"mutation_callback",message:"Error in dataproxy onMutation callback",details:err})}}))},onError(callback){assertProxyAvailable();clientEventEmitter.off("error");clientEventEmitter.on("error",(err=>{try{callback(err)}catch(err){console.error(`Error in dataproxy onError callback: ${err.message}`)}}))},on(event,callback){clientEventEmitter.on(event,callback)},off(event,callback){clientEventEmitter.off(event,callback)}}}}exports.LiveDataProxy=LiveDataProxy;function getTargetValue(obj,target){let val=obj;for(const key of target){val=typeof val==="object"&&val!==null&&key in val?val[key]:null}return val}function setTargetValue(obj,target,value){if(target.length===0){throw new Error("Cannot update root target, caller must do that itself!")}const targetObject=target.slice(0,-1).reduce(((obj,key)=>obj[key]),obj);const prop=target.slice(-1)[0];if(value===null||typeof value==="undefined"){targetObject instanceof Array?targetObject.splice(prop,1):delete targetObject[prop]}else{targetObject[prop]=value}}function getTargetRef(ref,target){const path=path_info_1.PathInfo.get(ref.path).childPath(target);return new data_reference_1.DataReference(ref.db,path)}function createProxy(context){const targetRef=getTargetRef(context.root.ref,context.target);const childProxies=[];const handler={get(target,prop,receiver){target=getTargetValue(context.root.cache,context.target);if(typeof prop==="symbol"){if(prop.toString()===Symbol.iterator.toString()){prop="values"}else if(prop.toString()===isProxy.toString()){return true}else{return Reflect.get(target,prop,receiver)}}if(prop==="valueOf"){return function valueOf(){return target}}if(target===null||typeof target!=="object"){throw new Error(`Cannot read property "${prop}" of ${target}. Value of path "/${targetRef.path}" is not an object (anymore)`)}if(target instanceof Array&&typeof prop==="string"&&/^[0-9]+$/.test(prop)){prop=parseInt(prop)}const value=target[prop];if(value===null){delete target[prop];return}const childProxy=childProxies.find((proxy=>proxy.prop===prop));if(childProxy){if(childProxy.typeof===typeof value){return childProxy.value}childProxies.splice(childProxies.indexOf(childProxy),1)}const proxifyChildValue=prop=>{const value=target[prop];const childProxy=childProxies.find((child=>child.prop===prop));if(childProxy){if(childProxy.typeof===typeof value){return childProxy.value}childProxies.splice(childProxies.indexOf(childProxy),1)}if(typeof value!=="object"){return value}const newChildProxy=createProxy({root:context.root,target:context.target.concat(prop),id:context.id,flag:context.flag});childProxies.push({typeof:typeof value,prop:prop,value:newChildProxy});return newChildProxy};const unproxyValue=value=>value!==null&&typeof value==="object"&&value[isProxy]?value.getTarget():value;if(["string","number","boolean"].includes(typeof value)||value instanceof Date||value instanceof path_reference_1.PathReference||value instanceof ArrayBuffer||typeof value==="object"&&"buffer"in value){return value}const isArray=target instanceof Array;if(prop==="toString"){return function toString(){return`[LiveDataProxy for "${targetRef.path}"]`}}if(typeof value==="undefined"){if(prop==="push"){return function push(item){const childRef=targetRef.push();context.flag("write",context.target.concat(childRef.key));target[childRef.key]=item;return childRef.key}}if(prop==="getTarget"){return function(warn=true){warn&&console.warn("Use getTarget with caution - any changes will not be synchronized!");return target}}if(prop==="getRef"){return function getRef(){const ref=getTargetRef(context.root.ref,context.target);return ref}}if(prop==="forEach"){return function forEach(callback){const keys=Object.keys(target);let stop=false;for(let i=0;!stop&&iproxifyChildValue(key)));if(sortFn){arr.sort(sortFn)}return arr}}if(prop==="onChanged"){return function onChanged(callback){return context.flag("onChange",context.target,{callback:callback})}}if(prop==="subscribe"){return function subscribe(){return context.flag("subscribe",context.target)}}if(prop==="getObservable"){return function getObservable(){return context.flag("observe",context.target)}}if(prop==="getOrderedCollection"){return function getOrderedCollection(orderProperty,orderIncrement){return new OrderedCollectionProxy(this,orderProperty,orderIncrement)}}if(prop==="startTransaction"){return function startTransaction(){return context.flag("transaction",context.target)}}if(prop==="remove"&&!isArray){return function remove(){if(context.target.length===0){throw new Error("Can't remove proxy root value")}const parent=getTargetValue(context.root.cache,context.target.slice(0,-1));const key=context.target.slice(-1)[0];context.flag("write",context.target);delete parent[key]}}return}else if(typeof value==="function"){if(isArray){const writeArray=action=>{context.flag("write",context.target);return action()};const cleanArrayValues=values=>values.map((value=>{value=unproxyValue(value);removeVoidProperties(value);return value}));if(prop==="push"){return function push(...items){items=cleanArrayValues(items);return writeArray((()=>target.push(...items)))}}if(prop==="pop"){return function pop(){return writeArray((()=>target.pop()))}}if(prop==="splice"){return function splice(start,deleteCount,...items){items=cleanArrayValues(items);return writeArray((()=>target.splice(start,deleteCount,...items)))}}if(prop==="shift"){return function shift(){return writeArray((()=>target.shift()))}}if(prop==="unshift"){return function unshift(...items){items=cleanArrayValues(items);return writeArray((()=>target.unshift(...items)))}}if(prop==="sort"){return function sort(compareFn){return writeArray((()=>target.sort(compareFn)))}}if(prop==="reverse"){return function reverse(){return writeArray((()=>target.reverse()))}}if(["indexOf","lastIndexOf"].includes(prop)){return function indexOf(item,start){if(item!==null&&typeof item==="object"&&item[isProxy]){item=item.getTarget(false)}return target[prop](item,start)}}if(["forEach","every","some","filter","map"].includes(prop)){return function iterate(callback){return target[prop](((value,i)=>callback(proxifyChildValue(i),i,proxy)))}}if(["reduce","reduceRight"].includes(prop)){return function reduce(callback,initialValue){return target[prop](((prev,value,i)=>callback(prev,proxifyChildValue(i),i,proxy)),initialValue)}}if(["find","findIndex"].includes(prop)){return function find(callback){let value=target[prop](((value,i)=>callback(proxifyChildValue(i),i,proxy)));if(prop==="find"&&value){const index=target.indexOf(value);value=proxifyChildValue(index)}return value}}if(["values","entries","keys"].includes(prop)){return function*generator(){for(let i=0;itypeof key==="number"))){context.flag("write",context.target.slice(0,context.target.findIndex((key=>typeof key==="number"))))}else if(target instanceof Array){context.flag("write",context.target)}else{context.flag("write",context.target.concat(prop))}if(value===null){delete target[prop]}else{removeVoidProperties(value);target[prop]=value}return true},deleteProperty(target,prop){target=getTargetValue(context.root.cache,context.target);if(target===null){throw new Error(`Cannot delete property ${prop.toString()} of null`)}if(typeof prop==="symbol"){return Reflect.deleteProperty(target,prop)}if(!(prop in target)){return true}context.flag("write",context.target.concat(prop));delete target[prop];return true},ownKeys(target){target=getTargetValue(context.root.cache,context.target);return Reflect.ownKeys(target)},has(target,prop){target=getTargetValue(context.root.cache,context.target);return Reflect.has(target,prop)},getOwnPropertyDescriptor(target,prop){target=getTargetValue(context.root.cache,context.target);const descriptor=Reflect.getOwnPropertyDescriptor(target,prop);if(descriptor){descriptor.configurable=true}return descriptor},getPrototypeOf(target){target=getTargetValue(context.root.cache,context.target);return Reflect.getPrototypeOf(target)}};const proxy=new Proxy({},handler);return proxy}function removeVoidProperties(obj){if(typeof obj!=="object"){return}Object.keys(obj).forEach((key=>{const val=obj[key];if(val===null||typeof val==="undefined"){delete obj[key]}else if(typeof val==="object"){removeVoidProperties(val)}}))}function proxyAccess(proxiedValue){if(typeof proxiedValue!=="object"||!proxiedValue[isProxy]){throw new Error("Given value is not proxied. Make sure you are referencing the value through the live data proxy.")}return proxiedValue}exports.proxyAccess=proxyAccess;class OrderedCollectionProxy{constructor(collection,orderProperty="order",orderIncrement=10){this.collection=collection;this.orderProperty=orderProperty;this.orderIncrement=orderIncrement;if(typeof collection!=="object"||!collection[isProxy]){throw new Error("Collection is not proxied")}if(collection.valueOf()instanceof Array){throw new Error("Collection is an array, not an object collection")}if(!Object.keys(collection).every((key=>typeof collection[key]==="object"))){throw new Error("Collection has non-object children")}const ok=Object.keys(collection).every((key=>typeof collection[key][orderProperty]==="number"));if(!ok){const keys=Object.keys(collection);for(let i=0;i{const subscription=this.getObservable().subscribe((()=>{const newArray=this.getArray();subscriber.next(newArray)}));return function unsubscribe(){subscription.unsubscribe()}}))}getArray(){const arr=proxyAccess(this.collection).toArray(((a,b)=>a[this.orderProperty]-b[this.orderProperty]));return arr}add(item,index,from){const arr=this.getArray();let minOrder=Number.POSITIVE_INFINITY,maxOrder=Number.NEGATIVE_INFINITY;for(let i=0;ithis.collection[key]===item));if(!fromKey){throw new Error("item not found in collection")}if(from===index){return{key:fromKey,index:index}}if(Math.abs(from-index)===1){const otherItem=arr[index];const otherOrder=otherItem[this.orderProperty];otherItem[this.orderProperty]=item[this.orderProperty];item[this.orderProperty]=otherOrder;return{key:fromKey,index:index}}else{arr.splice(from,1)}}if(typeof index!=="number"||index>=arr.length){index=arr.length;item[this.orderProperty]=arr.length==0?0:maxOrder+this.orderIncrement}else if(index===0){item[this.orderProperty]=arr.length==0?0:minOrder-this.orderIncrement}else{const orders=arr.map((item=>item[this.orderProperty]));const gap=orders[index]-orders[index-1];if(gap>1){item[this.orderProperty]=orders[index]-Math.floor(gap/2)}else{arr.splice(index,0,item);for(let i=0;ithis.collection[key]===item));if(!key){throw new Error("Cannot find target object to delete")}this.collection[key]=null;return{key:key,index:index}}move(fromIndex,toIndex){const arr=this.getArray();return this.add(arr[fromIndex],toIndex,fromIndex)}sort(sortFn){const arr=this.getArray();arr.sort(sortFn);for(let i=0;i{newContext[key]=context[key]}))}this[_private].context=newContext;return this}else if(typeof context==="undefined"){console.warn("Use snap.context() instead of snap.ref.context() to get updating context in event callbacks");return currentContext}else{throw new Error("Invalid context argument")}}get cursor(){return this[_private].cursor}set cursor(value){var _a;this[_private].cursor=value;(_a=this.onCursor)===null||_a===void 0?void 0:_a.call(this,value)}get path(){return this[_private].path}get key(){const key=this[_private].key;return typeof key==="number"?`[${key}]`:key}get index(){const key=this[_private].key;if(typeof key!=="number"){throw new Error(`"${key}" is not a number`)}return key}get parent(){const currentPath=path_info_1.PathInfo.fillVariables2(this.path,this.vars);const info=path_info_1.PathInfo.get(currentPath);if(info.parentPath===null){return null}return new DataReference(this.db,info.parentPath).context(this[_private].context)}get vars(){return this[_private].vars}child(childPath){childPath=typeof childPath==="number"?childPath:childPath.replace(/^\/|\/$/g,"");const currentPath=path_info_1.PathInfo.fillVariables2(this.path,this.vars);const targetPath=path_info_1.PathInfo.getChildPath(currentPath,childPath);return new DataReference(this.db,targetPath).context(this[_private].context)}async set(value,onComplete){try{if(this.isWildcardPath){throw new Error(`Cannot set the value of wildcard path "/${this.path}"`)}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 undefined value in "/${this.path}"`)}if(!this.db.isReady){await this.db.ready()}value=this.db.types.serialize(this.path,value);const{cursor:cursor}=await this.db.api.set(this.path,value,{context:this[_private].context});this.cursor=cursor;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,this)}catch(err){console.error("Error in onComplete callback:",err)}}else{throw err}}return this}async update(updates,onComplete){try{if(this.isWildcardPath){throw new Error(`Cannot update the value of wildcard path "/${this.path}"`)}if(!this.db.isReady){await this.db.ready()}if(typeof updates!=="object"||updates instanceof Array||updates instanceof ArrayBuffer||updates instanceof Date){await this.set(updates)}else if(Object.keys(updates).length===0){console.warn(`update called on path "/${this.path}", but there is nothing to update`)}else{updates=this.db.types.serialize(this.path,updates);const{cursor:cursor}=await this.db.api.update(this.path,updates,{context:this[_private].context});this.cursor=cursor}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,this)}catch(err){console.error("Error in onComplete callback:",err)}}else{throw err}}return this}async transaction(callback){if(this.isWildcardPath){throw new Error(`Cannot start a transaction on wildcard path "/${this.path}"`)}if(!this.db.isReady){await this.db.ready()}let throwError;const cb=currentValue=>{currentValue=this.db.types.deserialize(this.path,currentValue);const snap=new data_snapshot_1.DataSnapshot(this,currentValue);let newValue;try{newValue=callback(snap)}catch(err){throwError=err;return}if(newValue instanceof Promise){return newValue.then((val=>this.db.types.serialize(this.path,val))).catch((err=>{throwError=err;return}))}else{return this.db.types.serialize(this.path,newValue)}};const{cursor:cursor}=await this.db.api.transaction(this.path,cb,{context:this[_private].context});this.cursor=cursor;if(throwError){throw throwError}return this}on(event,callback,cancelCallback){if(this.path===""&&["value","child_changed"].includes(event)){console.warn("WARNING: Listening for value and child_changed events on the root node is a bad practice. These events require loading of all data (value event), or potentially lots of data (child_changed event) each time they are fired")}let eventPublisher=null;const eventStream=new subscription_1.EventStream((publisher=>{eventPublisher=publisher}));const cb={event:event,stream:eventStream,userCallback:typeof callback==="function"&&callback,ourCallback:(err,path,newValue,oldValue,eventContext)=>{if(err){this.db.logger.error(`Error getting data for event ${event} on path "${path}"`,err);return}const ref=this.db.ref(path);ref[_private].vars=path_info_1.PathInfo.extractVariables(this.path,path);let callbackObject;if(event.startsWith("notify_")){callbackObject=ref.context(eventContext||{})}else{const values={previous:this.db.types.deserialize(path,oldValue),current:this.db.types.deserialize(path,newValue)};if(event==="child_removed"){callbackObject=new data_snapshot_1.DataSnapshot(ref,values.previous,true,values.previous,eventContext)}else if(event==="mutations"){callbackObject=new data_snapshot_1.MutationsDataSnapshot(ref,values.current,eventContext)}else{const isRemoved=event==="mutated"&&values.current===null;callbackObject=new data_snapshot_1.DataSnapshot(ref,values.current,isRemoved,values.previous,eventContext)}}eventPublisher.publish(callbackObject);if(eventContext===null||eventContext===void 0?void 0:eventContext.acebase_cursor){this.cursor=eventContext.acebase_cursor}}};this[_private].callbacks.push(cb);const subscribe=()=>{if(typeof callback==="function"){eventStream.subscribe(callback,((activated,cancelReason)=>{if(!activated){cancelCallback&&cancelCallback(cancelReason)}}))}const advancedOptions=typeof callback==="object"?callback:{newOnly:!callback};if(typeof advancedOptions.newOnly!=="boolean"){advancedOptions.newOnly=false}if(this.isWildcardPath){advancedOptions.newOnly=true}const cancelSubscription=err=>{const callbacks=this[_private].callbacks;callbacks.splice(callbacks.indexOf(cb),1);this.db.api.unsubscribe(this.path,event,cb.ourCallback);this.db.logger.error(`Subscription "${event}" on path "/${this.path}" canceled because of an error: ${err.message}`);eventPublisher.cancel(err.message)};const authorized=this.db.api.subscribe(this.path,event,cb.ourCallback,{newOnly:advancedOptions.newOnly,cancelCallback:cancelSubscription,syncFallback:advancedOptions.syncFallback});const allSubscriptionsStoppedCallback=()=>{const callbacks=this[_private].callbacks;callbacks.splice(callbacks.indexOf(cb),1);return this.db.api.unsubscribe(this.path,event,cb.ourCallback)};if(authorized instanceof Promise){authorized.then((()=>{eventPublisher.start(allSubscriptionsStoppedCallback)})).catch(cancelSubscription)}else{eventPublisher.start(allSubscriptionsStoppedCallback)}if(!advancedOptions.newOnly){if(event==="value"){this.get((snap=>{eventPublisher.publish(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=>{const childSnap=new data_snapshot_1.DataSnapshot(this.child(key),val[key]);eventPublisher.publish(childSnap)}))}))}else if(event==="notify_child_added"){const step=100,limit=step;let skip=0;const more=async()=>{const children=await this.db.api.reflect(this.path,"children",{limit:limit,skip:skip});children.list.forEach((child=>{const childRef=this.child(child.key);eventPublisher.publish(childRef)}));if(children.more){skip+=step;more()}};more()}}};if(this.db.isReady){subscribe()}else{this.db.ready(subscribe)}return eventStream}off(event,callback){const subscriptions=this[_private].callbacks;const stopSubs=subscriptions.filter((sub=>(!event||sub.event===event)&&(!callback||sub.userCallback===callback)));if(stopSubs.length===0){this.db.logger.warn(`Can't find event subscriptions to stop (path: "${this.path}", event: ${event||"(any)"}, callback: ${callback})`)}stopSubs.forEach((sub=>{sub.stream.stop()}));return this}get(optionsOrCallback,callback){if(!this.db.isReady){const promise=this.db.ready().then((()=>this.get(optionsOrCallback,callback)));return typeof optionsOrCallback!=="function"&&typeof callback!=="function"?promise:undefined}callback=typeof optionsOrCallback==="function"?optionsOrCallback:typeof callback==="function"?callback:undefined;if(this.isWildcardPath){const error=new Error(`Cannot get value of wildcard path "/${this.path}". Use .query() instead`);if(typeof callback==="function"){throw error}return Promise.reject(error)}const options=new DataRetrievalOptions(typeof optionsOrCallback==="object"?optionsOrCallback:{cache_mode:"allow"});const promise=this.db.api.get(this.path,options).then((result=>{var _a;const isNewApiResult="context"in result&&"value"in result;if(!isNewApiResult){console.warn("AceBase api.get method returned an old response value. Update your acebase or acebase-client package");result={value:result,context:{}}}const value=this.db.types.deserialize(this.path,result.value);const snapshot=new data_snapshot_1.DataSnapshot(this,value,undefined,undefined,result.context);if((_a=result.context)===null||_a===void 0?void 0:_a.acebase_cursor){this.cursor=result.context.acebase_cursor}return snapshot}));if(callback){promise.then(callback).catch((err=>{console.error("Uncaught error:",err)}));return}else{return promise}}once(event,options){if(event==="value"&&!this.isWildcardPath){return this.get(options)}return new Promise((resolve=>{const callback=snap=>{this.off(event,callback);resolve(snap)};this.on(event,callback)}))}push(value,onComplete){if(this.isWildcardPath){const error=new Error(`Cannot push to wildcard path "/${this.path}"`);if(typeof value==="undefined"||typeof onComplete==="function"){throw error}return Promise.reject(error)}const id=id_1.ID.generate();const ref=this.child(id);ref[_private].pushed=true;if(typeof value!=="undefined"){return ref.set(value,onComplete).then((()=>ref))}else{return ref}}async remove(){if(this.isWildcardPath){throw new Error(`Cannot remove wildcard path "/${this.path}". Use query().remove instead`)}if(this.parent===null){throw new Error("Cannot remove the root node")}return this.set(null)}async exists(){if(this.isWildcardPath){throw new Error(`Cannot check wildcard path "/${this.path}" existence`)}if(!this.db.isReady){await this.db.ready()}return this.db.api.exists(this.path)}get isWildcardPath(){return this.path.indexOf("*")>=0||this.path.indexOf("$")>=0}query(){return new DataReferenceQuery(this)}async count(){const info=await this.reflect("info",{child_count:true});return info.children.count}async reflect(type,args){if(this.isWildcardPath){throw new Error(`Cannot reflect on wildcard path "/${this.path}"`)}if(!this.db.isReady){await this.db.ready()}return this.db.api.reflect(this.path,type,args)}async export(write,options={format:"json",type_safe:true}){if(this.isWildcardPath){throw new Error(`Cannot export wildcard path "/${this.path}"`)}if(!this.db.isReady){await this.db.ready()}const writeFn=typeof write==="function"?write:write.write.bind(write);return this.db.api.export(this.path,writeFn,options)}async import(read,options={format:"json",suppress_events:false}){if(this.isWildcardPath){throw new Error(`Cannot import to wildcard path "/${this.path}"`)}if(!this.db.isReady){await this.db.ready()}return this.db.api.import(this.path,read,options)}proxy(options){const isOptionsArg=typeof options==="object"&&(typeof options.cursor!=="undefined"||typeof options.defaultValue!=="undefined");if(typeof options!=="undefined"&&!isOptionsArg){this.db.logger.warn("Warning: live data proxy is being initialized with a deprecated method signature. Use ref.proxy(options) instead of ref.proxy(defaultValue)");options={defaultValue:options}}return data_proxy_1.LiveDataProxy.create(this,options)}observe(options){if(options){throw new Error("observe does not support data retrieval options yet")}if(this.isWildcardPath){throw new Error(`Cannot observe wildcard path "/${this.path}"`)}const Observable=(0,optional_observable_1.getObservable)();return new Observable((observer=>{let cache,resolved=false;let promise=this.get(options).then((snap=>{resolved=true;cache=snap.val();observer.next(cache)}));const updateCache=snap=>{if(!resolved){promise=promise.then((()=>updateCache(snap)));return}const mutatedPath=snap.ref.path;if(mutatedPath===this.path){cache=snap.val();return observer.next(cache)}const trailKeys=path_info_1.PathInfo.getPathKeys(mutatedPath).slice(path_info_1.PathInfo.getPathKeys(this.path).length);let target=cache;while(trailKeys.length>1){const key=trailKeys.shift();if(!(key in target)){target[key]=typeof trailKeys[0]==="number"?[]:{}}target=target[key]}const prop=trailKeys.shift();const newValue=snap.val();if(newValue===null){target instanceof Array&&typeof prop==="number"?target.splice(prop,1):delete target[prop]}else{target[prop]=newValue}observer.next(cache)};this.on("mutated",updateCache);return()=>{this.off("mutated",updateCache)}}))}async forEach(callbackOrOptions,callback){let options;if(typeof callbackOrOptions==="function"){callback=callbackOrOptions}else{options=callbackOrOptions}if(typeof callback!=="function"){throw new TypeError("No callback function given")}const info=await this.reflect("children",{limit:0,skip:0});const summary={canceled:false,total:info.list.length,processed:0};for(let i=0;ithis.get(optionsOrCallback,callback)));return typeof optionsOrCallback!=="function"&&typeof callback!=="function"?promise:undefined}callback=typeof optionsOrCallback==="function"?optionsOrCallback:typeof callback==="function"?callback:undefined;const options=new QueryDataRetrievalOptions(typeof optionsOrCallback==="object"?optionsOrCallback:{snapshots:true,cache_mode:"allow"});options.allow_cache=options.cache_mode!=="bypass";options.eventHandler=ev=>{if(!this[_private].events[ev.name]){return false}const listeners=this[_private].events[ev.name];if(typeof listeners!=="object"||listeners.length===0){return false}if(["add","change","remove"].includes(ev.name)){const eventData={name:ev.name,ref:new DataReference(this.ref.db,ev.path)};if(options.snapshots&&ev.name!=="remove"){const val=db.types.deserialize(ev.path,ev.value);eventData.snapshot=new data_snapshot_1.DataSnapshot(eventData.ref,val,false)}ev=eventData}listeners.forEach((callback=>{var _a,_b;try{callback(ev)}catch(err){this.ref.db.logger.error(`Error executing "${ev.name}" event handler of realtime query on path "${this.ref.path}": ${(_b=(_a=err===null||err===void 0?void 0:err.stack)!==null&&_a!==void 0?_a:err===null||err===void 0?void 0:err.message)!==null&&_b!==void 0?_b:err}`)}}))};options.monitor={add:false,change:false,remove:false};if(this[_private].events){if(this[_private].events["add"]&&this[_private].events["add"].length>0){options.monitor.add=true}if(this[_private].events["change"]&&this[_private].events["change"].length>0){options.monitor.change=true}if(this[_private].events["remove"]&&this[_private].events["remove"].length>0){options.monitor.remove=true}}this.stop();const db=this.ref.db;return db.api.query(this.ref.path,this[_private],options).catch((err=>{throw new Error(err)})).then((res=>{const{stop:stop}=res;let{results:results,context:context}=res;this.stop=async()=>{await stop()};if(!("results"in res&&"context"in res)){console.warn("Query results missing context. Update your acebase and/or acebase-client packages");results=res,context={}}if(options.snapshots){const snaps=results.map((result=>{const val=db.types.deserialize(result.path,result.val);return new data_snapshot_1.DataSnapshot(db.ref(result.path),val,false,undefined,context)}));return DataSnapshotsArray.from(snaps)}else{const refs=results.map((path=>db.ref(path)));return DataReferencesArray.from(refs)}})).then((results=>{callback&&callback(results);return results}))}async stop(){}getRefs(callback){return this.get({snapshots:false},callback)}find(){return this.get({snapshots:false})}async count(){const refs=await this.find();return refs.length}async exists(){const originalTake=this[_private].take;const p=this.take(1).find();this.take(originalTake);const refs=await p;return refs.length!==0}async remove(callback){const refs=await this.find();const parentUpdates=refs.reduce(((parents,ref)=>{const parent=parents[ref.parent.path];if(!parent){parents[ref.parent.path]=[ref]}else{parent.push(ref)}return parents}),{});const db=this.ref.db;const promises=Object.keys(parentUpdates).map((async parentPath=>{const updates=refs.reduce(((updates,ref)=>{updates[ref.key]=null;return updates}),{});const ref=db.ref(parentPath);try{await ref.update(updates);return{ref:ref,success:true}}catch(error){return{ref:ref,success:false,error:error}}}));const results=await Promise.all(promises);callback&&callback(results);return results}on(event,callback){if(!this[_private].events[event]){this[_private].events[event]=[]}this[_private].events[event].push(callback);return this}off(event,callback){if(typeof event==="undefined"){this[_private].events={};return this}if(!this[_private].events[event]){return this}if(typeof callback==="undefined"){delete this[_private].events[event];return this}const index=this[_private].events[event].indexOf(callback);if(!~index){return this}this[_private].events[event].splice(index,1);return this}async forEach(callbackOrOptions,callback){let options;if(typeof callbackOrOptions==="function"){callback=callbackOrOptions}else{options=callbackOrOptions}if(typeof callback!=="function"){throw new TypeError("No callback function given")}const refs=await this.find();const summary={canceled:false,total:refs.length,processed:0};for(let i=0;iarr[i]=snap));return arr}getValues(){return this.map((snap=>snap.val()))}}exports.DataSnapshotsArray=DataSnapshotsArray;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))}}exports.DataReferencesArray=DataReferencesArray},{"./data-proxy":7,"./data-snapshot":9,"./id":11,"./optional-observable":14,"./path-info":16,"./subscription":24}],9:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.MutationsDataSnapshot=exports.DataSnapshot=void 0;const path_info_1=require("./path-info");function getChild(snapshot,path,previous=false){if(!snapshot.exists()){return null}let child=previous?snapshot.previous():snapshot.val();if(typeof path==="number"){return child[path]}path_info_1.PathInfo.getPathKeys(path).every((key=>{child=child[key];return typeof child!=="undefined"}));return child||null}function getChildren(snapshot){if(!snapshot.exists()){return[]}const 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{exists(){return false}constructor(ref,value,isRemoved=false,prevValue,context){this.ref=ref;this.val=()=>value;this.previous=()=>prevValue;this.exists=()=>{if(isRemoved){return false}return value!==null&&typeof value!=="undefined"};this.context=()=>context||{}}static for(ref,value){return new DataSnapshot(ref,value)}child(path){const val=getChild(this,path,false);const prev=getChild(this,path,true);return new DataSnapshot(this.ref.child(path),val,false,prev)}hasChild(path){return getChild(this,path)!==null}hasChildren(){return getChildren(this).length>0}numChildren(){return getChildren(this).length}forEach(callback){const value=this.val();const prev=this.previous();return getChildren(this).every((key=>{const snap=new DataSnapshot(this.ref.child(key),value[key],false,prev[key]);return callback(snap)}))}get key(){return this.ref.key}}exports.DataSnapshot=DataSnapshot;class MutationsDataSnapshot extends DataSnapshot{constructor(ref,mutations,context){super(ref,mutations,false,undefined,context);this.previous=()=>{throw new Error("Iterate values to get previous values for each mutation")};this.val=(warn=true)=>{if(warn){console.warn("Unless you know what you are doing, it is best not to use the value of a mutations snapshot directly. Use child methods and forEach to iterate the mutations instead")}return mutations}}forEach(callback){const mutations=this.val(false);return mutations.every((mutation=>{const ref=mutation.target.reduce(((ref,key)=>ref.child(key)),this.ref);const snap=new DataSnapshot(ref,mutation.val,false,mutation.prev);return callback(snap)}))}child(index){if(typeof index!=="number"){throw new Error("child index must be a number")}const mutation=this.val(false)[index];const ref=mutation.target.reduce(((ref,key)=>ref.child(key)),this.ref);return new DataSnapshot(ref,mutation.val,false,mutation.prev)}}exports.MutationsDataSnapshot=MutationsDataSnapshot},{"./path-info":16}],10:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.DebugLogger=void 0;const process_1=require("./process");const noop=()=>{};class DebugLogger{constructor(level="log",prefix=""){this.level=level;this.prefix=prefix;this.setLevel(level)}setLevel(level){const prefix=this.prefix?this.prefix+" %s":"";this.verbose=["verbose"].includes(level)?prefix?console.log.bind(console,prefix):console.log.bind(console):noop;this.trace=["verbose"].includes(level)?prefix?console.log.bind(console,prefix):console.log.bind(console):noop;this.debug=["verbose"].includes(level)?prefix?console.log.bind(console,prefix):console.log.bind(console):noop;this.log=["verbose","log"].includes(level)?prefix?console.log.bind(console,prefix):console.log.bind(console):noop;this.info=["verbose","log"].includes(level)?prefix?console.log.bind(console,prefix):console.log.bind(console):noop;this.warn=["verbose","log","warn"].includes(level)?prefix?console.warn.bind(console,prefix):console.warn.bind(console):noop;this.error=["verbose","log","warn","error"].includes(level)?prefix?console.error.bind(console,prefix):console.error.bind(console):noop;this.fatal=["verbose","log","warn","error"].includes(level)?prefix?console.error.bind(console,prefix):console.error.bind(console):noop;this.write=text=>{const isRunKit=typeof process_1.default!=="undefined"&&process_1.default.env&&typeof process_1.default.env.RUNKIT_ENDPOINT_PATH==="string";if(text&&isRunKit){text.split("\n").forEach((line=>console.log(line)))}else{console.log(text)}}}}exports.DebugLogger=DebugLogger},{"./process":18}],11:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.ID=void 0;const cuid_1=require("./cuid");let timeBias=0;class ID{static set timeBias(bias){if(typeof bias!=="number"){return}timeBias=bias}static generate(){return(0,cuid_1.default)(timeBias).slice(1)}}exports.ID=ID},{"./cuid":5}],12:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.ObjectCollection=exports.PartialArray=exports.SimpleObservable=exports.SchemaDefinition=exports.Colorize=exports.ColorStyle=exports.SimpleEventEmitter=exports.SimpleCache=exports.ascii85=exports.PathInfo=exports.Utils=exports.TypeMappings=exports.Transport=exports.EventSubscription=exports.EventPublisher=exports.EventStream=exports.PathReference=exports.ID=exports.DebugLogger=exports.OrderedCollectionProxy=exports.proxyAccess=exports.MutationsDataSnapshot=exports.DataSnapshot=exports.DataReferencesArray=exports.DataSnapshotsArray=exports.QueryDataRetrievalOptions=exports.DataRetrievalOptions=exports.DataReferenceQuery=exports.DataReference=exports.Api=exports.AceBaseBaseSettings=exports.AceBaseBase=void 0;var acebase_base_1=require("./acebase-base");Object.defineProperty(exports,"AceBaseBase",{enumerable:true,get:function(){return acebase_base_1.AceBaseBase}});Object.defineProperty(exports,"AceBaseBaseSettings",{enumerable:true,get:function(){return acebase_base_1.AceBaseBaseSettings}});var api_1=require("./api");Object.defineProperty(exports,"Api",{enumerable:true,get:function(){return api_1.Api}});var data_reference_1=require("./data-reference");Object.defineProperty(exports,"DataReference",{enumerable:true,get:function(){return data_reference_1.DataReference}});Object.defineProperty(exports,"DataReferenceQuery",{enumerable:true,get:function(){return data_reference_1.DataReferenceQuery}});Object.defineProperty(exports,"DataRetrievalOptions",{enumerable:true,get:function(){return data_reference_1.DataRetrievalOptions}});Object.defineProperty(exports,"QueryDataRetrievalOptions",{enumerable:true,get:function(){return data_reference_1.QueryDataRetrievalOptions}});Object.defineProperty(exports,"DataSnapshotsArray",{enumerable:true,get:function(){return data_reference_1.DataSnapshotsArray}});Object.defineProperty(exports,"DataReferencesArray",{enumerable:true,get:function(){return data_reference_1.DataReferencesArray}});var data_snapshot_1=require("./data-snapshot");Object.defineProperty(exports,"DataSnapshot",{enumerable:true,get:function(){return data_snapshot_1.DataSnapshot}});Object.defineProperty(exports,"MutationsDataSnapshot",{enumerable:true,get:function(){return data_snapshot_1.MutationsDataSnapshot}});var data_proxy_1=require("./data-proxy");Object.defineProperty(exports,"proxyAccess",{enumerable:true,get:function(){return data_proxy_1.proxyAccess}});Object.defineProperty(exports,"OrderedCollectionProxy",{enumerable:true,get:function(){return data_proxy_1.OrderedCollectionProxy}});var debug_1=require("./debug");Object.defineProperty(exports,"DebugLogger",{enumerable:true,get:function(){return debug_1.DebugLogger}});var id_1=require("./id");Object.defineProperty(exports,"ID",{enumerable:true,get:function(){return id_1.ID}});var path_reference_1=require("./path-reference");Object.defineProperty(exports,"PathReference",{enumerable:true,get:function(){return path_reference_1.PathReference}});var subscription_1=require("./subscription");Object.defineProperty(exports,"EventStream",{enumerable:true,get:function(){return subscription_1.EventStream}});Object.defineProperty(exports,"EventPublisher",{enumerable:true,get:function(){return subscription_1.EventPublisher}});Object.defineProperty(exports,"EventSubscription",{enumerable:true,get:function(){return subscription_1.EventSubscription}});exports.Transport=require("./transport");var type_mappings_1=require("./type-mappings");Object.defineProperty(exports,"TypeMappings",{enumerable:true,get:function(){return type_mappings_1.TypeMappings}});exports.Utils=require("./utils");var path_info_1=require("./path-info");Object.defineProperty(exports,"PathInfo",{enumerable:true,get:function(){return path_info_1.PathInfo}});var ascii85_1=require("./ascii85");Object.defineProperty(exports,"ascii85",{enumerable:true,get:function(){return ascii85_1.ascii85}});var simple_cache_1=require("./simple-cache");Object.defineProperty(exports,"SimpleCache",{enumerable:true,get:function(){return simple_cache_1.SimpleCache}});var simple_event_emitter_1=require("./simple-event-emitter");Object.defineProperty(exports,"SimpleEventEmitter",{enumerable:true,get:function(){return simple_event_emitter_1.SimpleEventEmitter}});var simple_colors_1=require("./simple-colors");Object.defineProperty(exports,"ColorStyle",{enumerable:true,get:function(){return simple_colors_1.ColorStyle}});Object.defineProperty(exports,"Colorize",{enumerable:true,get:function(){return simple_colors_1.Colorize}});var schema_1=require("./schema");Object.defineProperty(exports,"SchemaDefinition",{enumerable:true,get:function(){return schema_1.SchemaDefinition}});var simple_observable_1=require("./simple-observable");Object.defineProperty(exports,"SimpleObservable",{enumerable:true,get:function(){return simple_observable_1.SimpleObservable}});var partial_array_1=require("./partial-array");Object.defineProperty(exports,"PartialArray",{enumerable:true,get:function(){return partial_array_1.PartialArray}});const object_collection_1=require("./object-collection");Object.defineProperty(exports,"ObjectCollection",{enumerable:true,get:function(){return object_collection_1.ObjectCollection}})},{"./acebase-base":1,"./api":2,"./ascii85":3,"./data-proxy":7,"./data-reference":8,"./data-snapshot":9,"./debug":10,"./id":11,"./object-collection":13,"./partial-array":15,"./path-info":16,"./path-reference":17,"./schema":19,"./simple-cache":20,"./simple-colors":21,"./simple-event-emitter":22,"./simple-observable":23,"./subscription":24,"./transport":25,"./type-mappings":26,"./utils":27}],13:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.ObjectCollection=void 0;const id_1=require("./id");class ObjectCollection{static from(array){const collection={};array.forEach((child=>{collection[id_1.ID.generate()]=child}));return collection}}exports.ObjectCollection=ObjectCollection},{"./id":11}],14:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.setObservable=exports.getObservable=void 0;const simple_observable_1=require("./simple-observable");const utils_1=require("./utils");let _shimRequested=false;let _observable;(async()=>{const global=(0,utils_1.getGlobalObject)();if(typeof global.Observable!=="undefined"){_observable=global.Observable;return}try{const{Observable:Observable}=await Promise.resolve().then((()=>require("rxjs")));_observable=Observable}catch(_a){_observable=simple_observable_1.SimpleObservable}})();function getObservable(){if(_observable===simple_observable_1.SimpleObservable&&!_shimRequested){console.warn("Using AceBase's simple Observable implementation because rxjs is not available. "+'Add it to your project with "npm install rxjs", add it to AceBase using db.setObservable(Observable), '+'or call db.setObservable("shim") to suppress this warning')}if(_observable){return _observable}throw new Error("RxJS Observable could not be loaded. ")}exports.getObservable=getObservable;function setObservable(Observable){if(Observable==="shim"){_observable=simple_observable_1.SimpleObservable;_shimRequested=true}else{_observable=Observable}}exports.setObservable=setObservable},{"./simple-observable":23,"./utils":27,rxjs:62}],15:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.PartialArray=void 0;class PartialArray{constructor(sparseArray){if(sparseArray instanceof Array){for(let i=0;ikey.startsWith("[")?parseInt(key.slice(1,-1)):key))}class PathInfo{static get(path){return new PathInfo(path)}static getChildPath(path,childKey){return PathInfo.get(path).child(childKey).path}static getPathKeys(path){return getPathKeys(path)}constructor(path){if(typeof path==="string"){this.keys=getPathKeys(path)}else if(path instanceof Array){this.keys=path}this.path=this.keys.reduce(((path,key,i)=>i===0?`${key}`:typeof key==="string"?`${path}/${key}`:`${path}[${key}]`),"")}get key(){return this.keys.length===0?null:this.keys.slice(-1)[0]}get parent(){if(this.keys.length==0){return null}const parentKeys=this.keys.slice(0,-1);return new PathInfo(parentKeys)}get parentPath(){return this.keys.length===0?null:this.parent.path}child(childKey){if(typeof childKey==="string"){if(childKey.length===0){throw new Error(`child key for path "${this.path}" cannot be empty`)}const keys=getPathKeys(childKey);keys.forEach((key=>{if(typeof key!=="string"){return}if(/[\x00-\x08\x0b\x0c\x0e-\x1f/[\]\\]/.test(key)){throw new Error(`Invalid child key "${key}" for path "${this.path}". Keys cannot contain control characters or any of the following characters: \\ / [ ]`)}if(key.length>128){throw new Error(`child key "${key}" for path "${this.path}" is too long. Max key length is 128`)}if(key.length===0){throw new Error(`child key for path "${this.path}" cannot be empty`)}}));childKey=keys}return new PathInfo(this.keys.concat(childKey))}childPath(childKey){return this.child(childKey).path}get pathKeys(){return this.keys}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);const 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 "${varPath}" 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}const pathKeys=getPathKeys(varPath);let n=0;const targetPath=pathKeys.reduce(((path,key)=>{if(typeof key==="string"&&(key==="*"||key.startsWith("$"))){return PathInfo.getChildPath(path,vars[n++])}else{return PathInfo.getChildPath(path,key)}}),"");return targetPath}equals(otherPath){const other=otherPath instanceof PathInfo?otherPath:new PathInfo(otherPath);if(this.path===other.path){return true}if(this.keys.length!==other.keys.length){return false}return this.keys.every(((key,index)=>{const otherKey=other.keys[index];return otherKey===key||typeof otherKey==="string"&&(otherKey==="*"||otherKey[0]==="$")||typeof key==="string"&&(key==="*"||key[0]==="$")}))}isAncestorOf(descendantPath){const descendant=descendantPath instanceof PathInfo?descendantPath:new PathInfo(descendantPath);if(descendant.path===""||this.path===descendant.path){return false}if(this.path===""){return true}if(this.keys.length>=descendant.keys.length){return false}return this.keys.every(((key,index)=>{const otherKey=descendant.keys[index];return otherKey===key||typeof otherKey==="string"&&(otherKey==="*"||otherKey[0]==="$")||typeof key==="string"&&(key==="*"||key[0]==="$")}))}isDescendantOf(ancestorPath){const ancestor=ancestorPath instanceof PathInfo?ancestorPath:new PathInfo(ancestorPath);if(this.path===""||this.path===ancestor.path){return false}if(ancestorPath===""){return true}if(ancestor.keys.length>=this.keys.length){return false}return ancestor.keys.every(((key,index)=>{const otherKey=this.keys[index];return otherKey===key||typeof otherKey==="string"&&(otherKey==="*"||otherKey[0]==="$")||typeof key==="string"&&(key==="*"||key[0]==="$")}))}isOnTrailOf(otherPath){const other=otherPath instanceof PathInfo?otherPath:new PathInfo(otherPath);if(this.path.length===0||other.path.length===0){return true}if(this.path===other.path){return true}return this.pathKeys.every(((key,index)=>{if(index>=other.keys.length){return true}const otherKey=other.keys[index];return otherKey===key||typeof otherKey==="string"&&(otherKey==="*"||otherKey[0]==="$")||typeof key==="string"&&(key==="*"||key[0]==="$")}))}isChildOf(otherPath){const other=otherPath instanceof PathInfo?otherPath:new PathInfo(otherPath);if(this.path===""){return false}return this.parent.equals(other)}isParentOf(otherPath){const other=otherPath instanceof PathInfo?otherPath:new PathInfo(otherPath);if(other.path===""){return false}return this.equals(other.parent)}}exports.PathInfo=PathInfo},{}],17:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.PathReference=void 0;class PathReference{constructor(path){this.path=path}}exports.PathReference=PathReference},{}],18:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.default={nextTick(fn){setTimeout(fn,0)}}},{}],19:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.SchemaDefinition=void 0;function parse(definition){let pos=0;function consumeSpaces(){let c;while(c=definition[pos],[" ","\r","\n","\t"].includes(c)){pos++}}function consumeCharacter(c){if(definition[pos]!==c){throw new Error(`Unexpected character at position ${pos}. Expected: '${c}', found '${definition[pos]}'`)}pos++}function readProperty(){consumeSpaces();const prop={name:"",optional:false,wildcard:false};let c;while(c=definition[pos],c==="_"||c==="$"||c>="a"&&c<="z"||c>="A"&&c<="Z"||prop.name.length>0&&c>="0"&&c<="9"||prop.name.length===0&&c==="*"){prop.name+=c;pos++}if(prop.name.length===0){throw new Error(`Property name expected at position ${pos}, found: ${definition.slice(pos,pos+10)}..`)}if(definition[pos]==="?"){prop.optional=true;pos++}if(prop.name==="*"||prop.name[0]==="$"){prop.optional=true;prop.wildcard=true}consumeSpaces();consumeCharacter(":");return prop}function readType(){consumeSpaces();let type={typeOf:"any"},c;let name="";while(c=definition[pos],c>="a"&&c<="z"||c>="A"&&c<="Z"){name+=c;pos++}if(name.length===0){if(definition[pos]==="*"){consumeCharacter("*");type.typeOf="any"}else if(["'",'"',"`"].includes(definition[pos])){type.typeOf="string";type.value="";const quote=definition[pos];consumeCharacter(quote);while(c=definition[pos],c&&c!==quote){type.value+=c;pos++}consumeCharacter(quote)}else if(definition[pos]>="0"&&definition[pos]<="9"){type.typeOf="number";let nr="";while(c=definition[pos],c==="."||c==="n"||c>="0"&&c<="9"){nr+=c;pos++}if(nr.endsWith("n")){type.value=BigInt(nr)}else if(nr.includes(".")){type.value=parseFloat(nr)}else{type.value=parseInt(nr)}}else if(definition[pos]==="{"){consumeCharacter("{");type.typeOf="object";type.instanceOf=Object;type.children=[];while(true){const prop=readProperty();const types=readTypes();type.children.push({name:prop.name,optional:prop.optional,wildcard:prop.wildcard,types:types});consumeSpaces();if(definition[pos]===";"||definition[pos]===","){consumeCharacter(definition[pos]);consumeSpaces()}if(definition[pos]==="}"){break}}consumeCharacter("}")}else if(definition[pos]==="/"){consumeCharacter("/");let pattern="",flags="";while(c=definition[pos],c!=="/"||pattern.endsWith("\\")){pattern+=c;pos++}consumeCharacter("/");while(c=definition[pos],["g","i","m","s","u","y","d"].includes(c)){flags+=c;pos++}type.typeOf="string";type.matches=new RegExp(pattern,flags)}else{throw new Error(`Expected a type definition at position ${pos}, found character '${definition[pos]}'`)}}else if(["string","number","boolean","bigint","undefined","String","Number","Boolean","BigInt"].includes(name)){type.typeOf=name.toLowerCase()}else if(name==="Object"||name==="object"){type.typeOf="object";type.instanceOf=Object}else if(name==="Date"){type.typeOf="object";type.instanceOf=Date}else if(name==="Binary"||name==="binary"){type.typeOf="object";type.instanceOf=ArrayBuffer}else if(name==="any"){type.typeOf="any"}else if(name==="null"){type.typeOf="object";type.value=null}else if(name==="Array"){consumeCharacter("<");type.typeOf="object";type.instanceOf=Array;type.genericTypes=readTypes();consumeCharacter(">")}else if(["true","false"].includes(name)){type.typeOf="boolean";type.value=name==="true"}else{throw new Error(`Unknown type at position ${pos}: "${type}"`)}consumeSpaces();while(definition[pos]==="["){consumeCharacter("[");consumeCharacter("]");type={typeOf:"object",instanceOf:Array,genericTypes:[type]}}return type}function readTypes(){consumeSpaces();const types=[readType()];while(definition[pos]==="|"){consumeCharacter("|");types.push(readType());consumeSpaces()}return types}return readType()}function checkObject(path,properties,obj,partial){const invalidProperties=properties.find((prop=>prop.name==="*"||prop.name[0]==="$"))?[]:Object.keys(obj).filter((key=>![null,undefined].includes(obj[key])&&!properties.find((prop=>prop.name===key))));if(invalidProperties.length>0){return{ok:false,reason:`Object at path "${path}" cannot have propert${invalidProperties.length===1?"y":"ies"} ${invalidProperties.map((p=>`"${p}"`)).join(", ")}`}}function checkProperty(property){const hasValue=![null,undefined].includes(obj[property.name]);if(!property.optional&&(partial?obj[property.name]===null:!hasValue)){return{ok:false,reason:`Property at path "${path}/${property.name}" is not optional`}}if(hasValue&&property.types.length===1){return checkType(`${path}/${property.name}`,property.types[0],obj[property.name],false)}if(hasValue&&!property.types.some((type=>checkType(`${path}/${property.name}`,type,obj[property.name],false).ok))){return{ok:false,reason:`Property at path "${path}/${property.name}" does not match any of ${property.types.length} allowed types`}}return{ok:true}}const namedProperties=properties.filter((prop=>!prop.wildcard));const failedProperty=namedProperties.find((prop=>!checkProperty(prop).ok));if(failedProperty){const reason=checkProperty(failedProperty).reason;return{ok:false,reason:reason}}const wildcardProperty=properties.find((prop=>prop.wildcard));if(!wildcardProperty){return{ok:true}}const wildcardChildKeys=Object.keys(obj).filter((key=>!namedProperties.find((prop=>prop.name===key))));let result={ok:true};for(let i=0;i0){if(type.typeOf!=="object"){return{ok:false,reason:`path "${path}" must be typeof ${type.typeOf}`}}if(!type.children){return ok}const childKey=trailKeys[0];let property=type.children.find((prop=>prop.name===childKey));if(!property){property=type.children.find((prop=>prop.name==="*"||prop.name[0]==="$"))}if(!property){return{ok:false,reason:`Object at path "${path}" cannot have property "${childKey}"`}}if(property.optional&&value===null&&trailKeys.length===1){return ok}let result;property.types.some((type=>{const childPath=typeof childKey==="number"?`${path}[${childKey}]`:`${path}/${childKey}`;result=checkType(childPath,type,value,partial,trailKeys.slice(1));return result.ok}));return result}if(value===null){return ok}if(type.instanceOf===Object&&(typeof value!=="object"||value instanceof Array||value instanceof Date)){return{ok:false,reason:`path "${path}" must be an object collection`}}if(type.instanceOf&&(typeof value!=="object"||value.constructor!==type.instanceOf)){return{ok:false,reason:`path "${path}" must be an instance of ${type.instanceOf.name}`}}if("value"in type&&value!==type.value){return{ok:false,reason:`path "${path}" must be value: ${type.value}`}}if(typeof value!==type.typeOf){return{ok:false,reason:`path "${path}" must be typeof ${type.typeOf}`}}if(type.instanceOf===Array&&type.genericTypes&&!value.every((v=>type.genericTypes.some((t=>checkType(path,t,v,false).ok))))){return{ok:false,reason:`every array value of path "${path}" must match one of the specified types`}}if(type.typeOf==="object"&&type.children){return checkObject(path,type.children,value,partial)}if(type.matches&&!type.matches.test(value)){return{ok:false,reason:`path "${path}" must match regular expression /${type.matches.source}/${type.matches.flags}`}}return ok}function getConstructorType(val){switch(val){case String:return"string";case Number:return"number";case Boolean:return"boolean";case Date:return"Date";case BigInt:return"bigint";case Array:throw new Error("Schema error: Array cannot be used without a type. Use string[] or Array instead");default:throw new Error(`Schema error: unknown type used: ${val.name}`)}}class SchemaDefinition{constructor(definition,handling={warnOnly:false}){this.handling=handling;this.source=definition;if(typeof definition==="object"){const toTS=obj=>"{"+Object.keys(obj).map((key=>{let val=obj[key];if(val===undefined){val="undefined"}else if(val instanceof RegExp){val=`/${val.source}/${val.flags}`}else if(typeof val==="object"){val=toTS(val)}else if(typeof val==="function"){val=getConstructorType(val)}else if(!["string","number","boolean","bigint"].includes(typeof val)){throw new Error(`Type definition for key "${key}" must be a string, number, boolean, bigint, object, regular expression, or one of these classes: String, Number, Boolean, Date, BigInt`)}return`${key}:${val}`})).join(",")+"}";this.text=toTS(definition)}else if(typeof definition==="string"){this.text=definition}else{throw new Error("Type definiton must be a string or an object")}this.type=parse(this.text)}check(path,value,partial,trailKeys){const result=checkType(path,this.type,value,partial,trailKeys);if(!result.ok&&this.handling.warnOnly){result.warning=`${partial?"Partial schema":"Schema"} check on path "${path}"${trailKeys?` for child "${trailKeys.join("/")}"`:""} failed: ${result.reason}`;result.ok=true;this.handling.warnCallback(result.warning)}return result}}exports.SchemaDefinition=SchemaDefinition},{}],20:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.SimpleCache=void 0;const utils_1=require("./utils");const calculateExpiryTime=expirySeconds=>expirySeconds>0?Date.now()+expirySeconds*1e3:Infinity;class SimpleCache{get size(){return this.cache.size}constructor(options){var _a;this.enabled=true;if(typeof options==="number"){options={expirySeconds:options}}options.cloneValues=options.cloneValues!==false;if(typeof options.expirySeconds!=="number"&&typeof options.maxEntries!=="number"){throw new Error("Either expirySeconds or maxEntries must be specified")}this.options=options;this.cache=new Map;const interval=setInterval((()=>{this.cleanUp()}),60*1e3);(_a=interval.unref)===null||_a===void 0?void 0:_a.call(interval)}has(key){if(!this.enabled){return false}return this.cache.has(key)}get(key){if(!this.enabled){return null}const entry=this.cache.get(key);if(!entry){return null}entry.expires=calculateExpiryTime(this.options.expirySeconds);entry.accessed=Date.now();return this.options.cloneValues?(0,utils_1.cloneObject)(entry.value):entry.value}set(key,value){if(this.options.maxEntries>0&&this.cache.size>=this.options.maxEntries&&!this.cache.has(key)){let oldest=null;const now=Date.now();for(const[key,entry]of this.cache.entries()){if(entry.expires<=now){this.cache.delete(key);oldest=null;break}if(!oldest||entry.accessed{if(entry.expires<=now){this.cache.delete(key)}}))}}exports.SimpleCache=SimpleCache},{"./utils":27}],21:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.Colorize=exports.SetColorsEnabled=exports.ColorsSupported=exports.ColorStyle=void 0;const process_1=require("./process");const FontCode={bold:1,dim:2,italic:3,underline:4,inverse:7,hidden:8,strikethrough:94};const ColorCode={black:30,red:31,green:32,yellow:33,blue:34,magenta:35,cyan:36,white:37,grey:90,brightRed:91};const BgColorCode={bgBlack:40,bgRed:41,bgGreen:42,bgYellow:43,bgBlue:44,bgMagenta:45,bgCyan:46,bgWhite:47,bgGrey:100,bgBrightRed:101};const ResetCode={all:0,color:39,background:49,bold:22,dim:22,italic:23,underline:24,inverse:27,hidden:28,strikethrough:29};var ColorStyle;(function(ColorStyle){ColorStyle["reset"]="reset";ColorStyle["bold"]="bold";ColorStyle["dim"]="dim";ColorStyle["italic"]="italic";ColorStyle["underline"]="underline";ColorStyle["inverse"]="inverse";ColorStyle["hidden"]="hidden";ColorStyle["strikethrough"]="strikethrough";ColorStyle["black"]="black";ColorStyle["red"]="red";ColorStyle["green"]="green";ColorStyle["yellow"]="yellow";ColorStyle["blue"]="blue";ColorStyle["magenta"]="magenta";ColorStyle["cyan"]="cyan";ColorStyle["grey"]="grey";ColorStyle["bgBlack"]="bgBlack";ColorStyle["bgRed"]="bgRed";ColorStyle["bgGreen"]="bgGreen";ColorStyle["bgYellow"]="bgYellow";ColorStyle["bgBlue"]="bgBlue";ColorStyle["bgMagenta"]="bgMagenta";ColorStyle["bgCyan"]="bgCyan";ColorStyle["bgWhite"]="bgWhite";ColorStyle["bgGrey"]="bgGrey"})(ColorStyle=exports.ColorStyle||(exports.ColorStyle={}));function ColorsSupported(){if(typeof process_1.default==="undefined"||!process_1.default.stdout||!process_1.default.env||!process_1.default.platform||process_1.default.platform==="browser"){return false}if(process_1.default.platform==="win32"){return true}const env=process_1.default.env;if(env.COLORTERM){return true}if(env.TERM==="dumb"){return false}if(env.CI||env.TEAMCITY_VERSION){return!!env.TRAVIS}if(["iTerm.app","HyperTerm","Hyper","MacTerm","Apple_Terminal","vscode"].includes(env.TERM_PROGRAM)){return true}if(/^xterm-256|^screen|^xterm|^vt100|color|ansi|cygwin|linux/i.test(env.TERM)){return true}return false}exports.ColorsSupported=ColorsSupported;let _enabled=ColorsSupported();function SetColorsEnabled(enabled){_enabled=ColorsSupported()&&enabled}exports.SetColorsEnabled=SetColorsEnabled;function Colorize(str,style){if(!_enabled){return str}const openCodes=[],closeCodes=[];const addStyle=style=>{if(style===ColorStyle.reset){openCodes.push(ResetCode.all)}else if(style in FontCode){openCodes.push(FontCode[style]);closeCodes.push(ResetCode[style])}else if(style in ColorCode){openCodes.push(ColorCode[style]);closeCodes.push(ResetCode.color)}else if(style in BgColorCode){openCodes.push(BgColorCode[style]);closeCodes.push(ResetCode.background)}};if(style instanceof Array){style.forEach(addStyle)}else{addStyle(style)}const open=openCodes.map((code=>"["+code+"m")).join("");const close=closeCodes.map((code=>"["+code+"m")).join("");return str.split("\n").map((line=>open+line+close)).join("\n")}exports.Colorize=Colorize;String.prototype.colorize=function(style){return Colorize(this,style)}},{"./process":18}],22:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.SimpleEventEmitter=void 0;function runCallback(callback,data){try{callback(data)}catch(err){console.error("Error in subscription callback",err)}}const _subscriptions=Symbol("subscriptions");const _oneTimeEvents=Symbol("oneTimeEvents");class SimpleEventEmitter{constructor(){this[_subscriptions]=[];this[_oneTimeEvents]=new Map}on(event,callback){if(this[_oneTimeEvents].has(event)){return runCallback(callback,this[_oneTimeEvents].get(event))}this[_subscriptions].push({event:event,callback:callback,once:false});return this}off(event,callback){this[_subscriptions]=this[_subscriptions].filter((s=>s.event!==event||callback&&s.callback!==callback));return this}once(event,callback){return new Promise((resolve=>{const ourCallback=data=>{resolve(data);callback===null||callback===void 0?void 0:callback(data)};if(this[_oneTimeEvents].has(event)){runCallback(ourCallback,this[_oneTimeEvents].get(event))}else{this[_subscriptions].push({event:event,callback:ourCallback,once:true})}}))}emit(event,data){if(this[_oneTimeEvents].has(event)){throw new Error(`Event "${event}" was supposed to be emitted only once`)}for(let i=0;i{eventEmitter.emit(event,data)}))}pipeOnce(event,eventEmitter){this.once(event,(data=>{eventEmitter.emitOnce(event,data)}))}}exports.SimpleEventEmitter=SimpleEventEmitter},{}],23:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.SimpleObservable=void 0;class SimpleObservable{constructor(create){this._active=false;this._subscribers=[];this._create=create}subscribe(subscriber){if(!this._active){const next=value=>{this._subscribers.forEach((s=>{try{s(value)}catch(err){console.error("Error in subscriber callback:",err)}}))};const observer={next:next};this._cleanup=this._create(observer);this._active=true}this._subscribers.push(subscriber);const unsubscribe=()=>{this._subscribers.splice(this._subscribers.indexOf(subscriber),1);if(this._subscribers.length===0){this._active=false;this._cleanup()}};const subscription={unsubscribe:unsubscribe};return subscription}}exports.SimpleObservable=SimpleObservable},{}],24:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.EventStream=exports.EventPublisher=exports.EventSubscription=void 0;class EventSubscription{constructor(stop){this.stop=stop;this._internal={state:"init",activatePromises:[]}}activated(callback){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))}const noop=()=>{};this._internal.activatePromises.push({resolve:resolve,reject:callback?noop: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)}}}}exports.EventSubscription=EventSubscription;class EventPublisher{constructor(publish,start,cancel){this.publish=publish;this.start=start;this.cancel=cancel}}exports.EventPublisher=EventPublisher;class EventStream{constructor(eventPublisherCallback){const subscribers=[];let noMoreSubscribersCallback;let activationState;const STATE_STOPPED="stopped (no more subscribers)";this.subscribe=(callback,activationCallback)=>{if(typeof callback!=="function"){throw new TypeError("callback must be a function")}else if(activationState===STATE_STOPPED){throw new Error("stream can't be used anymore because all subscribers were stopped")}const sub={callback:callback,activationCallback:function(activated,cancelReason){activationCallback===null||activationCallback===void 0?void 0:activationCallback(activated,cancelReason);this.subscription._setActivationState(activated,cancelReason)},subscription:new EventSubscription((function stop(){subscribers.splice(subscribers.indexOf(this),1);return checkActiveSubscribers()}))};subscribers.push(sub);if(typeof activationState!=="undefined"){if(activationState===true){activationCallback===null||activationCallback===void 0?void 0:activationCallback(true);sub.subscription._setActivationState(true)}else if(typeof activationState==="string"){activationCallback===null||activationCallback===void 0?void 0:activationCallback(false,activationState);sub.subscription._setActivationState(false,activationState)}}return sub.subscription};const checkActiveSubscribers=()=>{let ret;if(subscribers.length===0){ret=noMoreSubscribersCallback===null||noMoreSubscribersCallback===void 0?void 0:noMoreSubscribersCallback();activationState=STATE_STOPPED}return Promise.resolve(ret)};this.unsubscribe=callback=>{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){console.error(`Error running subscriber callback: ${err.message}`)}}));if(subscribers.length===0){checkActiveSubscribers()}return subscribers.length>0};const start=allSubscriptionsStoppedCallback=>{activationState=true;noMoreSubscribersCallback=allSubscriptionsStoppedCallback;subscribers.forEach((sub=>{var _a;(_a=sub.activationCallback)===null||_a===void 0?void 0:_a.call(sub,true)}))};const cancel=reason=>{activationState=reason;subscribers.forEach((sub=>{var _a;(_a=sub.activationCallback)===null||_a===void 0?void 0:_a.call(sub,false,reason||new Error("unknown reason"))}));subscribers.splice(0)};const publisher=new EventPublisher(publish,start,cancel);eventPublisherCallback(publisher)}}exports.EventStream=EventStream},{}],25:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.deserialize2=exports.serialize2=exports.serialize=exports.detectSerializeVersion=exports.deserialize=void 0;const path_reference_1=require("./path-reference");const utils_1=require("./utils");const ascii85_1=require("./ascii85");const path_info_1=require("./path-info");const partial_array_1=require("./partial-array");const deserialize=data=>{if(data.map===null||typeof data.map==="undefined"){if(typeof data.val==="undefined"){throw new Error("serialized value must have a val property")}return data.val}const deserializeValue=(type,val)=>{if(type==="date"){return new Date(val)}else if(type==="binary"){return ascii85_1.ascii85.decode(val)}else if(type==="reference"){return new path_reference_1.PathReference(val)}else if(type==="regexp"){return new RegExp(val.pattern,val.flags)}else if(type==="array"){return new partial_array_1.PartialArray(val)}else if(type==="bigint"){return BigInt(val)}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_info_1.PathInfo.getPathKeys(path);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};exports.deserialize=deserialize;const detectSerializeVersion=data=>{if(typeof data!=="object"||data===null){return 2}if("map"in data&&"val"in data){return 1}else if("val"in data){if(Object.keys(data).length>1){return 2}return 1}return 2};exports.detectSerializeVersion=detectSerializeVersion;const serialize=obj=>{var _a;if(obj===null||typeof obj!=="object"||obj instanceof Date||obj instanceof ArrayBuffer||obj instanceof path_reference_1.PathReference||obj instanceof RegExp){const ser=(0,exports.serialize)({value:obj});return{map:(_a=ser.map)===null||_a===void 0?void 0:_a.value,val:ser.val.value}}obj=(0,utils_1.cloneObject)(obj);const process=(obj,mappings,prefix)=>{if(obj instanceof partial_array_1.PartialArray){mappings[prefix]="array"}Object.keys(obj).forEach((key=>{const val=obj[key];const path=prefix.length===0?key:`${prefix}/${key}`;if(typeof val==="bigint"){obj[key]=val.toString();mappings[path]="bigint"}else if(val instanceof Date){obj[key]=val.toISOString();mappings[path]="date"}else if(val instanceof ArrayBuffer){obj[key]=ascii85_1.ascii85.encode(val);mappings[path]="binary"}else if(val instanceof path_reference_1.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,"");const serialized={val:obj};if(Object.keys(mappings).length>0){serialized.map=mappings}return serialized};exports.serialize=serialize;const serialize2=obj=>{const getSerializedValue=val=>{if(typeof val==="bigint"){return{".type":"bigint",".val":val.toString()}}else if(val instanceof Date){return{".type":"date",".val":val.toISOString()}}else if(val instanceof ArrayBuffer){return{".type":"binary",".val":ascii85_1.ascii85.encode(val)}}else if(val instanceof path_reference_1.PathReference){return{".type":"reference",".val":val.path}}else if(val instanceof RegExp){return{".type":"regexp",".val":`/${val.source}/${val.flags}`}}else if(typeof val==="object"&&val!==null){if(val instanceof Array){const copy=[];for(let i=0;i{if(typeof data!=="object"||data===null){return data}if(typeof data[".type"]==="undefined"){if(data instanceof Array){const copy=[];const arr=data;for(let i=0;i{const mkeys=path_info_1.PathInfo.getPathKeys(mpath);if(mkeys.length!==keys.length){return false}return mkeys.every(((mkey,index)=>{if(mkey==="*"||typeof mkey==="string"&&mkey[0]==="$"){return true}return mkey===keys[index]}))}));const mapping=mappings[mappedPath];return mapping}function map(mappings,path){const targetPath=path_info_1.PathInfo.get(path).parentPath;if(targetPath===null){return}return get(mappings,targetPath)}function mapDeep(mappings,entryPath){entryPath=entryPath.replace(/^\/|\/$/g,"");const pathInfo=path_info_1.PathInfo.get(entryPath);const startPath=pathInfo.parentPath;const keys=startPath?path_info_1.PathInfo.getPathKeys(startPath):[];const matches=Object.keys(mappings).reduce(((m,mpath)=>{const mkeys=path_info_1.PathInfo.getPathKeys(mpath);if(mkeys.length{if(index>=keys.length){return false}else if(mkey==="*"||typeof mkey==="string"&&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}function process(db,mappings,path,obj,action){if(obj===null||typeof obj!=="object"){return obj}const keys=path_info_1.PathInfo.getPathKeys(path);const m=mapDeep(mappings,path);const changes=[];m.sort(((a,b)=>path_info_1.PathInfo.getPathKeys(a.path).length>path_info_1.PathInfo.getPathKeys(b.path).length?-1:1));m.forEach((mapping=>{const mkeys=path_info_1.PathInfo.getPathKeys(mapping.path);mkeys.push("*");const mTrailKeys=mkeys.slice(keys.length);if(mTrailKeys.length===0){const vars=path_info_1.PathInfo.extractVariables(mapping.path,path);const ref=new data_reference_1.DataReference(db,path,vars);if(action==="serialize"){obj=mapping.type.serialize(obj,ref)}else if(action==="deserialize"){const snap=new data_snapshot_1.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==="*"||typeof key==="string"&&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=path_info_1.PathInfo.getChildPath(parentPath,child.key);const vars=path_info_1.PathInfo.extractVariables(mapping.path,childPath);const ref=new data_reference_1.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 data_snapshot_1.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=(0,utils_1.cloneObject)(obj);if(changes.length>0){changes.forEach((change=>{change.parent[change.key]=change.original}))}}return obj}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")}}exports.TypeMappings=TypeMappings},{"./data-reference":8,"./data-snapshot":9,"./path-info":16,"./utils":27}],27:[function(require,module,exports){(function(global,Buffer){(function(){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.getGlobalObject=exports.defer=exports.getChildValues=exports.getMutations=exports.compareValues=exports.ObjectDifferences=exports.valuesAreEqual=exports.cloneObject=exports.concatTypedArrays=exports.decodeString=exports.encodeString=exports.bytesToBigint=exports.bigintToBytes=exports.bytesToNumber=exports.numberToBytes=void 0;const path_reference_1=require("./path-reference");const process_1=require("./process");const partial_array_1=require("./partial-array");function numberToBytes(number){const bytes=new Uint8Array(8);const view=new DataView(bytes.buffer);view.setFloat64(0,number);return new Array(...bytes)}exports.numberToBytes=numberToBytes;function bytesToNumber(bytes){const length=Array.isArray(bytes)?bytes.length:bytes.byteLength;if(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}exports.bytesToNumber=bytesToNumber;const hasBigIntSupport=(()=>{try{return typeof BigInt(0)==="bigint"}catch(err){return false}})();const noBigIntError="BigInt is not supported on this platform";const bigIntFunctions={bigintToBytes(number){throw new Error(noBigIntError)},bytesToBigint(bytes){throw new Error(noBigIntError)}};if(hasBigIntSupport){const big={zero:BigInt(0),one:BigInt(1),two:BigInt(2),eight:BigInt(8),ff:BigInt(255)};bigIntFunctions.bigintToBytes=function bigintToBytes(number){if(typeof number!=="bigint"){throw new Error("number must be a bigint")}const bytes=[];const negative=number>big.eight}while(number!==(negative?-big.one:big.zero));bytes.reverse();if(negative?bytes[0]<128:bytes[0]>=128){bytes.unshift(negative?255:0)}return bytes};bigIntFunctions.bytesToBigint=function bytesToBigint(bytes){const negative=bytes[0]>=128;let number=big.zero;for(let b of bytes){if(negative){b=~b&255}number=(number<128){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)}}exports.encodeString=encodeString;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 Array){buffer=Uint8Array.from(buffer)}if(!(buffer instanceof Buffer)&&"buffer"in buffer&&buffer.buffer instanceof ArrayBuffer){const typedArray=buffer;buffer=Buffer.from(typedArray.buffer,typedArray.byteOffset,typedArray.byteLength)}if(!(buffer instanceof Buffer)){throw new Error("Unsupported buffer argument")}return buffer.toString("utf-8")}else{if(!(buffer instanceof Uint8Array)&&"buffer"in buffer&&buffer["buffer"]instanceof ArrayBuffer){const typedArray=buffer;buffer=new Uint8Array(typedArray.buffer,typedArray.byteOffset,typedArray.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")}}}exports.decodeString=decodeString;function concatTypedArrays(a,b){const c=new a.constructor(a.length+b.length);c.set(a);c.set(b,a.length);return c}exports.concatTypedArrays=concatTypedArrays;function cloneObject(original,stack){var _a;if(((_a=original===null||original===void 0?void 0:original.constructor)===null||_a===void 0?void 0:_a.name)==="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 path_reference_1.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 path_reference_1.PathReference||val instanceof RegExp){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=original instanceof Array?[]:original instanceof partial_array_1.PartialArray?new partial_array_1.PartialArray:{};Object.keys(original).forEach((key=>{const val=original[key];if(typeof val==="function"){return}clone[key]=cloneValue(val)}));return clone}exports.cloneObject=cloneObject;const isTypedArray=val=>typeof val==="object"&&["ArrayBuffer","Buffer","Uint8Array","Uint16Array","Uint32Array","Int8Array","Int16Array","Int32Array"].includes(val.constructor.name);function valuesAreEqual(val1,val2){if(val1===val2){return true}if(typeof val1!==typeof val2){return false}if(typeof val1==="object"||typeof val2==="object"){if(val1===null||val2===null){return false}if(val1 instanceof path_reference_1.PathReference||val2 instanceof path_reference_1.PathReference){return val1 instanceof path_reference_1.PathReference&&val2 instanceof path_reference_1.PathReference&&val1.path===val2.path}if(val1 instanceof Date||val2 instanceof Date){return val1 instanceof Date&&val2 instanceof Date&&val1.getTime()===val2.getTime()}if(val1 instanceof Array||val2 instanceof Array){return val1 instanceof Array&&val2 instanceof Array&&val1.length===val2.length&&val1.every(((item,i)=>valuesAreEqual(val1[i],val2[i])))}if(isTypedArray(val1)||isTypedArray(val2)){if(!isTypedArray(val1)||!isTypedArray(val2)||val1.byteLength===val2.byteLength){return false}const typed1=val1 instanceof ArrayBuffer?new Uint8Array(val1):new Uint8Array(val1.buffer,val1.byteOffset,val1.byteLength),typed2=val2 instanceof ArrayBuffer?new Uint8Array(val2):new Uint8Array(val2.buffer,val2.byteOffset,val2.byteLength);return typed1.every(((val,i)=>typed2[i]===val))}const keys1=Object.keys(val1),keys2=Object.keys(val2);return keys1.length===keys2.length&&keys1.every((key=>keys2.includes(key)))&&keys1.every((key=>valuesAreEqual(val1[key],val2[key])))}return false}exports.valuesAreEqual=valuesAreEqual;class ObjectDifferences{constructor(added,removed,changed){this.added=added;this.removed=removed;this.changed=changed}forChild(key){if(this.added.includes(key)){return"added"}if(this.removed.includes(key)){return"removed"}const changed=this.changed.find((ch=>ch.key===key));return changed?changed.change:"identical"}}exports.ObjectDifferences=ObjectDifferences;function compareValues(oldVal,newVal,sortedResults=false){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(isTypedArray(oldVal)||isTypedArray(newVal)){if(!isTypedArray(oldVal)||!isTypedArray(newVal)){return"changed"}const typed1=oldVal instanceof Uint8Array?oldVal:oldVal instanceof ArrayBuffer?new Uint8Array(oldVal):new Uint8Array(oldVal.buffer,oldVal.byteOffset,oldVal.byteLength);const typed2=newVal instanceof Uint8Array?newVal:newVal instanceof ArrayBuffer?new Uint8Array(newVal):new Uint8Array(newVal.buffer,newVal.byteOffset,newVal.byteLength);return typed1.byteLength===typed2.byteLength&&typed1.every(((val,i)=>typed2[i]===val))?"identical":"changed"}else if(oldVal instanceof Date||newVal instanceof Date){return oldVal instanceof Date&&newVal instanceof Date&&oldVal.getTime()===newVal.getTime()?"identical":"changed"}else if(oldVal instanceof path_reference_1.PathReference||newVal instanceof path_reference_1.PathReference){return oldVal instanceof path_reference_1.PathReference&&newVal instanceof path_reference_1.PathReference&&oldVal.path===newVal.path?"identical":"changed"}else if(typeof oldVal==="object"){const isArray=oldVal instanceof Array;const getKeys=obj=>{let keys=Object.keys(obj).filter((key=>!voids.includes(obj[key])));if(isArray){keys=keys.map((v=>parseInt(v)))}return keys};const oldKeys=getKeys(oldVal);const newKeys=getKeys(newVal);const removedKeys=oldKeys.filter((key=>!newKeys.includes(key)));const addedKeys=newKeys.filter((key=>!oldKeys.includes(key)));const changedKeys=newKeys.reduce(((changed,key)=>{if(oldKeys.includes(key)){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 new ObjectDifferences(addedKeys,removedKeys,sortedResults?changedKeys.sort(((a,b)=>a.key{switch(compareResult){case"identical":return[];case"changed":return[{target:target,prev:prev,val:val}];case"added":return[{target:target,prev:null,val:val}];case"removed":return[{target:target,prev:prev,val:null}];default:{let changes=[];compareResult.added.forEach((key=>changes.push({target:target.concat(key),prev:null,val:val[key]})));compareResult.removed.forEach((key=>changes.push({target:target.concat(key),prev:prev[key],val:null})));compareResult.changed.forEach((item=>{const childChanges=process(target.concat(item.key),item.change,prev[item.key],val[item.key]);changes=changes.concat(childChanges)}));return changes}}};const compareResult=compareValues(oldVal,newVal,sortedResults);return process([],compareResult,oldVal,newVal)}exports.getMutations=getMutations;function 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}}exports.getChildValues=getChildValues;function defer(fn){process_1.default.nextTick(fn)}exports.defer=defer;function getGlobalObject(){var _a;if(typeof globalThis!=="undefined"){return globalThis}if(typeof global!=="undefined"){return global}if(typeof window!=="undefined"){return window}if(typeof self!=="undefined"){return self}return(_a=function(){return this}())!==null&&_a!==void 0?_a:Function("return this")()}exports.getGlobalObject=getGlobalObject}).call(this)}).call(this,typeof global!=="undefined"?global:typeof self!=="undefined"?self:typeof window!=="undefined"?window:{},require("buffer").Buffer)},{"./partial-array":15,"./path-reference":17,"./process":18,buffer:62}],28:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.BrowserAceBase=void 0;const acebase_local_js_1=require("./acebase-local.js");const index_js_1=require("./storage/custom/indexed-db/index.js");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_local_js_1.AceBase{constructor(name,settings){if(typeof settings!=="object"||typeof settings.storage!=="object"){throw new Error(deprecatedConstructorError)}super(name,settings);this.settings.ipcEvents=settings.multipleTabs===true}static WithIndexedDB(dbname,init={}){return(0,index_js_1.createIndexedDBInstance)(dbname,init)}}exports.BrowserAceBase=BrowserAceBase},{"./acebase-local.js":29,"./storage/custom/indexed-db/index.js":49}],29:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.AceBase=exports.AceBaseLocalSettings=exports.IndexedDBStorageSettings=exports.LocalStorageSettings=void 0;const acebase_core_1=require("acebase-core");const index_js_1=require("./storage/binary/index.js");const api_local_js_1=require("./api-local.js");const index_js_2=require("./storage/custom/local-storage/index.js");Object.defineProperty(exports,"LocalStorageSettings",{enumerable:true,get:function(){return index_js_2.LocalStorageSettings}});const settings_js_1=require("./storage/custom/indexed-db/settings.js");Object.defineProperty(exports,"IndexedDBStorageSettings",{enumerable:true,get:function(){return settings_js_1.IndexedDBStorageSettings}});class AceBaseLocalSettings extends acebase_core_1.AceBaseBaseSettings{constructor(options={}){super(options);if(options.storage){this.storage=options.storage;if(options.ipc){this.storage.ipc=options.ipc}if(options.transactions){this.storage.transactions=options.transactions}}}}exports.AceBaseLocalSettings=AceBaseLocalSettings;class AceBase extends acebase_core_1.AceBaseBase{constructor(dbname,init={}){const settings=new AceBaseLocalSettings(init);super(dbname,settings);this.recovery={repairNode:async(path,options)=>{await this.ready();if(this.api.storage instanceof index_js_1.AceBaseStorage){await this.api.storage.repairNode(path,options)}else if(!this.api.storage.repairNode){throw new Error(`repairNode is not supported with chosen storage engine`)}},repairNodeTree:async path=>{await this.ready();const storage=this.api.storage;await storage.repairNodeTree(path)}};const apiSettings={db:this,settings:settings};this.api=new api_local_js_1.LocalApi(dbname,apiSettings,(()=>{this.emit("ready")}))}async close(){await this.api.storage.close()}get settings(){const ipc=this.api.storage.ipc,debug=this.debug;return{get logLevel(){return debug.level},set logLevel(level){debug.setLevel(level)},get ipcEvents(){return ipc.eventsEnabled},set ipcEvents(enabled){ipc.eventsEnabled=enabled}}}static WithLocalStorage(dbname,settings={}){const db=(0,index_js_2.createLocalStorageInstance)(dbname,settings);return db}static WithIndexedDB(dbname,init={}){throw new Error(`IndexedDB storage can only be used in browser contexts`)}}exports.AceBase=AceBase},{"./api-local.js":30,"./storage/binary/index.js":45,"./storage/custom/indexed-db/settings.js":50,"./storage/custom/local-storage/index.js":52,"acebase-core":12}],30:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.LocalApi=void 0;const acebase_core_1=require("acebase-core");const index_js_1=require("./storage/binary/index.js");const index_js_2=require("./storage/sqlite/index.js");const index_js_3=require("./storage/mssql/index.js");const index_js_4=require("./storage/custom/index.js");const node_value_types_js_1=require("./node-value-types.js");const query_js_1=require("./query.js");const node_errors_js_1=require("./node-errors.js");class LocalApi extends acebase_core_1.Api{constructor(dbname="default",init,readyCallback){super();this.db=init.db;this.logger=init.db.logger;const storageEnv={logLevel:init.settings.logLevel,logColors:init.settings.logColors,logger:init.settings.logger};if(typeof init.settings.storage==="object"){if(index_js_2.SQLiteStorageSettings&&init.settings.storage instanceof index_js_2.SQLiteStorageSettings){this.storage=new index_js_2.SQLiteStorage(dbname,init.settings.storage,storageEnv)}else if(index_js_3.MSSQLStorageSettings&&init.settings.storage instanceof index_js_3.MSSQLStorageSettings){this.storage=new index_js_3.MSSQLStorage(dbname,init.settings.storage,storageEnv)}else if(index_js_4.CustomStorageSettings&&init.settings.storage instanceof index_js_4.CustomStorageSettings){this.storage=new index_js_4.CustomStorage(dbname,init.settings.storage,storageEnv)}else{const storageSettings=init.settings.storage instanceof index_js_1.AceBaseStorageSettings?init.settings.storage:new index_js_1.AceBaseStorageSettings(init.settings.storage);this.storage=new index_js_1.AceBaseStorage(dbname,storageSettings,storageEnv)}}else{this.storage=new index_js_1.AceBaseStorage(dbname,new index_js_1.AceBaseStorageSettings,storageEnv)}this.storage.on("ready",readyCallback)}async stats(options){return this.storage.stats}subscribe(path,event,callback){this.storage.subscriptions.add(path,event,callback)}unsubscribe(path,event,callback){this.storage.subscriptions.remove(path,event,callback)}async set(path,value,options={suppress_events:false,context:null}){const cursor=await this.storage.setNode(path,value,{suppress_events:options.suppress_events,context:options.context});return Object.assign({},cursor&&{cursor:cursor})}async update(path,updates,options={suppress_events:false,context:null}){const cursor=await this.storage.updateNode(path,updates,{suppress_events:options.suppress_events,context:options.context});return Object.assign({},cursor&&{cursor:cursor})}get transactionLoggingEnabled(){return this.storage.settings.transactions&&this.storage.settings.transactions.log===true}async get(path,options){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`)}const node=await this.storage.getNode(path,options);return{value:node.value,context:{acebase_cursor:node.cursor},cursor:node.cursor}}async transaction(path,callback,options={suppress_events:false,context:null}){const cursor=await this.storage.transactNode(path,callback,{suppress_events:options.suppress_events,context:options.context});return Object.assign({},cursor&&{cursor:cursor})}async exists(path){const nodeInfo=await this.storage.getNodeInfo(path);return nodeInfo.exists}async query(path,query,options={snapshots:false}){const results=await(0,query_js_1.executeQuery)(this,path,query,options);return results}createIndex(path,key,options){return this.storage.indexes.create(path,key,options)}async getIndexes(){return this.storage.indexes.list()}async deleteIndex(filePath){return this.storage.indexes.delete(filePath)}async reflect(path,type,args){args=args||{};const getChildren=async(path,limit=50,skip=0,from=null)=>{if(typeof limit==="string"){limit=parseInt(limit)}if(typeof skip==="string"){skip=parseInt(skip)}const children=[];let n=0,stop=false,more=false;await this.storage.getChildren(path,Object.assign({},["number","string"].includes(typeof from)&&{fromKey:from})).next((childInfo=>{if(stop){more=true;return false}n++;const include=skip===0||n>skip;if(include){children.push(Object.assign({key:typeof childInfo.key==="string"?childInfo.key:childInfo.index,type:childInfo.valueTypeName,value:childInfo.value},typeof childInfo.address==="object"&&"pageNr"in childInfo.address&&{address:{pageNr:childInfo.address.pageNr,recordNr:childInfo.address.recordNr}}))}stop=limit>0&&children.length===limit})).catch((err=>{if(!(err instanceof node_errors_js_1.NodeNotFoundError)){throw err}}));return{more:more,list:children}};switch(type){case"children":{const result=await getChildren(path,args.limit,args.skip,args.from);return result}case"info":{const info={key:"",exists:false,type:"unknown",value:undefined,address:undefined,children:{count:0,more:false,list:[]}};const nodeInfo=await this.storage.getNodeInfo(path,{include_child_count:args.child_count===true});info.key=typeof nodeInfo.key!=="undefined"?nodeInfo.key:nodeInfo.index;info.exists=nodeInfo.exists;info.type=nodeInfo.exists?nodeInfo.valueTypeName:undefined;info.value=nodeInfo.value;info.address=typeof nodeInfo.address==="object"&&"pageNr"in nodeInfo.address?{pageNr:nodeInfo.address.pageNr,recordNr:nodeInfo.address.recordNr}:undefined;const isObjectOrArray=nodeInfo.exists&&nodeInfo.address&&[node_value_types_js_1.VALUE_TYPES.OBJECT,node_value_types_js_1.VALUE_TYPES.ARRAY].includes(nodeInfo.type);if(args.child_count===true){info.children={count:isObjectOrArray?nodeInfo.childCount:0}}else if(typeof args.child_limit==="number"&&args.child_limit>0){if(isObjectOrArray){info.children=await getChildren(path,args.child_limit,args.child_skip,args.child_from)}}return info}}}export(path,stream,options={format:"json",type_safe:true}){return this.storage.exportNode(path,stream,options)}import(path,read,options={format:"json",suppress_events:false,method:"set"}){return this.storage.importNode(path,read,options)}async setSchema(path,schema,warnOnly=false){return this.storage.setSchema(path,schema,warnOnly)}async getSchema(path){return this.storage.getSchema(path)}async getSchemas(){return this.storage.getSchemas()}async validateSchema(path,value,isUpdate){return this.storage.validateSchema(path,value,{updates:isUpdate})}async getMutations(filter){if(typeof this.storage.getMutations!=="function"){throw new Error("Used storage type does not support getMutations")}if(typeof filter!=="object"){throw new Error("No filter specified")}if(typeof filter.cursor!=="string"&&typeof filter.timestamp!=="number"){throw new Error("No cursor or timestamp given")}return this.storage.getMutations(filter)}async getChanges(filter){if(typeof this.storage.getChanges!=="function"){throw new Error("Used storage type does not support getChanges")}if(typeof filter!=="object"){throw new Error("No filter specified")}if(typeof filter.cursor!=="string"&&typeof filter.timestamp!=="number"){throw new Error("No cursor or timestamp given")}return this.storage.getChanges(filter)}}exports.LocalApi=LocalApi},{"./node-errors.js":38,"./node-value-types.js":41,"./query.js":44,"./storage/binary/index.js":45,"./storage/custom/index.js":48,"./storage/mssql/index.js":58,"./storage/sqlite/index.js":59,"acebase-core":12}],31:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.assert=void 0;function assert(condition,error){if(!condition){throw new Error(`Assertion failed: ${error!==null&&error!==void 0?error:"check your code"}`)}}exports.assert=assert},{}],32:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.AsyncTaskBatch=void 0;class AsyncTaskBatch{constructor(limit=1e3,options){this.limit=limit;this.options=options;this.added=0;this.scheduled=[];this.running=0;this.results=[];this.done=false}async execute(task,index){var _a,_b;try{this.running++;const result=await task();this.results[index]=result;this.running--;if(this.running===0&&this.scheduled.length===0){this.done=true;(_a=this.doneCallback)===null||_a===void 0?void 0:_a.call(this,this.results)}else if(this.scheduled.length>0){const next=this.scheduled.shift();this.execute(next.task,next.index)}}catch(err){this.done=true;(_b=this.errorCallback)===null||_b===void 0?void 0:_b.call(this,err)}}add(task){var _a;if(this.done){throw new Error(`Cannot add to a batch that has already finished. Use wait option and start batch processing manually if you are adding tasks in an async loop`)}const index=this.added++;if(((_a=this.options)===null||_a===void 0?void 0:_a.wait)!==true&&this.running{this.doneCallback=resolve;this.errorCallback=reject}));return this.results}}exports.AsyncTaskBatch=AsyncTaskBatch},{}],33:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.SchemaValidationError=exports.StorageSettings=exports.ICustomStorageNodeMetaData=exports.ICustomStorageNode=exports.CustomStorageHelpers=exports.CustomStorageSettings=exports.CustomStorageTransaction=exports.MSSQLStorageSettings=exports.SQLiteStorageSettings=exports.AceBaseStorageSettings=exports.IndexedDBStorageSettings=exports.LocalStorageSettings=exports.AceBaseLocalSettings=exports.AceBase=exports.PartialArray=exports.proxyAccess=exports.ID=exports.ObjectCollection=exports.TypeMappings=exports.PathReference=exports.EventSubscription=exports.EventStream=exports.DataReferencesArray=exports.DataSnapshotsArray=exports.DataReference=exports.DataSnapshot=void 0;const acebase_core_1=require("acebase-core");Object.defineProperty(exports,"DataReference",{enumerable:true,get:function(){return acebase_core_1.DataReference}});Object.defineProperty(exports,"DataSnapshot",{enumerable:true,get:function(){return acebase_core_1.DataSnapshot}});Object.defineProperty(exports,"EventSubscription",{enumerable:true,get:function(){return acebase_core_1.EventSubscription}});Object.defineProperty(exports,"PathReference",{enumerable:true,get:function(){return acebase_core_1.PathReference}});Object.defineProperty(exports,"TypeMappings",{enumerable:true,get:function(){return acebase_core_1.TypeMappings}});Object.defineProperty(exports,"ID",{enumerable:true,get:function(){return acebase_core_1.ID}});Object.defineProperty(exports,"proxyAccess",{enumerable:true,get:function(){return acebase_core_1.proxyAccess}});Object.defineProperty(exports,"DataSnapshotsArray",{enumerable:true,get:function(){return acebase_core_1.DataSnapshotsArray}});Object.defineProperty(exports,"ObjectCollection",{enumerable:true,get:function(){return acebase_core_1.ObjectCollection}});Object.defineProperty(exports,"DataReferencesArray",{enumerable:true,get:function(){return acebase_core_1.DataReferencesArray}});Object.defineProperty(exports,"EventStream",{enumerable:true,get:function(){return acebase_core_1.EventStream}});Object.defineProperty(exports,"PartialArray",{enumerable:true,get:function(){return acebase_core_1.PartialArray}});const acebase_local_js_1=require("./acebase-local.js");const acebase_browser_js_1=require("./acebase-browser.js");Object.defineProperty(exports,"AceBase",{enumerable:true,get:function(){return acebase_browser_js_1.BrowserAceBase}});const index_js_1=require("./storage/custom/index.js");const acebase={AceBase:acebase_browser_js_1.BrowserAceBase,AceBaseLocalSettings:acebase_local_js_1.AceBaseLocalSettings,DataReference:acebase_core_1.DataReference,DataSnapshot:acebase_core_1.DataSnapshot,EventSubscription:acebase_core_1.EventSubscription,PathReference:acebase_core_1.PathReference,TypeMappings:acebase_core_1.TypeMappings,CustomStorageSettings:index_js_1.CustomStorageSettings,CustomStorageTransaction:index_js_1.CustomStorageTransaction,CustomStorageHelpers:index_js_1.CustomStorageHelpers,ID:acebase_core_1.ID,proxyAccess:acebase_core_1.proxyAccess,DataSnapshotsArray:acebase_core_1.DataSnapshotsArray};if(typeof window!=="undefined"){window.acebase=acebase;window.AceBase=acebase_browser_js_1.BrowserAceBase}exports.default=acebase;var acebase_local_js_2=require("./acebase-local.js");Object.defineProperty(exports,"AceBaseLocalSettings",{enumerable:true,get:function(){return acebase_local_js_2.AceBaseLocalSettings}});Object.defineProperty(exports,"LocalStorageSettings",{enumerable:true,get:function(){return acebase_local_js_2.LocalStorageSettings}});Object.defineProperty(exports,"IndexedDBStorageSettings",{enumerable:true,get:function(){return acebase_local_js_2.IndexedDBStorageSettings}});var index_js_2=require("./storage/binary/index.js");Object.defineProperty(exports,"AceBaseStorageSettings",{enumerable:true,get:function(){return index_js_2.AceBaseStorageSettings}});var index_js_3=require("./storage/sqlite/index.js");Object.defineProperty(exports,"SQLiteStorageSettings",{enumerable:true,get:function(){return index_js_3.SQLiteStorageSettings}});var index_js_4=require("./storage/mssql/index.js");Object.defineProperty(exports,"MSSQLStorageSettings",{enumerable:true,get:function(){return index_js_4.MSSQLStorageSettings}});var index_js_5=require("./storage/custom/index.js");Object.defineProperty(exports,"CustomStorageTransaction",{enumerable:true,get:function(){return index_js_5.CustomStorageTransaction}});Object.defineProperty(exports,"CustomStorageSettings",{enumerable:true,get:function(){return index_js_5.CustomStorageSettings}});Object.defineProperty(exports,"CustomStorageHelpers",{enumerable:true,get:function(){return index_js_5.CustomStorageHelpers}});Object.defineProperty(exports,"ICustomStorageNode",{enumerable:true,get:function(){return index_js_5.ICustomStorageNode}});Object.defineProperty(exports,"ICustomStorageNodeMetaData",{enumerable:true,get:function(){return index_js_5.ICustomStorageNodeMetaData}});var index_js_6=require("./storage/index.js");Object.defineProperty(exports,"StorageSettings",{enumerable:true,get:function(){return index_js_6.StorageSettings}});Object.defineProperty(exports,"SchemaValidationError",{enumerable:true,get:function(){return index_js_6.SchemaValidationError}})},{"./acebase-browser.js":28,"./acebase-local.js":29,"./storage/binary/index.js":45,"./storage/custom/index.js":48,"./storage/index.js":56,"./storage/mssql/index.js":58,"./storage/sqlite/index.js":59,"acebase-core":12}],34:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.ArrayIndex=exports.GeoIndex=exports.FullTextIndex=exports.DataIndex=void 0;const not_supported_js_1=require("../not-supported.js");class DataIndex extends not_supported_js_1.NotSupported{}exports.DataIndex=DataIndex;class FullTextIndex extends not_supported_js_1.NotSupported{}exports.FullTextIndex=FullTextIndex;class GeoIndex extends not_supported_js_1.NotSupported{}exports.GeoIndex=GeoIndex;class ArrayIndex extends not_supported_js_1.NotSupported{}exports.ArrayIndex=ArrayIndex},{"../not-supported.js":42}],35:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.NetIPCServer=exports.IPCSocketPeer=exports.RemoteIPCPeer=exports.IPCPeer=void 0;const acebase_core_1=require("acebase-core");const ipc_js_1=require("./ipc.js");const not_supported_js_1=require("../not-supported.js");Object.defineProperty(exports,"RemoteIPCPeer",{enumerable:true,get:function(){return not_supported_js_1.NotSupported}});Object.defineProperty(exports,"IPCSocketPeer",{enumerable:true,get:function(){return not_supported_js_1.NotSupported}});Object.defineProperty(exports,"NetIPCServer",{enumerable:true,get:function(){return not_supported_js_1.NotSupported}});class IPCPeer extends ipc_js_1.AceBaseIPCPeer{constructor(storage){super(storage,acebase_core_1.ID.generate());this.masterPeerId=this.id;this.ipcType="browser.bcc";addEventListener("beforeunload",(()=>{this.exit()}));if(typeof BroadcastChannel!=="undefined"){this.channel=new BroadcastChannel(`acebase:${storage.name}`)}else if(typeof localStorage!=="undefined"){const listeners=[null];const notImplemented=()=>{throw new Error("Not implemented")};this.channel={name:`acebase:${storage.name}`,postMessage:message=>{const messageId=acebase_core_1.ID.generate(),key=`acebase:${storage.name}:${this.id}:${messageId}`,payload=JSON.stringify(acebase_core_1.Transport.serialize(message));localStorage.setItem(key,payload);setTimeout((()=>localStorage.removeItem(key)),10)},set onmessage(handler){listeners[0]=handler},set onmessageerror(handler){notImplemented()},close(){notImplemented()},addEventListener(event,callback){if(event!=="message"){notImplemented()}listeners.push(callback)},removeEventListener(event,callback){const i=listeners.indexOf(callback);i>=1&&listeners.splice(i,1)},dispatchEvent(event){listeners.forEach((callback=>{try{callback&&callback(event)}catch(err){console.error(err)}}));return true}};addEventListener("storage",(event=>{const[acebase,dbname,peerId,messageId]=event.key.split(":");if(acebase!=="acebase"||dbname!==storage.name||peerId===this.id||event.newValue===null){return}const message=acebase_core_1.Transport.deserialize(JSON.parse(event.newValue));this.channel.dispatchEvent({data:message})}))}else{this.logger.warn(`[BroadcastChannel] not supported`);this.sendMessage=()=>{};return}this.channel.addEventListener("message",(async event=>{const message=event.data;if(message.to&&message.to!==this.id){return}this.logger.trace(`[BroadcastChannel] received: `,message);if(message.type==="hello"&&message.frompeer.id)).concat(this.id).filter((id=>id!==this.masterPeerId));this.masterPeerId=allPeerIds.sort()[0];this.logger.info(`[BroadcastChannel] ${this.masterPeerId===this.id?"We are":`tab ${this.masterPeerId} is`} the new master. Requesting ${this._locks.length} locks (${this._locks.filter((r=>!r.granted)).length} pending)`);const requests=this._locks.splice(0);await Promise.all(requests.filter((req=>req.granted)).map((async req=>{let released,movedToParent;req.lock.release=()=>new Promise((resolve=>released=resolve));req.lock.moveToParent=()=>new Promise((resolve=>movedToParent=resolve));const lock=await this.lock({path:req.lock.path,write:req.lock.forWriting,tid:req.lock.tid,comment:req.lock.comment});if(movedToParent){const newLock=await lock.moveToParent();movedToParent(newLock)}if(released){await lock.release();released()}})));await Promise.all(requests.filter((req=>!req.granted)).map((async req=>{await this.lock(req.request)})))}return this.handleMessage(message)}));const helloMsg={type:"hello",from:this.id,data:undefined};this.sendMessage(helloMsg)}sendMessage(message){this.logger.trace(`[BroadcastChannel] sending: `,message);this.channel.postMessage(message)}}exports.IPCPeer=IPCPeer},{"../not-supported.js":42,"./ipc.js":36,"acebase-core":12}],36:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.AceBaseIPCPeer=exports.AceBaseIPCPeerExitingError=void 0;const acebase_core_1=require("acebase-core");const node_lock_js_1=require("../node-lock.js");class AceBaseIPCPeerExitingError extends Error{constructor(message){super(`Exiting: ${message}`)}}exports.AceBaseIPCPeerExitingError=AceBaseIPCPeerExitingError;class AceBaseIPCPeer extends acebase_core_1.SimpleEventEmitter{get isMaster(){return this.masterPeerId===this.id}constructor(storage,id,dbname=storage.name){super();this.storage=storage;this.id=id;this.dbname=dbname;this.ipcType="ipc";this.ourSubscriptions=[];this.remoteSubscriptions=[];this.peers=[];this._exiting=false;this._locks=[];this._requests=new Map;this._eventsEnabled=true;this._nodeLocker=new node_lock_js_1.NodeLocker(storage.logger,storage.settings.lockTimeout);this.logger=storage.logger;storage.on("subscribe",(subscription=>{const remoteSubscription=this.remoteSubscriptions.find((sub=>sub.callback===subscription.callback));if(remoteSubscription){return}const othersAlreadyNotifying=this.ourSubscriptions.some((sub=>sub.event===subscription.event&&sub.path===subscription.path));this.ourSubscriptions.push(subscription);if(othersAlreadyNotifying){return}const message={type:"subscribe",from:this.id,data:{path:subscription.path,event:subscription.event}};this.sendMessage(message)}));storage.on("unsubscribe",(subscription=>{const remoteSubscription=this.remoteSubscriptions.find((sub=>sub.callback===subscription.callback));if(remoteSubscription){this.remoteSubscriptions.splice(this.remoteSubscriptions.indexOf(remoteSubscription),1);return}this.ourSubscriptions.filter((sub=>sub.path===subscription.path&&(!subscription.event||sub.event===subscription.event)&&(!subscription.callback||sub.callback===subscription.callback))).forEach((sub=>{this.ourSubscriptions.splice(this.ourSubscriptions.indexOf(sub),1);const message={type:"unsubscribe",from:this.id,data:{path:sub.path,event:sub.event}};this.sendMessage(message)}))}))}async exit(code=0){if(this._exiting){return this.once("exit")}this._exiting=true;this.logger.warn(`Received ${this.isMaster?"master":"worker "+this.id} process exit request`);if(this._locks.length>0){this.logger.warn(`Waiting for ${this.isMaster?"master":"worker"} ${this.id} locks to clear`);await this.once("locks-cleared")}this.sayGoodbye(this.id);this.logger.warn(`${this.isMaster?"Master":"Worker "+this.id} will now exit`);this.emitOnce("exit",code)}sayGoodbye(forPeerId){const bye={type:"bye",from:forPeerId,data:undefined};this.sendMessage(bye)}addPeer(id,sendReply=true){if(this._exiting){return}const peer=this.peers.find((w=>w.id===id));if(!peer){this.peers.push({id:id,lastSeen:Date.now()})}if(sendReply){const helloMessage={type:"hello",from:this.id,to:id,data:undefined};this.sendMessage(helloMessage);this.ourSubscriptions.forEach((sub=>{const message={type:"subscribe",from:this.id,to:id,data:{path:sub.path,event:sub.event}};this.sendMessage(message)}))}}removePeer(id,ignoreUnknown=false){if(this._exiting){return}const peer=this.peers.find((peer=>peer.id===id));if(!peer){if(!ignoreUnknown){throw new Error(`We are supposed to know this peer!`)}return}this.peers.splice(this.peers.indexOf(peer),1);const subscriptions=this.remoteSubscriptions.filter((sub=>sub.for===id));subscriptions.forEach((sub=>{this.remoteSubscriptions.splice(this.remoteSubscriptions.indexOf(sub),1);this.storage.subscriptions.remove(sub.path,sub.event,sub.callback)}))}addRemoteSubscription(peerId,details){if(this._exiting){return}if(this.remoteSubscriptions.some((sub=>sub.for===peerId&&sub.event===details.event&&sub.path===details.path))){return}const subscribeCallback=(err,path,val,previous,context)=>{const eventMessage={type:"event",from:this.id,to:peerId,path:details.path,event:details.event,data:{path:path,val:val,previous:previous,context:context}};this.sendMessage(eventMessage)};this.remoteSubscriptions.push({for:peerId,event:details.event,path:details.path,callback:subscribeCallback});this.storage.subscriptions.add(details.path,details.event,subscribeCallback)}cancelRemoteSubscription(peerId,details){const sub=this.remoteSubscriptions.find((sub=>sub.for===peerId&&sub.event===details.event&&sub.path===details.event));if(!sub){return}this.storage.subscriptions.remove(details.path,details.event,sub.callback)}async handleMessage(message){switch(message.type){case"hello":return this.addPeer(message.from,message.to!==this.id);case"bye":return this.removePeer(message.from,true);case"subscribe":return this.addRemoteSubscription(message.from,message.data);case"unsubscribe":return this.cancelRemoteSubscription(message.from,message.data);case"event":{if(!this._eventsEnabled){break}const eventMessage=message;const context=eventMessage.data.context||{};context.acebase_ipc={type:this.ipcType,origin:eventMessage.from};const subscriptions=this.ourSubscriptions.filter((sub=>sub.event===eventMessage.event&&sub.path===eventMessage.path));subscriptions.forEach((sub=>{sub.callback(null,eventMessage.data.path,eventMessage.data.val,eventMessage.data.previous,context)}));break}case"lock-request":{if(!this.isMaster){throw new Error(`Workers are not supposed to receive lock requests!`)}const request=message;const result={type:"lock-result",id:request.id,from:this.id,to:request.from,ok:true,data:undefined};try{const lock=await this.lock(request.data);result.data={id:lock.id,path:lock.path,tid:lock.tid,write:lock.forWriting,expires:lock.expires,comment:lock.comment}}catch(err){result.ok=false;result.reason=err.stack||err.message||err}return this.sendMessage(result)}case"lock-result":{if(this.isMaster){throw new Error(`Masters are not supposed to receive results for lock requests!`)}const result=message;const request=this._requests.get(result.id);if(typeof request!=="object"){throw new Error(`The request must be known to us!`)}if(result.ok){request.resolve(result.data)}else{request.reject(new Error(result.reason))}return}case"unlock-request":{if(!this.isMaster){throw new Error(`Workers are not supposed to receive unlock requests!`)}const request=message;const result={type:"unlock-result",id:request.id,from:this.id,to:request.from,ok:true,data:{id:request.data.id}};try{const lockInfo=this._locks.find((l=>{var _a;return((_a=l.lock)===null||_a===void 0?void 0:_a.id)===request.data.id}));await lockInfo.lock.release()}catch(err){result.ok=false;result.reason=err.stack||err.message||err}return this.sendMessage(result)}case"unlock-result":{if(this.isMaster){throw new Error(`Masters are not supposed to receive results for unlock requests!`)}const result=message;const request=this._requests.get(result.id);if(typeof request!=="object"){throw new Error(`The request must be known to us!`)}if(result.ok){request.resolve(result.data)}else{request.reject(new Error(result.reason))}return}case"move-lock-request":{if(!this.isMaster){throw new Error(`Workers are not supposed to receive move lock requests!`)}const request=message;const result={type:"lock-result",id:request.id,from:this.id,to:request.from,ok:true,data:undefined};try{let movedLock;const lockRequest=this._locks.find((r=>{var _a;return((_a=r.lock)===null||_a===void 0?void 0:_a.id)===request.data.id}));if(request.data.move_to==="parent"){movedLock=await lockRequest.lock.moveToParent()}else{throw new Error(`Unknown lock move_to "${request.data.move_to}"`)}lockRequest.lock=movedLock;result.data={id:movedLock.id,path:movedLock.path,tid:movedLock.tid,write:movedLock.forWriting,expires:movedLock.expires,comment:movedLock.comment}}catch(err){result.ok=false;result.reason=err.stack||err.message||err}return this.sendMessage(result)}case"notification":{return this.emit("notification",message)}case"request":{return this.emit("request",message)}case"result":{const result=message;const request=this._requests.get(result.id);if(typeof request!=="object"){throw new Error(`Result of unknown request received`)}if(result.ok){request.resolve(result.data)}else{request.reject(new Error(result.reason))}}}}async lock(details){if(this._exiting){const tidApproved=this._locks.find((l=>l.tid===details.tid&&l.granted));if(!tidApproved){throw new AceBaseIPCPeerExitingError("new transaction lock denied because the IPC peer is exiting")}}const removeLock=lockDetails=>{this._locks.splice(this._locks.indexOf(lockDetails),1);if(this._locks.length===0){this.emit("locks-cleared")}};if(this.isMaster){const lockInfo={tid:details.tid,granted:false,request:details,lock:null};this._locks.push(lockInfo);const lock=await this._nodeLocker.lock(details.path,details.tid,details.write,details.comment);lockInfo.tid=lock.tid;lockInfo.granted=true;const createIPCLock=lock=>({get id(){return lock.id},get tid(){return lock.tid},get path(){return lock.path},get forWriting(){return lock.forWriting},get expires(){return lock.expires},get comment(){return lock.comment},get state(){return lock.state},release:async()=>{await lock.release();removeLock(lockInfo)},moveToParent:async()=>{const parentLock=await lock.moveToParent();lockInfo.lock=createIPCLock(parentLock);return lockInfo.lock}});lockInfo.lock=createIPCLock(lock);return lockInfo.lock}else{const lockInfo={tid:details.tid,granted:false,request:details,lock:null};this._locks.push(lockInfo);const createIPCLock=result=>{lockInfo.granted=true;lockInfo.tid=result.tid;lockInfo.lock={id:result.id,tid:result.tid,path:result.path,forWriting:result.write,state:node_lock_js_1.LOCK_STATE.LOCKED,expires:result.expires,comment:result.comment,release:async()=>{const req={type:"unlock-request",id:acebase_core_1.ID.generate(),from:this.id,to:this.masterPeerId,data:{id:lockInfo.lock.id}};await this.request(req);lockInfo.lock.state=node_lock_js_1.LOCK_STATE.DONE;removeLock(lockInfo)},moveToParent:async()=>{const req={type:"move-lock-request",id:acebase_core_1.ID.generate(),from:this.id,to:this.masterPeerId,data:{id:lockInfo.lock.id,move_to:"parent"}};let result;try{result=await this.request(req)}catch(err){lockInfo.lock.state=node_lock_js_1.LOCK_STATE.DONE;removeLock(lockInfo);throw err}lockInfo.lock=createIPCLock(result);return lockInfo.lock}};return lockInfo.lock};const req={type:"lock-request",id:acebase_core_1.ID.generate(),from:this.id,to:this.masterPeerId,data:details};let result,err;try{result=await this.request(req)}catch(e){err=e;result=null}if(err){removeLock(lockInfo);throw err}return createIPCLock(result)}}async request(req){let resolve,reject;const promise=new Promise(((rs,rj)=>{resolve=result=>{this._requests.delete(req.id);rs(result)};reject=err=>{this._requests.delete(req.id);rj(err)}}));this._requests.set(req.id,{resolve:resolve,reject:reject,request:req});this.sendMessage(req);return promise}sendRequest(request){const req={type:"request",from:this.id,to:this.masterPeerId,id:acebase_core_1.ID.generate(),data:request};return this.request(req).catch((err=>{this.logger.error(err);throw err}))}replyRequest(requestMessage,result){const reply={type:"result",id:requestMessage.id,ok:true,from:this.id,to:requestMessage.from,data:result};this.sendMessage(reply)}sendNotification(notification){const msg={type:"notification",from:this.id,data:notification};this.sendMessage(msg)}get eventsEnabled(){return this._eventsEnabled}set eventsEnabled(enabled){this.logger.info(`ipc events ${enabled?"enabled":"disabled"}`);this._eventsEnabled=enabled}}exports.AceBaseIPCPeer=AceBaseIPCPeer},{"../node-lock.js":40,"acebase-core":12}],37:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.RemovedNodeAddress=exports.NodeAddress=void 0;class NodeAddress{constructor(path){this.path=path}toString(){return`"/${this.path}"`}equals(address){return this.path===address.path}}exports.NodeAddress=NodeAddress;class RemovedNodeAddress extends NodeAddress{constructor(path){super(path)}toString(){return`"/${this.path}" (removed)`}equals(address){return address instanceof RemovedNodeAddress&&this.path===address.path}}exports.RemovedNodeAddress=RemovedNodeAddress},{}],38:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.NodeRevisionError=exports.NodeNotFoundError=void 0;class NodeNotFoundError extends Error{}exports.NodeNotFoundError=NodeNotFoundError;class NodeRevisionError extends Error{}exports.NodeRevisionError=NodeRevisionError},{}],39:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.NodeInfo=void 0;const acebase_core_1=require("acebase-core");const node_value_types_js_1=require("./node-value-types.js");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;this.childCount=info.childCount;if(typeof this.path==="string"&&(typeof this.key==="undefined"&&typeof this.index==="undefined")){const pathInfo=acebase_core_1.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(0,node_value_types_js_1.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.toString()}`}else{return`"${this.path}" is ${this.valueTypeName} with value ${this.value}`}}}exports.NodeInfo=NodeInfo},{"./node-value-types.js":41,"acebase-core":12}],40:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.NodeLock=exports.NodeLocker=exports.NodeLockError=exports.LOCK_STATE=void 0;const acebase_core_1=require("acebase-core");const assert_js_1=require("./assert.js");const DEBUG_MODE=false;const DEFAULT_LOCK_TIMEOUT=120;exports.LOCK_STATE={PENDING:"pending",LOCKED:"locked",EXPIRED:"expired",DONE:"done"};class NodeLockError extends Error{constructor(message,lock){super(message);this.lock=lock}}exports.NodeLockError=NodeLockError;class NodeLocker{constructor(logger,lockTimeout=DEFAULT_LOCK_TIMEOUT){this.logger=logger;this._locks=[];this._lastTid=0;this.timeout=lockTimeout*1e3}setTimeout(timeout){this.timeout=timeout*1e3}createTid(){return DEBUG_MODE?++this._lastTid:acebase_core_1.ID.generate()}_allowLock(path,tid,forWriting){const conflict=this._locks.find((otherLock=>otherLock.tid!==tid&&otherLock.state===exports.LOCK_STATE.LOCKED&&(forWriting||otherLock.forWriting)));return{allow:!conflict,conflict:conflict}}quit(){return new Promise((resolve=>{if(this._locks.length===0){return resolve()}this._quit=resolve}))}_rejectLock(lock,err){this._locks.splice(this._locks.indexOf(lock),1);clearTimeout(lock.timeout);try{lock.reject(err)}catch(err){console.error(`Unhandled promise rejection:`,err)}}_processLockQueue(){if(this._quit){const quitError=new Error("Quitting");this._locks.filter((lock=>lock.state===exports.LOCK_STATE.PENDING)).forEach((lock=>this._rejectLock(lock,quitError)));if(this._locks.length===0){this._quit()}}const pending=this._locks.filter((lock=>lock.state===exports.LOCK_STATE.PENDING)).sort(((a,b)=>{if(a.priority&&!b.priority){return-1}else if(!a.priority&&b.priority){return 1}return a.requested-b.requested}));pending.forEach((lock=>{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((err=>this._rejectLock(lock,err)))}}))}async lock(path,tid,forWriting=true,comment="",options={withPriority:false,noTimeout:false}){let lock,proceed;if(path instanceof NodeLock){lock=path;proceed=true}else if(this._locks.findIndex((l=>l.tid===tid&&l.state===exports.LOCK_STATE.EXPIRED))>=0){const expiredLock=this._locks.find((l=>l.tid===tid&&l.state===exports.LOCK_STATE.EXPIRED));throw new NodeLockError(`lock on tid ${tid} has expired, not allowed to continue`,expiredLock!==null&&expiredLock!==void 0?expiredLock:null)}else if(this._quit&&!options.withPriority){const refLock=this._locks.find((l=>l.tid===tid&&l.path===path));throw new NodeLockError(`Quitting`,refLock!==null&&refLock!==void 0?refLock:null)}else{DEBUG_MODE&&console.error(`${forWriting?"write":"read"} lock requested on "${path}" by tid ${tid} (${comment})`);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){DEBUG_MODE&&console.error(`${lock.forWriting?"write":"read"} lock ALLOWED on "${lock.path}" by tid ${lock.tid} (${lock.comment})`);lock.state=exports.LOCK_STATE.LOCKED;if(typeof lock.granted==="number"){}else{lock.granted=Date.now();if(options.noTimeout!==true){lock.expires=Date.now()+this.timeout;let timeoutCount=0;const timeoutHandler=()=>{if(lock.state!==exports.LOCK_STATE.LOCKED){return}timeoutCount++;if(timeoutCount<=3){this.logger.warn(`${lock.forWriting?"write":"read"} lock on "/${lock.path}" is taking long [${timeoutCount}]; tid=${lock.tid} comment=${lock.comment}`);lock.warned=true;lock.timeout=setTimeout(timeoutHandler,this.timeout/4);return}this.logger.error(`${lock.forWriting?"write":"read"} lock on "/${lock.path}" expired! tid=${lock.tid} comment=${lock.comment}`);lock.state=exports.LOCK_STATE.EXPIRED;this._processLockQueue()};lock.timeout=setTimeout(timeoutHandler,this.timeout/4)}}return lock}else{(0,assert_js_1.assert)(lock.state===exports.LOCK_STATE.PENDING);return new Promise(((resolve,reject)=>{lock.resolve=resolve;lock.reject=reject}))}}unlock(lockOrId,comment,processQueue=true){var _a,_b;let lock,i;if(lockOrId instanceof NodeLock){lock=lockOrId;i=this._locks.indexOf(lock)}else{const id=lockOrId;i=this._locks.findIndex((l=>l.id===id));lock=this._locks[i]}if(i<0){const msg=`lock on "/${(_a=lock===null||lock===void 0?void 0:lock.path)!==null&&_a!==void 0?_a:"?"}" for tid ${(_b=lock===null||lock===void 0?void 0:lock.tid)!==null&&_b!==void 0?_b:"?"} wasn't found; ${comment}`;throw new NodeLockError(msg,lock!==null&&lock!==void 0?lock:null)}lock.state=exports.LOCK_STATE.DONE;clearTimeout(lock.timeout);if(lock.warned){this.logger.info(`long running ${lock.forWriting?"write":"read"} lock on "${lock.path}" by tid ${lock.tid} has been released`)}this._locks.splice(i,1);DEBUG_MODE&&console.error(`${lock.forWriting?"write":"read"} lock RELEASED on "${lock.path}" by tid ${lock.tid}`);processQueue&&this._processLockQueue();return lock}list(){return this._locks||[]}isAllowed(path,tid,forWriting){return this._allowLock(path,tid,forWriting).allow}}exports.NodeLocker=NodeLocker;let lastid=0;class NodeLock{static get LOCK_STATE(){return exports.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=exports.LOCK_STATE.PENDING;this.requested=Date.now();this.comment="";this.waitingFor=null;this.id=++lastid;this.history=[];this.warned=false}async release(comment){this.history.push({action:"release",path:this.path,forWriting:this.forWriting,comment:comment});return this.locker.unlock(this,comment||this.comment)}async moveToParent(){const parentPath=acebase_core_1.PathInfo.get(this.path).parentPath;const allowed=this.locker.isAllowed(parentPath,this.tid,this.forWriting);if(allowed){DEBUG_MODE&&console.error(`moveToParent ALLOWED for ${this.forWriting?"write":"read"} lock on "${this.path}" by tid ${this.tid} (${this.comment})`);this.history.push({path:this.path,forWriting:this.forWriting,action:"moving to parent"});this.waitingFor=null;this.path=parentPath;return this}else{DEBUG_MODE&&console.error(`moveToParent QUEUED for ${this.forWriting?"write":"read"} lock on "${this.path}" by tid ${this.tid} (${this.comment})`);this.locker.unlock(this,`moveLockToParent: ${this.comment}`,false);const newLock=await this.locker.lock(parentPath,this.tid,this.forWriting,this.comment,{withPriority:true});DEBUG_MODE&&console.error(`QUEUED moveToParent ALLOWED for ${this.forWriting?"write":"read"} lock on "${this.path}" by tid ${this.tid} (${this.comment})`);newLock.history=this.history;newLock.history.push({path:this.path,forWriting:this.forWriting,action:"moving to parent through queue (priority)"});return newLock}}}exports.NodeLock=NodeLock},{"./assert.js":31,"acebase-core":12}],41:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.getValueType=exports.getNodeValueType=exports.getValueTypeName=exports.VALUE_TYPES=void 0;const acebase_core_1=require("acebase-core");const nodeValueTypes={OBJECT:1,ARRAY:2,NUMBER:3,BOOLEAN:4,STRING:5,BIGINT:7,DATETIME:6,BINARY:8,REFERENCE:9};exports.VALUE_TYPES=nodeValueTypes;function getValueTypeName(valueType){switch(valueType){case exports.VALUE_TYPES.ARRAY:return"array";case exports.VALUE_TYPES.BINARY:return"binary";case exports.VALUE_TYPES.BOOLEAN:return"boolean";case exports.VALUE_TYPES.DATETIME:return"date";case exports.VALUE_TYPES.NUMBER:return"number";case exports.VALUE_TYPES.OBJECT:return"object";case exports.VALUE_TYPES.REFERENCE:return"reference";case exports.VALUE_TYPES.STRING:return"string";case exports.VALUE_TYPES.BIGINT:return"bigint";default:"unknown"}}exports.getValueTypeName=getValueTypeName;function getNodeValueType(value){if(value instanceof Array){return exports.VALUE_TYPES.ARRAY}else if(value instanceof acebase_core_1.PathReference){return exports.VALUE_TYPES.REFERENCE}else if(value instanceof ArrayBuffer){return exports.VALUE_TYPES.BINARY}else if(typeof value==="string"){return exports.VALUE_TYPES.STRING}else if(typeof value==="object"){return exports.VALUE_TYPES.OBJECT}else if(typeof value==="bigint"){return exports.VALUE_TYPES.BIGINT}throw new Error(`Invalid value for standalone node: ${value}`)}exports.getNodeValueType=getNodeValueType;function getValueType(value){if(value instanceof Array){return exports.VALUE_TYPES.ARRAY}else if(value instanceof acebase_core_1.PathReference){return exports.VALUE_TYPES.REFERENCE}else if(value instanceof ArrayBuffer){return exports.VALUE_TYPES.BINARY}else if(value instanceof Date){return exports.VALUE_TYPES.DATETIME}else if(typeof value==="string"){return exports.VALUE_TYPES.STRING}else if(typeof value==="object"){return exports.VALUE_TYPES.OBJECT}else if(typeof value==="number"){return exports.VALUE_TYPES.NUMBER}else if(typeof value==="boolean"){return exports.VALUE_TYPES.BOOLEAN}else if(typeof value==="bigint"){return exports.VALUE_TYPES.BIGINT}throw new Error(`Unknown value type: ${value}`)}exports.getValueType=getValueType},{"acebase-core":12}],42:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.NotSupported=void 0;class NotSupported{constructor(context="browser"){throw new Error(`This feature is not supported in ${context} context`)}}exports.NotSupported=NotSupported},{}],43:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.pfs=void 0;class pfs{static get hasFileSystem(){return false}static get fs(){return null}}exports.pfs=pfs},{}],44:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.executeQuery=void 0;const acebase_core_1=require("acebase-core");const node_value_types_js_1=require("./node-value-types.js");const node_errors_js_1=require("./node-errors.js");const index_js_1=require("./data-index/index.js");const async_task_batch_js_1=require("./async-task-batch.js");const noop=()=>{};async function executeQuery(api,path,query,options={snapshots:false,include:undefined,exclude:undefined,child_objects:undefined,eventHandler:noop}){var _a,_b,_c,_d,_e,_f;if(typeof options!=="object"){options={}}if(typeof options.snapshots==="undefined"){options.snapshots=false}const context={};if((_a=api.storage.settings.transactions)===null||_a===void 0?void 0:_a.log){context.acebase_cursor=acebase_core_1.ID.generate()}const queryFilters=query.filters.map((f=>Object.assign({},f)));const querySort=query.order.map((s=>Object.assign({},s)));const sortMatches=matches=>{matches.sort(((a,b)=>{const compare=i=>{const o=querySort[i];const trailKeys=acebase_core_1.PathInfo.getPathKeys(typeof o.key==="number"?`[${o.key}]`:o.key);const left=trailKeys.reduce(((val,key)=>val!==null&&typeof val==="object"&&key in val?val[key]:null),a.val);const right=trailKeys.reduce(((val,key)=>val!==null&&typeof val==="object"&&key in val?val[key]:null),b.val);if(left===null){return right===null?0:o.ascending?-1:1}if(right===null){return o.ascending?1:-1}if(left==right){if(i{if(preResults.length===0){return[]}const maxBatchSize=50;const batch=new async_task_batch_js_1.AsyncTaskBatch(maxBatchSize);const results=[];preResults.forEach((({path:path},index)=>batch.add((async()=>{const node=await api.storage.getNode(path,options);const val=node.value;if(val===null){api.logger.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+Math.abs(query.take)){sortMatches(results);results.pop()}}}))));await batch.finish();return results};const pathInfo=acebase_core_1.PathInfo.get(path);const isWildcardPath=pathInfo.keys.some((key=>key==="*"||key.toString().startsWith("$")));const availableIndexes=api.storage.indexes.get(path);const usingIndexes=[];let stop=async()=>{};if(isWildcardPath){const vars=pathInfo.keys.filter((key=>typeof key==="string"&&key.startsWith("$")));const hasExplicitFilterValues=vars.length>0&&vars.every((v=>query.filters.some((f=>f.key===v&&["==","in"].includes(f.op)))));const isRealtime=typeof options.monitor==="object"&&[(_b=options.monitor)===null||_b===void 0?void 0:_b.add,(_c=options.monitor)===null||_c===void 0?void 0:_c.change,(_d=options.monitor)===null||_d===void 0?void 0:_d.remove].some((val=>val===true));if(hasExplicitFilterValues&&!isRealtime){const combinations=[];for(const v of vars){const filters=query.filters.filter((f=>f.key===v));const filterValues=filters.reduce(((values,f)=>{if(f.op==="=="){values.push(f.compare)}if(f.op==="in"){if(!(f.compare instanceof Array)){throw new Error(`compare argument for 'in' operator must be an Array`)}values.push(...f.compare)}return values}),[]);const prevCombinations=combinations.splice(0);filterValues.forEach((fv=>{if(prevCombinations.length===0){combinations.push({[v]:fv})}else{combinations.push(...prevCombinations.map((c=>Object.assign(Object.assign({},c),{[v]:fv}))))}}))}const filters=query.filters.filter((f=>!vars.includes(f.key)));const paths=combinations.map((vars=>acebase_core_1.PathInfo.get(acebase_core_1.PathInfo.getPathKeys(path).map((key=>{var _a;return(_a=vars[key])!==null&&_a!==void 0?_a:key}))).path));const loadData=query.order.length>0;const promises=paths.map((path=>{var _a;return executeQuery(api,path,{filters:filters,take:0,skip:0,order:[]},{snapshots:loadData,cache_mode:options.cache_mode,include:[...(_a=options.include)!==null&&_a!==void 0?_a:[],...query.order.map((o=>o.key))],exclude:options.exclude})}));const resultSets=await Promise.all(promises);let results=resultSets.reduce(((results,set)=>(results.push(...set.results),results)),[]);if(loadData){sortMatches(results)}if(query.skip>0){results.splice(0,query.skip)}if(query.take>0){results.splice(query.take)}if(options.snapshots&&(!loadData||((_e=options.include)===null||_e===void 0?void 0:_e.length)>0||((_f=options.exclude)===null||_f===void 0?void 0:_f.length)>0||!options.child_objects)){const{include:include,exclude:exclude,child_objects:child_objects}=options;results=await loadResultsData(results,{include:include,exclude:exclude,child_objects:child_objects})}return{results:results,context:null,stop:stop}}else if(availableIndexes.length===0){const err=new Error(`Query on wildcard path "/${path}" requires an index`);return Promise.reject(err)}if(queryFilters.length===0){const index=availableIndexes.filter((index=>index.type==="normal"))[0];queryFilters.push({key:index.key,op:"!=",compare:null})}}queryFilters.forEach((filter=>{if(filter.index){return}const indexesOnKey=availableIndexes.filter((index=>index.key===filter.key)).filter((index=>index.validOperators.includes(filter.op)));if(indexesOnKey.length>=1){const otherFilterKeys=queryFilters.filter((f=>f!==filter)).map((f=>f.key));const sortKeys=querySort.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.includes(key)));const forSorting=availableKeys.filter((key=>sortKeys.includes(key)));const forBoth=forOtherFilters.concat(forSorting.filter((index=>!forOtherFilters.includes(index))));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=>{queryFilters.filter((f=>f!==filter&&f.key===key)).forEach((f=>{if(!index_js_1.DataIndex.validOperators.includes(f.op)){return}f.indexUsage="filter";f.index=bestBenificialIndex.index}))}));bestBenificialIndex.sortKeys.forEach((key=>{querySort.filter((s=>s.key===key)).forEach((s=>{s.index=bestBenificialIndex.index}))}))}if(filter.index){usingIndexes.push({index:filter.index,description:filter.index.description})}}));if(querySort.length>0&&query.take!==0&&queryFilters.length===0){querySort.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&&api.logger.info(`Using indexes for query: ${indexDescriptions}`);const tableScanFilters=queryFilters.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=[];queryFilters.forEach((filter=>{if(filter.index&&filter.indexUsage!=="filter"){let promise=filter.index.query(filter.op,filter.compare).then((results=>{var _a,_b;(_a=options.eventHandler)===null||_a===void 0?void 0:_a.call(options,{name:"stats",type:"index_query",source:filter.index.description,stats:results.stats});if(results.hints.length>0){(_b=options.eventHandler)===null||_b===void 0?void 0:_b.call(options,{name:"hints",type:"index_query",source:filter.index.description,hints:results.hints})}return results}));const resultFilters=queryFilters.filter((f=>f.index===filter.index&&f.indexUsage==="filter"));if(resultFilters.length>0){promise=promise.then((results=>{resultFilters.forEach((filter=>{const{key:key,op:op,index:index}=filter;let{compare:compare}=filter;if(typeof compare==="string"&&!index.caseSensitive){compare=compare.toLocaleLowerCase(index.textLocale)}results=results.filterMetadata(key,op,compare)}));return results}))}indexScanPromises.push(promise)}}));const stepsExecuted={filtered:queryFilters.length===0,skipped:query.skip===0,taken:query.take===0,sorted:querySort.length===0,preDataLoaded:false,dataLoaded:false};if(queryFilters.length===0&&query.take===0){api.logger.warn(`Filterless queries must use .take to limit the results. Defaulting to 100 for query on path "${path}"`);query.take=100}if(querySort.length>0&&querySort[0].index){const sortIndex=querySort[0].index;const ascending=query.take<0?!querySort[0].ascending:querySort[0].ascending;if(queryFilters.length===0&&querySort.slice(1).every((s=>sortIndex.allMetadataKeys.includes(s.key)))){api.logger.info(`Using index for sorting: ${sortIndex.description}`);const metadataSort=querySort.slice(1).map((s=>{s.index=sortIndex;return{key:s.key,ascending:s.ascending}}));const promise=sortIndex.take(query.skip,Math.abs(query.take),{ascending:ascending,metadataSort:metadataSort}).then((results=>{var _a,_b;(_a=options.eventHandler)===null||_a===void 0?void 0:_a.call(options,{name:"stats",type:"sort_index_take",source:sortIndex.description,stats:results.stats});if(results.hints.length>0){(_b=options.eventHandler)===null||_b===void 0?void 0:_b.call(options,{name:"hints",type:"sort_index_take",source:sortIndex.description,hints:results.hints})}return results}));indexScanPromises.push(promise);stepsExecuted.skipped=true;stepsExecuted.taken=true;stepsExecuted.sorted=true}}return Promise.all(indexScanPromises).then((async 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=>m.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(querySort.length===0||querySort.every((o=>o.index))){stepsExecuted.preDataLoaded=true;if(!stepsExecuted.sorted&&querySort.length>0){sortMatches(indexedResults)}stepsExecuted.sorted=true;if(!stepsExecuted.skipped&&query.skip>0){indexedResults=query.take<0?indexedResults.slice(0,-query.skip):indexedResults.slice(query.skip)}if(!stepsExecuted.taken&&query.take!==0){indexedResults=query.take<0?indexedResults.slice(query.take):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=querySort.length>0;const childOptions=loadPartialResults?{include:querySort.map((order=>order.key))}:{include:options.include,exclude:options.exclude,child_objects:options.child_objects};return loadResultsData(indexedResults,childOptions).then((results=>{if(querySort.length>0){sortMatches(results)}stepsExecuted.sorted=true;if(query.skip>0){results=query.take<0?results.slice(0,-query.skip):results.slice(query.skip)}if(query.take!==0){results=query.take<0?results.slice(query.take):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))}let matches=[];let preliminaryStop=false;const loadPartialData=querySort.length>0;const childOptions=loadPartialData?{include:querySort.map((order=>order.key))}:{include:options.include,exclude:options.exclude,child_objects:options.child_objects};const batch={promises:[],async add(promise){this.promises.push(promise);if(this.promises.length>=1e3){await Promise.all(this.promises.splice(0))}}};try{await api.storage.getChildren(path,{keyFilter:indexKeyFilter,async:true}).next((child=>{if(child.type!==node_value_types_js_1.VALUE_TYPES.OBJECT){return}if(!child.address){return}if(preliminaryStop){return false}const matchNode=async()=>{const isMatch=await api.storage.matchNode(child.address.path,tableScanFilters);if(!isMatch){return}const childPath=child.address.path;let result;if(options.snapshots||querySort.length>0){const node=await api.storage.getNode(childPath,childOptions);result={path:childPath,val:node.value}}else{result={path:childPath}}matches.push(result);if(query.take!==0&&matches.length>Math.abs(query.take)+query.skip){if(querySort.length>0){sortMatches(matches)}else if(query.take>0){preliminaryStop=true}matches.pop()}};const p=batch.add(matchNode());if(p instanceof Promise){return p}}))}catch(reason){if(!(reason instanceof node_errors_js_1.NodeNotFoundError)){api.logger.warn(`Error getting child stream: ${reason}`)}return[]}await Promise.all(batch.promises);stepsExecuted.preDataLoaded=loadPartialData;stepsExecuted.dataLoaded=!loadPartialData;if(querySort.length>0){sortMatches(matches)}stepsExecuted.sorted=true;if(query.skip>0){matches=query.take<0?matches.slice(0,-query.skip):matches.slice(query.skip)}stepsExecuted.skipped=true;if(query.take!==0){matches=query.take<0?matches.slice(query.take):matches.slice(0,query.take)}stepsExecuted.taken=true;if(!stepsExecuted.dataLoaded){matches=await loadResultsData(matches,{include:options.include,exclude:options.exclude,child_objects:options.child_objects});stepsExecuted.dataLoaded=true}return matches})).then((matches=>{if(!stepsExecuted.sorted&&querySort.length>0){sortMatches(matches)}if(!options.snapshots){matches=matches.map((match=>match.path))}if(!stepsExecuted.skipped&&query.skip>0){matches=query.take<0?matches.slice(0,-query.skip):matches.slice(query.skip)}if(!stepsExecuted.taken&&query.take!==0){matches=query.take<0?matches.slice(query.take):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 monitor=options.monitor;const matchedPaths=options.snapshots?matches.map((match=>match.path)):matches.slice();const ref=api.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=()=>{api.unsubscribe(ref.path,"child_changed",childChangedCallback);api.unsubscribe(ref.path,"child_added",childAddedCallback);api.unsubscribe(ref.path,"notify_child_removed",childRemovedCallback)};stop=async()=>{stopMonitoring()};const childChangedCallback=async(err,path,newValue,oldValue)=>{const wasMatch=matchedPaths.includes(path);let keepMonitoring=true;const checkKeys=[];queryFilters.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=queryFilters.filter((filter=>filter.key===key));return filters.every((filter=>{var _a;if(((_a=filter.index)===null||_a===void 0?void 0:_a.textLocaleKey)&&!seenKeys.includes(filter.index.textLocaleKey)){missingKeys.push(filter.index.textLocaleKey);return true}else if(allowedTableScanOperators.includes(filter.op)){return api.storage.test(newValue[key],filter.op,filter.compare)}else{return filter.index.test(newValue,filter.op,filter.compare)}}))}));if(isMatch){missingKeys.push(...checkKeys.filter((key=>!seenKeys.includes(key))));if(!wasMatch&&missingKeys.length>0){const filterQueue=queryFilters.filter((f=>missingKeys.includes(f.key)));const simpleFilters=filterQueue.filter((f=>allowedTableScanOperators.includes(f.op)));const indexFilters=filterQueue.filter((f=>!allowedTableScanOperators.includes(f.op)));if(simpleFilters.length>0){isMatch=await api.storage.matchNode(path,simpleFilters)}if(isMatch&&indexFilters.length>0){const keysToLoad=indexFilters.reduce(((keys,filter)=>{if(!keys.includes(filter.key)){keys.push(filter.key)}if(filter.index instanceof index_js_1.FullTextIndex&&filter.index.config.localeKey&&!keys.includes(filter.index.config.localeKey)){keys.push(filter.index.config.localeKey)}return keys}),[]);const node=await api.storage.getNode(path,{include:keysToLoad});if(node.value===null){return false}isMatch=indexFilters.every((filter=>filter.index.test(node.value,filter.op,filter.compare)))}}}if(isMatch){if(!wasMatch){addMatch(path)}if(options.snapshots){const loadOptions={include:options.include,exclude:options.exclude,child_objects:options.child_objects};const node=await api.storage.getNode(path,loadOptions);newValue=node.value}if(wasMatch&&monitor.change){keepMonitoring=options.eventHandler({name:"change",path:path,value:newValue})!==false}else if(!wasMatch&&monitor.add){keepMonitoring=options.eventHandler({name:"add",path:path,value:newValue})!==false}}else if(wasMatch){removeMatch(path);if(monitor.remove){keepMonitoring=options.eventHandler({name:"remove",path:path,value:oldValue})!==false}}if(keepMonitoring===false){stopMonitoring()}};const childAddedCallback=(err,path,newValue)=>{const isMatch=queryFilters.every((filter=>{if(allowedTableScanOperators.includes(filter.op)){return api.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(monitor.add){keepMonitoring=options.eventHandler({name:"add",path:path,value:options.snapshots?newValue:null})!==false}}if(keepMonitoring===false){stopMonitoring()}};const childRemovedCallback=(err,path,newValue,oldValue)=>{let keepMonitoring=true;removeMatch(path);if(monitor.remove){keepMonitoring=options.eventHandler({name:"remove",path:path,value:options.snapshots?oldValue:null})!==false}if(keepMonitoring===false){stopMonitoring()}};if(options.monitor.add||options.monitor.change||options.monitor.remove){api.subscribe(ref.path,"child_changed",childChangedCallback)}if(options.monitor.remove){api.subscribe(ref.path,"notify_child_removed",childRemovedCallback)}if(options.monitor.add){api.subscribe(ref.path,"child_added",childAddedCallback)}}return{results:matches,context:context,stop:stop}}))}exports.executeQuery=executeQuery},{"./async-task-batch.js":32,"./data-index/index.js":34,"./node-errors.js":38,"./node-value-types.js":41,"acebase-core":12}],45:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.AceBaseStorage=exports.AceBaseStorageSettings=void 0;const not_supported_js_1=require("../../not-supported.js");class AceBaseStorageSettings extends not_supported_js_1.NotSupported{}exports.AceBaseStorageSettings=AceBaseStorageSettings;class AceBaseStorage extends not_supported_js_1.NotSupported{}exports.AceBaseStorage=AceBaseStorage},{"../../not-supported.js":42}],46:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.createIndex=void 0;const acebase_core_1=require("acebase-core");const index_js_1=require("../data-index/index.js");const index_js_2=require("../promise-fs/index.js");async function createIndex(context,path,key,options){if(!context.storage.indexes.supported){throw new Error("Indexes are not supported in current environment because it requires Node.js fs")}const{ipc:ipc,logger:logger,indexes:indexes,storage:storage}=context;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&&options.config){existingIndex.config=options.config}if(existingIndex&&rebuild!==true){logger.info(`Index on "/${path}/*/${key}" already exists`.colorize(acebase_core_1.ColorStyle.inverse));return existingIndex}if(!ipc.isMaster){const result=await ipc.sendRequest({type:"index.create",path:path,key:key,options:options});if(result.ok){return storage.indexes.add(result.fileName)}throw new Error(result.reason)}await index_js_2.pfs.mkdir(`${storage.settings.path}/${storage.name}.acebase`).catch((err=>{if(err.code!=="EEXIST"){throw err}}));const index=existingIndex||(()=>{const{include:include,caseSensitive:caseSensitive,textLocale:textLocale,textLocaleKey:textLocaleKey}=options;const indexOptions={include:include,caseSensitive:caseSensitive,textLocale:textLocale,textLocaleKey:textLocaleKey};switch(indexType){case"array":return new index_js_1.ArrayIndex(storage,path,key,Object.assign({},indexOptions));case"fulltext":return new index_js_1.FullTextIndex(storage,path,key,Object.assign(Object.assign({},indexOptions),{config:options.config}));case"geo":return new index_js_1.GeoIndex(storage,path,key,Object.assign({},indexOptions));default:return new index_js_1.DataIndex(storage,path,key,Object.assign({},indexOptions))}})();if(!existingIndex){indexes.push(index)}try{await index.build()}catch(err){context.logger.error(`Index build on "/${path}/*/${key}" failed: ${err.message} (code: ${err.code})`.colorize(acebase_core_1.ColorStyle.red));if(!existingIndex){indexes.splice(indexes.indexOf(index),1)}throw err}ipc.sendNotification({type:"index.created",fileName:index.fileName,path:path,key:key,options:options});return index}exports.createIndex=createIndex},{"../data-index/index.js":34,"../promise-fs/index.js":43,"acebase-core":12}],47:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.CustomStorageHelpers=void 0;const acebase_core_1=require("acebase-core");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 acebase_core_1.PathInfo}}exports.CustomStorageHelpers=CustomStorageHelpers},{"acebase-core":12}],48:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.CustomStorage=exports.CustomStorageNodeInfo=exports.CustomStorageNodeAddress=exports.CustomStorageSettings=exports.CustomStorageTransaction=exports.ICustomStorageNode=exports.ICustomStorageNodeMetaData=exports.CustomStorageHelpers=void 0;const acebase_core_1=require("acebase-core");const{compareValues:compareValues}=acebase_core_1.Utils;const node_info_js_1=require("../../node-info.js");const node_lock_js_1=require("../../node-lock.js");const node_value_types_js_1=require("../../node-value-types.js");const node_errors_js_1=require("../../node-errors.js");const index_js_1=require("../index.js");const helpers_js_1=require("./helpers.js");const node_address_js_1=require("../../node-address.js");const assert_js_1=require("../../assert.js");var helpers_js_2=require("./helpers.js");Object.defineProperty(exports,"CustomStorageHelpers",{enumerable:true,get:function(){return helpers_js_2.CustomStorageHelpers}});class ICustomStorageNodeMetaData{constructor(){this.revision="";this.revision_nr=0;this.created=0;this.modified=0;this.type=0}}exports.ICustomStorageNodeMetaData=ICustomStorageNodeMetaData;class ICustomStorageNode extends ICustomStorageNodeMetaData{constructor(){super();this.value=null}}exports.ICustomStorageNode=ICustomStorageNode;class CustomStorageTransaction{constructor(target){this.production=false;this.target={get originalPath(){return target.path},path:target.path,get write(){return target.write}};this.id=acebase_core_1.ID.generate()}async getChildCount(path){let childCount=0;await this.childrenOf(path,{metadata:false,value:false},(()=>{childCount++;return false}));return childCount}async getMultiple(paths){const map=new Map;await Promise.all(paths.map((path=>this.get(path).then((val=>map.set(path,val))))));return map}async setMultiple(nodes){await Promise.all(nodes.map((({path:path,node:node})=>this.set(path,node))))}async removeMultiple(paths){await Promise.all(paths.map((path=>this.remove(path))))}async 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=helpers_js_1.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}}exports.CustomStorageTransaction=CustomStorageTransaction;class CustomStorageSettings extends index_js_1.StorageSettings{constructor(settings){super(settings);this.locking=true;if(typeof settings!=="object"){throw new Error("settings missing")}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.locking=settings.locking!==false;if(this.locking){this.lockTimeout=typeof settings.lockTimeout==="number"?settings.lockTimeout:120}this.ready=settings.ready;const useLocking=this.locking;const nodeLocker=useLocking?new node_lock_js_1.NodeLocker(console,this.lockTimeout):null;this.getTransaction=async({path:path,write:write})=>{const transaction=await settings.getTransaction({path:path,write:write});(0,assert_js_1.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}}}exports.CustomStorageSettings=CustomStorageSettings;class CustomStorageNodeAddress{constructor(containerPath){this.path=containerPath}}exports.CustomStorageNodeAddress=CustomStorageNodeAddress;class CustomStorageNodeInfo extends node_info_js_1.NodeInfo{constructor(info){super(info);this.revision=info.revision;this.revision_nr=info.revision_nr;this.created=info.created;this.modified=info.modified}}exports.CustomStorageNodeInfo=CustomStorageNodeInfo;class CustomStorage extends index_js_1.Storage{constructor(dbname,settings,env){super(dbname,settings,env);this._customImplementation=settings;this._init()}async _init(){this.logger.info(`Database "${this.name}" details:`.colorize(acebase_core_1.ColorStyle.dim));this.logger.info(`- Type: CustomStorage`.colorize(acebase_core_1.ColorStyle.dim));this.logger.info(`- Path: ${this.settings.path}`.colorize(acebase_core_1.ColorStyle.dim));this.logger.info(`- Max inline value size: ${this.settings.maxInlineValueSize}`.colorize(acebase_core_1.ColorStyle.dim));this.logger.info(`- Autoremove undefined props: ${this.settings.removeVoidProperties}`.colorize(acebase_core_1.ColorStyle.dim));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")}throwImplementationError(message){throw new Error(`CustomStorage "${this._customImplementation.name}" ${message}`)}_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:node_value_types_js_1.VALUE_TYPES.DATETIME,value:val.getTime()}}else if(val instanceof acebase_core_1.PathReference){return{type:node_value_types_js_1.VALUE_TYPES.REFERENCE,value:val.path}}else if(val instanceof ArrayBuffer){return{type:node_value_types_js_1.VALUE_TYPES.BINARY,value:acebase_core_1.ascii85.encode(val)}}else if(typeof val==="object"){(0,assert_js_1.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===node_value_types_js_1.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===node_value_types_js_1.VALUE_TYPES.BINARY){return acebase_core_1.ascii85.decode(val.value)}else if(val.type===node_value_types_js_1.VALUE_TYPES.DATETIME){return new Date(val.value)}else if(val.type===node_value_types_js_1.VALUE_TYPES.REFERENCE){return new acebase_core_1.PathReference(val.value)}else{throw new Error(`Unhandled child value type ${val.type}`)}};switch(node.type){case node_value_types_js_1.VALUE_TYPES.ARRAY:case node_value_types_js_1.VALUE_TYPES.OBJECT:{const obj=node.value;Object.keys(obj).forEach((key=>{const item=obj[key];if(typeof item==="object"&&"type"in item){obj[key]=getTypedChildValue(item)}}));node.value=obj;break}case node_value_types_js_1.VALUE_TYPES.BINARY:{node.value=acebase_core_1.ascii85.decode(node.value);break}case node_value_types_js_1.VALUE_TYPES.REFERENCE:{node.value=new acebase_core_1.PathReference(node.value);break}case node_value_types_js_1.VALUE_TYPES.STRING:{break}default:throw new Error(`Invalid standalone record value type`)}}async _readNode(path,options){const node=await options.transaction.get(path);if(node===null){return null}if(typeof node!=="object"){this.throwImplementationError(`transaction.get 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=node_value_types_js_1.VALUE_TYPES.STRING}else if(typeof val==="number"){type=node_value_types_js_1.VALUE_TYPES.NUMBER}else if(typeof val==="boolean"){type=node_value_types_js_1.VALUE_TYPES.BOOLEAN}else if(val instanceof Array){type=node_value_types_js_1.VALUE_TYPES.ARRAY}else if(typeof val==="object"){if("type"in val){const serialized=val;type=serialized.type;val=serialized.value;if(type===node_value_types_js_1.VALUE_TYPES.DATETIME){val=new Date(val)}else if(type===node_value_types_js_1.VALUE_TYPES.REFERENCE){val=new acebase_core_1.PathReference(val)}}else{type=node_value_types_js_1.VALUE_TYPES.OBJECT}}else{throw new Error(`Unknown value type`)}return{type:type,value:val}}async _writeNode(path,value,options){if(!options.merge&&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`)}if(typeof options.diff==="undefined"&&typeof options.currentValue!=="undefined"){const diff=compareValues(options.currentValue,value);if(options.merge&&typeof diff==="object"){diff.removed=diff.removed.filter((key=>value[key]===null))}options.diff=diff}if(options.diff==="identical"){return}const transaction=options.transaction;const currentRow=options.currentValue===null?null:await this._readNode(path,{transaction:transaction});if(options.merge&¤tRow){if(currentRow.type===node_value_types_js_1.VALUE_TYPES.ARRAY&&!(value instanceof Array)&&typeof value==="object"&&Object.keys(value).some((key=>isNaN(parseInt(key))))){throw new Error(`Cannot merge existing array of path "${path}" with an object`)}if(value instanceof Array&¤tRow.type!==node_value_types_js_1.VALUE_TYPES.ARRAY){throw new Error(`Cannot merge existing object of path "${path}" with an array`)}}const revision=options.revision||acebase_core_1.ID.generate();const mainNode={type:currentRow&¤tRow.type===node_value_types_js_1.VALUE_TYPES.ARRAY?node_value_types_js_1.VALUE_TYPES.ARRAY:node_value_types_js_1.VALUE_TYPES.OBJECT,value:{}};const childNodeValues={};if(value instanceof Array){mainNode.type=node_value_types_js_1.VALUE_TYPES.ARRAY;const obj={};for(let i=0;i{if(!(key in value)){value[key]=null}}))}Object.keys(value).forEach((key=>{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}}))}const isArray=mainNode.type===node_value_types_js_1.VALUE_TYPES.ARRAY;if(currentRow){this.logger.info(`Node "/${path}" is being ${options.merge?"updated":"overwritten"}`.colorize(acebase_core_1.ColorStyle.cyan));if(currentIsObjectOrArray||newIsObjectOrArray){const pathInfo=acebase_core_1.PathInfo.get(path);const keys=[];let checkExecuted=false;const includeChildCheck=childPath=>{checkExecuted=true;if(!transaction.production&&!pathInfo.isParentOf(childPath)){this.throwImplementationError(`"${childPath}" is not a child of "${path}" - childrenOf must only check and return paths that are children`)}return true};const addChildPath=childPath=>{if(!checkExecuted){this.throwImplementationError(`childrenOf did not call checkCallback before addCallback`)}const key=acebase_core_1.PathInfo.get(childPath).key;keys.push(key.toString());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:[],delete:options&&options.merge?Object.keys(value).filter((key=>value[key]===null)):children.current.filter((key=>!children.new.includes(key)))};changes.update=children.new.filter((key=>children.current.includes(key)&&!changes.delete.includes(key)));if(isArray&&options.merge&&(changes.insert.length>0||changes.delete.length>0)){const newArrayKeys=changes.update.concat(changes.insert);const isExhaustive=newArrayKeys.every(((k,index,arr)=>arr.includes(index.toString())));if(!isExhaustive){throw new Error(`Elements cannot be inserted beyond, or removed before the end of an array. Rewrite the whole array at path "${path}" or change your schema to use an object collection instead`)}}const writePromises=Object.keys(childNodeValues).map((key=>{const keyOrIndex=isArray?parseInt(key):key;const childDiff=typeof options.diff==="object"?options.diff.forChild(keyOrIndex):undefined;if(childDiff==="identical"){return}const childPath=pathInfo.childPath(keyOrIndex);const childValue=childNodeValues[keyOrIndex];const currentChildValue=typeof options.currentValue==="undefined"?undefined:options.currentValue!==null&&typeof options.currentValue==="object"&&keyOrIndex in options.currentValue?options.currentValue[keyOrIndex]:null;return this._writeNode(childPath,childValue,{transaction:transaction,revision:revision,merge:false,currentValue:currentChildValue,diff:childDiff})}));const movingNodes=newIsObjectOrArray?keys.filter((key=>key in mainNode.value)):[];const deleteDedicatedKeys=changes.delete.concat(movingNodes);const deletePromises=deleteDedicatedKeys.map((key=>{const keyOrIndex=isArray?parseInt(key):key;const childPath=pathInfo.childPath(keyOrIndex);return this._deleteNode(childPath,{transaction:transaction})}));const promises=writePromises.concat(deletePromises);await Promise.all(promises)}const p=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});if(p instanceof Promise){return await p}}else{this.logger.info(`Node "/${path}" is being created`.colorize(acebase_core_1.ColorStyle.cyan));if(isArray){const arrayKeys=Object.keys(mainNode.value).concat(Object.keys(childNodeValues));const isExhaustive=arrayKeys.every(((k,index,arr)=>arr.includes(index.toString())));if(!isExhaustive){throw new Error(`Cannot store arrays with missing entries`)}}const promises=Object.keys(childNodeValues).map((key=>{const keyOrIndex=isArray?parseInt(key):key;const childPath=acebase_core_1.PathInfo.getChildPath(path,keyOrIndex);const childValue=childNodeValues[keyOrIndex];return this._writeNode(childPath,childValue,{transaction:transaction,revision:revision,merge:false,currentValue:null})}));const p=this._storeNode(path,{type:mainNode.type,value:mainNode.value,revision:revision,revision_nr:1,created:Date.now(),modified:Date.now()},{transaction:transaction});if(p instanceof Promise){promises.push(p)}await Promise.all(promises)}}async _deleteNode(path,options){const pathInfo=acebase_core_1.PathInfo.get(path);this.logger.info(`Node "/${path}" is being deleted`.colorize(acebase_core_1.ColorStyle.cyan));const deletePaths=[path];let checkExecuted=false;const includeDescendantCheck=descPath=>{checkExecuted=true;if(!transaction.production&&!pathInfo.isAncestorOf(descPath)){this.throwImplementationError(`"${descPath}" is not a descendant of "${path}" - descendantsOf must only check and return paths that are descendants`)}return true};const addDescendant=descPath=>{if(!checkExecuted){this.throwImplementationError(`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.logger.info(`Nodes ${deletePaths.map((p=>`"/${p}"`)).join(",")} are being deleted`.colorize(acebase_core_1.ColorStyle.cyan));return transaction.removeMultiple(deletePaths)}getChildren(path,options={}){let 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()=>{const node=await this._readNode(path,{transaction:transaction});if(!node){throw new node_errors_js_1.NodeNotFoundError(`Node "/${path}" does not exist`)}if(![node_value_types_js_1.VALUE_TYPES.OBJECT,node_value_types_js_1.VALUE_TYPES.ARRAY].includes(node.type)){return}const isArray=node.type===node_value_types_js_1.VALUE_TYPES.ARRAY;const value=node.value;let keys=Object.keys(value).map((key=>isArray?parseInt(key):key));if(options.keyFilter){keys=keys.filter((key=>options.keyFilter.includes(key)))}const pathInfo=acebase_core_1.PathInfo.get(path);keys.length>0&&keys.every((key=>{const 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:new Date(node.created),modified:new Date(node.modified)});canceled=callback(info)===false;return!canceled}));if(canceled){return}let checkExecuted=false;const includeChildCheck=childPath=>{checkExecuted=true;if(!transaction.production&&!pathInfo.isParentOf(childPath)){this.throwImplementationError(`"${childPath}" is not a child of "${path}" - childrenOf must only check and return paths that are children`)}if(options.keyFilter){const key=acebase_core_1.PathInfo.get(childPath).key;return options.keyFilter.includes(key)}return true};const addChildNode=(childPath,node)=>{if(!checkExecuted){this.throwImplementationError(`childrenOf did not call checkCallback before addCallback`)}const key=acebase_core_1.PathInfo.get(childPath).key;const info=new CustomStorageNodeInfo({path:childPath,type:node.type,key:isArray?null:key,index:isArray?key:null,address:new node_address_js_1.NodeAddress(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.include.length>0||options.exclude&&options.exclude.length>0||options.child_objects===false;const pathInfo=acebase_core_1.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);(0,assert_js_1.assert)(lockPath===pathInfo.parentPath,`transaction.moveToParentPath() did not move to the right parent path of "${path}"`);const parentNode=await this._readNode(pathInfo.parentPath,{transaction:transaction});if(parentNode&&[node_value_types_js_1.VALUE_TYPES.OBJECT,node_value_types_js_1.VALUE_TYPES.ARRAY].includes(parentNode.type)&&pathInfo.key in parentNode.value){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 isArray=targetNode.type===node_value_types_js_1.VALUE_TYPES.ARRAY;const convertFilterArray=arr=>{const isNumber=key=>/^[0-9]+$/.test(key);return arr.map((path=>acebase_core_1.PathInfo.get(isArray&&isNumber(path)?`[${path}]`:path)))};const includeFilter=options.include?convertFilterArray(options.include):[];const excludeFilter=options.exclude?convertFilterArray(options.exclude):[];const applyFiltersOnInlineData=(descPath,node)=>{if([node_value_types_js_1.VALUE_TYPES.OBJECT,node_value_types_js_1.VALUE_TYPES.ARRAY].includes(node.type)&&includeFilter.length>0){const trailKeys=acebase_core_1.PathInfo.getPathKeys(descPath).slice(pathInfo.keys.length);const checkPathInfo=new acebase_core_1.PathInfo(trailKeys);const remove=[];const includes=includeFilter.filter((info=>info.isDescendantOf(checkPathInfo)));if(includes.length>0){const isArray=node.type===node_value_types_js_1.VALUE_TYPES.ARRAY;remove.push(...Object.keys(node.value).map((key=>isArray?+key:key)));for(const info of includes){const targetProp=info.keys[trailKeys.length];if(typeof targetProp==="string"&&(targetProp==="*"||targetProp.startsWith("$"))){remove.splice(0);break}const index=remove.indexOf(targetProp);index>=0&&remove.splice(index,1)}}const hasIncludeOnChild=includeFilter.some((info=>info.isChildOf(checkPathInfo)));const hasExcludeOnChild=excludeFilter.some((info=>info.isChildOf(checkPathInfo)));if(hasExcludeOnChild&&!hasIncludeOnChild){const excludes=excludeFilter.filter((info=>info.isChildOf(checkPathInfo)));for(let i=0;iinfo.equals(remove[i])))){remove.splice(i,1);i--}}}for(const key of remove){delete node.value[key]}}};applyFiltersOnInlineData(path,targetNode);let checkExecuted=false;const includeDescendantCheck=(descPath,metadata)=>{checkExecuted=true;if(!transaction.production&&!pathInfo.isAncestorOf(descPath)){this.throwImplementationError(`"${descPath}" is not a descendant of "${path}" - descendantsOf must only check and return paths that are descendants`)}if(!filtered){return true}const descPathKeys=acebase_core_1.PathInfo.getPathKeys(descPath);const trailKeys=descPathKeys.slice(pathInfo.keys.length);const checkPathInfo=new acebase_core_1.PathInfo(trailKeys);let include=(includeFilter.length>0?includeFilter.some((info=>checkPathInfo.isOnTrailOf(info))):true)&&(excludeFilter.length>0?!excludeFilter.some((info=>info.equals(checkPathInfo)||info.isAncestorOf(checkPathInfo))):true);if(include&&options.child_objects===false&&(pathInfo.isParentOf(descPath)&&[node_value_types_js_1.VALUE_TYPES.OBJECT,node_value_types_js_1.VALUE_TYPES.ARRAY].includes(metadata?metadata.type:-1)||acebase_core_1.PathInfo.getPathKeys(descPath).length>pathInfo.pathKeys.length+1)){include=false}return include};const descRows=[];const addDescendant=(descPath,node)=>{if(!checkExecuted){this.throwImplementationError("descendantsOf did not call checkCallback before addCallback")}if(options.child_objects===false&&[node_value_types_js_1.VALUE_TYPES.OBJECT,node_value_types_js_1.VALUE_TYPES.ARRAY].includes(node.type)){return true}applyFiltersOnInlineData(descPath,node);this._processReadNodeValue(node);const row=node;row.path=descPath;descRows.push(row);return true};await transaction.descendantsOf(path,{metadata:true,value:true},includeDescendantCheck,addDescendant);this.logger.info(`Read node "/${path}" and ${filtered?"(filtered) ":""}descendants from ${descRows.length+1} records`.colorize(acebase_core_1.ColorStyle.magenta));const result=targetNode;const objectToArray=obj=>{const arr=[];Object.keys(obj).forEach((key=>{const index=parseInt(key);arr[index]=obj[index]}));return arr};if(targetNode.type===node_value_types_js_1.VALUE_TYPES.ARRAY){result.value=objectToArray(result.value)}if(targetNode.type===node_value_types_js_1.VALUE_TYPES.OBJECT||targetNode.type===node_value_types_js_1.VALUE_TYPES.ARRAY){const targetPathKeys=acebase_core_1.PathInfo.getPathKeys(path);const value=targetNode.value;for(let i=0;i{if(childKey in parent[key]){this.throwImplementationError(`Custom storage merge error: child key "${childKey}" is in parent value already! Make sure the get/childrenOf/descendantsOf methods of the custom storage class return values that can be modified by AceBase without affecting the stored source`)}parent[key][childKey]=nodeValue[childKey]}))}}else{parent[key]=nodeValue}parent=parent[key]}}}else if(descRows.length>0){this.throwImplementationError(`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){(0,assert_js_1.assert)(Object.keys(result.value[key]).length===0);delete result.value[key]}}))}if(options.include){}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=acebase_core_1.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=acebase_core_1.PathInfo.get(path);const transaction=options.transaction||await this._customImplementation.getTransaction({path:path,write:false});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 node_address_js_1.NodeAddress(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);(0,assert_js_1.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&&[node_value_types_js_1.VALUE_TYPES.OBJECT,node_value_types_js_1.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.include_child_count){info.childCount=0;if([node_value_types_js_1.VALUE_TYPES.OBJECT,node_value_types_js_1.VALUE_TYPES.ARRAY].includes(info.valueType)&&info.address){info.childCount=node.value?Object.keys(node.value).length:0;info.childCount+=await transaction.getChildCount(path)}}if(!options.transaction){await transaction.commit()}return info}catch(err){if(!options.transaction){await transaction.rollback(err)}throw err}}async setNode(path,value,options={suppress_events:false,context:null}){if(this.settings.readOnly){throw new Error(`Database is opened in read-only mode`)}const pathInfo=acebase_core_1.PathInfo.get(path);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,suppress_events:options.suppress_events,context:options.context})}else if(typeof options.assert_revision!=="undefined"){const info=await this.getNodeInfo(path,{transaction:transaction});if(info.revision!==options.assert_revision){throw new node_errors_js_1.NodeRevisionError(`revision '${info.revision}' does not match requested revision '${options.assert_revision}'`)}if(info.address&&info.address.path===path&&value!==null&&!this.valueFitsInline(value)){await this._writeNodeWithTracking(path,value,{merge:false,transaction:transaction,suppress_events:options.suppress_events,context:options.context})}else{const lockPath=await transaction.moveToParentPath(pathInfo.parentPath);(0,assert_js_1.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,suppress_events:options.suppress_events,context:options.context})}}else{const lockPath=await transaction.moveToParentPath(pathInfo.parentPath);(0,assert_js_1.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,suppress_events:options.suppress_events,context:options.context})}if(!options.transaction){await transaction.commit()}}catch(err){if(!options.transaction){await transaction.rollback(err)}throw err}}async updateNode(path,updates,options={suppress_events:false,context:null}){if(this.settings.readOnly){throw new Error(`Database is opened in read-only mode`)}if(typeof updates!=="object"){throw new Error(`invalid updates argument`)}else if(Object.keys(updates).length===0){return}const transaction=options.transaction||await this._customImplementation.getTransaction({path:path,write:true});try{const nodeInfo=await this.getNodeInfo(path,{transaction:transaction});const pathInfo=acebase_core_1.PathInfo.get(path);if(nodeInfo.exists&&nodeInfo.address&&nodeInfo.address.path===path){await this._writeNodeWithTracking(path,updates,{transaction:transaction,merge:true,suppress_events:options.suppress_events,context:options.context})}else if(nodeInfo.exists){const pathInfo=acebase_core_1.PathInfo.get(path);const lockPath=await transaction.moveToParentPath(pathInfo.parentPath);(0,assert_js_1.assert)(lockPath===pathInfo.parentPath,`transaction.moveToParentPath() did not move to the right parent path of "${path}"`);await this._writeNodeWithTracking(pathInfo.parentPath,{[pathInfo.key]:updates},{transaction:transaction,merge:true,suppress_events:options.suppress_events,context:options.context})}else{const lockPath=await transaction.moveToParentPath(pathInfo.parentPath);(0,assert_js_1.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,suppress_events:options.suppress_events,context:options.context})}if(!options.transaction){await transaction.commit()}}catch(err){if(!options.transaction){await transaction.rollback(err)}throw err}}}exports.CustomStorage=CustomStorage},{"../../assert.js":31,"../../node-address.js":37,"../../node-errors.js":38,"../../node-info.js":39,"../../node-lock.js":40,"../../node-value-types.js":41,"../index.js":56,"./helpers.js":47,"acebase-core":12}],49:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.createIndexedDBInstance=void 0;const acebase_core_1=require("acebase-core");const index_js_1=require("../index.js");const index_js_2=require("../../../index.js");const settings_js_1=require("./settings.js");const transaction_js_1=require("./transaction.js");function createIndexedDBInstance(dbname,init={}){const settings=new settings_js_1.IndexedDBStorageSettings(init);const request=indexedDB.open(`${dbname}.acebase`,1);request.onupgradeneeded=e=>{const db=request.result;db.createObjectStore("nodes",{keyPath:"path"});db.createObjectStore("content")};let idb;const readyPromise=new Promise(((resolve,reject)=>{request.onsuccess=e=>{idb=request.result;resolve()};request.onerror=e=>{reject(e)}}));const cache=new acebase_core_1.SimpleCache(typeof settings.cacheSeconds==="number"?settings.cacheSeconds:60);const storageSettings=new index_js_1.CustomStorageSettings({name:"IndexedDB",locking:true,removeVoidProperties:settings.removeVoidProperties,maxInlineValueSize:settings.maxInlineValueSize,lockTimeout:settings.lockTimeout,ready(){return readyPromise},async getTransaction(target){await readyPromise;const context={debug:false,db:idb,cache:cache,ipc:ipc};return new transaction_js_1.IndexedDBStorageTransaction(context,target)}});const db=new index_js_2.AceBase(dbname,{logLevel:settings.logLevel,storage:storageSettings,sponsor:settings.sponsor});const ipc=db.api.storage.ipc;db.settings.ipcEvents=settings.multipleTabs===true;ipc.on("notification",(async notification=>{const message=notification.data;if(typeof message!=="object"){return}if(message.action==="cache.invalidate"){for(const path of message.paths){cache.remove(path)}}}));return db}exports.createIndexedDBInstance=createIndexedDBInstance},{"../../../index.js":33,"../index.js":48,"./settings.js":50,"./transaction.js":51,"acebase-core":12}],50:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.IndexedDBStorageSettings=void 0;const index_js_1=require("../../index.js");class IndexedDBStorageSettings extends index_js_1.StorageSettings{constructor(settings){super(settings);this.multipleTabs=false;this.cacheSeconds=60;this.sponsor=false;if(typeof settings.logLevel==="string"){this.logLevel=settings.logLevel}if(typeof settings.multipleTabs==="boolean"){this.multipleTabs=settings.multipleTabs}if(typeof settings.cacheSeconds==="number"){this.cacheSeconds=settings.cacheSeconds}if(typeof settings.sponsor==="boolean"){this.sponsor=settings.sponsor}["type","ipc","path"].forEach((prop=>{if(prop in settings){console.warn(`${prop} setting is not supported for AceBase IndexedDBStorage`)}}))}}exports.IndexedDBStorageSettings=IndexedDBStorageSettings},{"../../index.js":56}],51:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.IndexedDBStorageTransaction=void 0;const index_js_1=require("../index.js");function _requestToPromise(request){return new Promise(((resolve,reject)=>{request.onsuccess=event=>resolve(request.result||null);request.onerror=reject}))}class IndexedDBStorageTransaction extends index_js_1.CustomStorageTransaction{constructor(context,target){super(target);this.context=context;this.production=true;this._pending=[]}_createTransaction(write=false){const tx=this.context.db.transaction(["nodes","content"],write?"readwrite":"readonly");return tx}_splitMetadata(node){const value=node.value;const copy=Object.assign({},node);delete copy.value;const metadata=copy;return{metadata:metadata,value:value}}async commit(){if(this._pending.length===0){return}const batch=this._pending.splice(0);this.context.ipc.sendNotification({action:"cache.invalidate",paths:batch.map((op=>op.path))});const tx=this._createTransaction(true);try{await new Promise(((resolve,reject)=>{let stop=false,processed=0;const handleError=err=>{stop=true;reject(err)};const handleSuccess=()=>{if(++processed===batch.length){resolve()}};batch.forEach(((op,i)=>{if(stop){return}let r1,r2;const path=op.path;if(op.action==="set"){const{metadata:metadata,value:value}=this._splitMetadata(op.node);const nodeInfo={path:path,metadata:metadata};r1=tx.objectStore("nodes").put(nodeInfo);r2=tx.objectStore("content").put(value,path);this.context.cache.set(path,op.node)}else if(op.action==="remove"){r1=tx.objectStore("content").delete(path);r2=tx.objectStore("nodes").delete(path);this.context.cache.set(path,null)}else{handleError(new Error(`Unknown pending operation "${op.action}" on path "${path}" `))}let succeeded=0;r1.onsuccess=r2.onsuccess=()=>{if(++succeeded===2){handleSuccess()}};r1.onerror=r2.onerror=handleError}))}));tx.commit&&tx.commit()}catch(err){console.error(err);tx.abort&&tx.abort();throw err}}async rollback(err){this._pending=[]}async get(path){if(this.context.cache.has(path)){const cache=this.context.cache.get(path);return cache}const tx=this._createTransaction(false);const r1=_requestToPromise(tx.objectStore("nodes").get(path));const r2=_requestToPromise(tx.objectStore("content").get(path));try{const results=await Promise.all([r1,r2]);tx.commit&&tx.commit();const info=results[0];if(!info){this.context.cache.set(path,null);return null}const node=info.metadata;node.value=results[1];this.context.cache.set(path,node);return node}catch(err){console.error(`IndexedDB get error`,err);tx.abort&&tx.abort();throw err}}set(path,node){this._pending.push({action:"set",path:path,node:node})}remove(path){this._pending.push({action:"remove",path:path})}async removeMultiple(paths){paths.forEach((path=>{this._pending.push({action:"remove",path:path})}))}childrenOf(path,include,checkCallback,addCallback){return this._getChildrenOf(path,Object.assign(Object.assign({},include),{descendants:false}),checkCallback,addCallback)}descendantsOf(path,include,checkCallback,addCallback){return this._getChildrenOf(path,Object.assign(Object.assign({},include),{descendants:true}),checkCallback,addCallback)}_getChildrenOf(path,include,checkCallback,addCallback){return new Promise(((resolve,reject)=>{const pathInfo=index_js_1.CustomStorageHelpers.PathInfo.get(path);const tx=this._createTransaction(false);const store=tx.objectStore("nodes");const query=IDBKeyRange.lowerBound(path,true);const cursor=include.metadata?store.openCursor(query):store.openKeyCursor(query);cursor.onerror=e=>{var _a;(_a=tx.abort)===null||_a===void 0?void 0:_a.call(tx);reject(e)};cursor.onsuccess=async e=>{var _a,_b,_c;const otherPath=(_b=(_a=cursor.result)===null||_a===void 0?void 0:_a.key)!==null&&_b!==void 0?_b:null;let keepGoing=true;if(otherPath===null){keepGoing=false}else if(!pathInfo.isAncestorOf(otherPath)){keepGoing=false}else if(include.descendants||pathInfo.isParentOf(otherPath)){let node;if(include.metadata){const valueCursor=cursor;const data=valueCursor.result.value;node=data.metadata}const shouldAdd=checkCallback(otherPath,node);if(shouldAdd){if(include.value){if(this.context.cache.has(otherPath)){const cache=this.context.cache.get(otherPath);node.value=cache.value}else{const req=tx.objectStore("content").get(otherPath);node.value=await new Promise(((resolve,reject)=>{req.onerror=e=>{resolve(null)};req.onsuccess=e=>{resolve(req.result)}}));this.context.cache.set(otherPath,node.value===null?null:node)}}keepGoing=addCallback(otherPath,node)}}if(keepGoing){try{cursor.result.continue()}catch(err){keepGoing=false}}if(!keepGoing){(_c=tx.commit)===null||_c===void 0?void 0:_c.call(tx);resolve()}}}))}}exports.IndexedDBStorageTransaction=IndexedDBStorageTransaction},{"../index.js":48}],52:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.createLocalStorageInstance=exports.LocalStorageTransaction=exports.LocalStorageSettings=void 0;const index_js_1=require("../index.js");const index_js_2=require("../../../index.js");const settings_js_1=require("./settings.js");Object.defineProperty(exports,"LocalStorageSettings",{enumerable:true,get:function(){return settings_js_1.LocalStorageSettings}});const transaction_js_1=require("./transaction.js");Object.defineProperty(exports,"LocalStorageTransaction",{enumerable:true,get:function(){return transaction_js_1.LocalStorageTransaction}});function createLocalStorageInstance(dbname,init={}){const settings=new settings_js_1.LocalStorageSettings(init);const ls=settings.provider?settings.provider:settings.temp?localStorage:sessionStorage;const storageSettings=new index_js_1.CustomStorageSettings({name:"LocalStorage",locking:true,removeVoidProperties:settings.removeVoidProperties,maxInlineValueSize:settings.maxInlineValueSize,async ready(){},async getTransaction(target){const context={debug:true,dbname:dbname,localStorage:ls};const transaction=new transaction_js_1.LocalStorageTransaction(context,target);return transaction}});const db=new index_js_2.AceBase(dbname,{logLevel:settings.logLevel,storage:storageSettings,sponsor:settings.sponsor});db.settings.ipcEvents=settings.multipleTabs===true;return db}exports.createLocalStorageInstance=createLocalStorageInstance},{"../../../index.js":33,"../index.js":48,"./settings.js":53,"./transaction.js":54}],53:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.LocalStorageSettings=void 0;const index_js_1=require("../../index.js");class LocalStorageSettings extends index_js_1.StorageSettings{constructor(settings){super(settings);this.temp=false;this.multipleTabs=false;if(typeof settings.temp==="boolean"){this.temp=settings.temp}if(typeof settings.provider==="object"){this.provider=settings.provider}if(typeof settings.multipleTabs==="boolean"){this.multipleTabs=settings.multipleTabs}if(typeof settings.logLevel==="string"){this.logLevel=settings.logLevel}if(typeof settings.sponsor==="boolean"){this.sponsor=settings.sponsor}["type","ipc","path"].forEach((prop=>{if(prop in settings){console.warn(`${prop} setting is not supported for AceBase LocalStorage`)}}))}}exports.LocalStorageSettings=LocalStorageSettings},{"../../index.js":56}],54:[function(require,module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.LocalStorageTransaction=void 0;const index_js_1=require("../index.js");class LocalStorageTransaction extends index_js_1.CustomStorageTransaction{constructor(context,target){super(target);this.context=context;this._storageKeysPrefix=`${this.context.dbname}.acebase::`}async commit(){}async rollback(err){}async get(path){const json=this.context.localStorage.getItem(this.getStorageKeyForPath(path));const val=JSON.parse(json);return val}async set(path,val){const json=JSON.stringify(val);this.context.localStorage.setItem(this.getStorageKeyForPath(path),json)}async remove(path){this.context.localStorage.removeItem(this.getStorageKeyForPath(path))}async childrenOf(path,include,checkCallback,addCallback){const pathInfo=index_js_1.CustomStorageHelpers.PathInfo.get(path);for(let i=0;i`notify_${event}`)));const NOOP=()=>{};class Storage extends acebase_core_1.SimpleEventEmitter{createTid(){return DEBUG_MODE?++this._lastTid:acebase_core_1.ID.generate()}constructor(name,settings,env){var _a;super();this.name=name;this.settings=settings;this._schemas=[];this._indexes=[];this._annoucedIndexes=new Map;this.indexes={get supported(){return index_js_3.pfs===null||index_js_3.pfs===void 0?void 0:index_js_3.pfs.hasFileSystem},create:(path,key,options={rebuild:false})=>{const context={storage:this,logger:this.logger,indexes:this._indexes,ipc:this.ipc};return(0,indexes_js_1.createIndex)(context,path,key,options)},get:(path,key=null)=>{if(path.includes("$")){const pathKeys=acebase_core_1.PathInfo.getPathKeys(path).map((key=>typeof key==="string"&&key.startsWith("$")?"*":key));path=new acebase_core_1.PathInfo(pathKeys).path}return this._indexes.filter((index=>index.path===path&&(key===null||key===index.key)))},getAll:(targetPath,options={parentPaths:true,childPaths:true})=>{const pathKeys=acebase_core_1.PathInfo.getPathKeys(targetPath);return this._indexes.filter((index=>{const indexKeys=acebase_core_1.PathInfo.getPathKeys(index.path+"/*");if(options.parentPaths&&indexKeys.every(((key,i)=>key==="*"||pathKeys[i]===key))&&[index.key].concat(...index.includeKeys).includes(pathKeys[indexKeys.length])){return true}else if(indexKeys.length[key,"*"].includes(indexKeys[i])))}))},list:()=>this._indexes.slice(),load:async()=>{this._indexes.splice(0);if(!index_js_3.pfs.hasFileSystem){return}let files=[];try{files=await index_js_3.pfs.readdir(`${this.settings.path}/${this.name}.acebase`)}catch(err){if(err.code!=="ENOENT"){this.logger.error(err)}}const promises=[];files.forEach((fileName=>{if(!fileName.endsWith(".idx")){return}const needsStoragePrefix=this.settings.type!=="data";const hasStoragePrefix=/^\[[a-z]+\]-/.test(fileName);if(!needsStoragePrefix&&!hasStoragePrefix||needsStoragePrefix&&fileName.startsWith(`[${this.settings.type}]-`)){const p=this.indexes.add(fileName);promises.push(p)}}));await Promise.all(promises)},add:async fileName=>{const existingIndex=this._indexes.find((index=>index.fileName===fileName));if(existingIndex){return existingIndex}else if(this._annoucedIndexes.has(fileName)){const index=await this._annoucedIndexes.get(fileName);return index}try{const indexPromise=index_js_2.DataIndex.readFromFile(this,fileName);this._annoucedIndexes.set(fileName,indexPromise);const index=await indexPromise;this._indexes.push(index);this._annoucedIndexes.delete(fileName);return index}catch(err){this.logger.error(err);return null}},delete:async fileName=>{const index=await this.indexes.remove(fileName);await index.delete();this.ipc.sendNotification({type:"index.deleted",fileName:index.fileName,path:index.path,keys:index.key})},remove:async fileName=>{const index=this._indexes.find((index=>index.fileName===fileName));if(!index){throw new Error(`Index ${fileName} not found`)}this._indexes.splice(this._indexes.indexOf(index),1);return index},close:async()=>{const promises=this.indexes.list().map((index=>index.close().catch((err=>this.logger.error(err)))));await Promise.all(promises)}};this._eventSubscriptions={};this.subscriptions={add:(path,type,callback)=>{if(SUPPORTED_EVENTS.indexOf(type)<0){throw new TypeError(`Invalid event type "${type}"`)}let pathSubs=this._eventSubscriptions[path];if(!pathSubs){pathSubs=this._eventSubscriptions[path]=[]}pathSubs.push({created:Date.now(),type:type,callback:callback});this.emit("subscribe",{path:path,event:type,callback:callback})},remove:(path,type,callback)=>{const pathSubs=this._eventSubscriptions[path];if(!pathSubs){return}const next=()=>pathSubs.findIndex((ps=>(type?ps.type===type:true)&&(callback?ps.callback===callback:true)));let i;while((i=next())>=0){pathSubs.splice(i,1)}this.emit("unsubscribe",{path:path,event:type,callback:callback})},hasValueSubscribersForPath(path){const valueNeeded=this.getValueSubscribersForPath(path);return!!valueNeeded},getValueSubscribersForPath:path=>{const pathInfo=new acebase_core_1.PathInfo(path);const valueSubscribers=[];Object.keys(this._eventSubscriptions).forEach((subscriptionPath=>{if(pathInfo.equals(subscriptionPath)||pathInfo.isDescendantOf(subscriptionPath)){const pathSubs=this._eventSubscriptions[subscriptionPath];const eventPath=acebase_core_1.PathInfo.fillVariables(subscriptionPath,path);pathSubs.filter((sub=>!sub.type.startsWith("notify_"))).forEach((sub=>{let dataPath=null;if(sub.type==="value"){dataPath=eventPath}else if(["mutated","mutations"].includes(sub.type)&&pathInfo.isDescendantOf(eventPath)){dataPath=path}else if(sub.type==="child_changed"&&path!==eventPath){const childKey=acebase_core_1.PathInfo.getPathKeys(path.slice(eventPath.length).replace(/^\//,""))[0];dataPath=acebase_core_1.PathInfo.getChildPath(eventPath,childKey)}else if(["child_added","child_removed"].includes(sub.type)&&pathInfo.isChildOf(eventPath)){const childKey=acebase_core_1.PathInfo.getPathKeys(path.slice(eventPath.length).replace(/^\//,""))[0];dataPath=acebase_core_1.PathInfo.getChildPath(eventPath,childKey)}if(dataPath!==null&&!valueSubscribers.some((s=>s.type===sub.type&&s.eventPath===eventPath))){valueSubscribers.push({type:sub.type,eventPath:eventPath,dataPath:dataPath,subscriptionPath:subscriptionPath})}}))}}));return valueSubscribers},getAllSubscribersForPath:path=>{const pathInfo=acebase_core_1.PathInfo.get(path);const subscribers=[];Object.keys(this._eventSubscriptions).forEach((subscriptionPath=>{if(pathInfo.isOnTrailOf(subscriptionPath)){const pathSubs=this._eventSubscriptions[subscriptionPath];const eventPath=acebase_core_1.PathInfo.fillVariables(subscriptionPath,path);pathSubs.forEach((sub=>{let dataPath=null;if(sub.type==="value"||sub.type==="notify_value"){dataPath=eventPath}else if(["child_changed","notify_child_changed"].includes(sub.type)){const childKey=path===eventPath||pathInfo.isAncestorOf(eventPath)?"*":acebase_core_1.PathInfo.getPathKeys(path.slice(eventPath.length).replace(/^\//,""))[0];dataPath=acebase_core_1.PathInfo.getChildPath(eventPath,childKey)}else if(["mutated","mutations","notify_mutated","notify_mutations"].includes(sub.type)){dataPath=path}else if(["child_added","child_removed","notify_child_added","notify_child_removed"].includes(sub.type)&&(pathInfo.isChildOf(eventPath)||path===eventPath||pathInfo.isAncestorOf(eventPath))){const childKey=path===eventPath||pathInfo.isAncestorOf(eventPath)?"*":acebase_core_1.PathInfo.getPathKeys(path.slice(eventPath.length).replace(/^\//,""))[0];dataPath=acebase_core_1.PathInfo.getChildPath(eventPath,childKey)}if(dataPath!==null&&!subscribers.some((s=>s.type===sub.type&&s.eventPath===eventPath&&s.subscriptionPath===subscriptionPath))){subscribers.push({type:sub.type,eventPath:eventPath,dataPath:dataPath,subscriptionPath:subscriptionPath})}}))}}));return subscribers},trigger:(event,path,dataPath,oldValue,newValue,context)=>{const pathSubscriptions=this._eventSubscriptions[path]||[];pathSubscriptions.filter((sub=>sub.type===event)).forEach((sub=>{sub.callback(null,dataPath,newValue,oldValue,context)}))}};this.logger=(_a=env.logger)!==null&&_a!==void 0?_a:new acebase_core_1.DebugLogger(env.logLevel,`[${name}${typeof settings.type==="string"&&settings.type!=="data"?`:${settings.type}`:""}]`);const ipcName=name+(typeof settings.type==="string"?`_${settings.type}`:"");const ipcSocketSettings=typeof settings.ipc==="object"&&settings.ipc!==null&&"role"in settings.ipc&&settings.ipc.role==="socket"?settings.ipc:null;if(ipcSocketSettings||settings.ipc==="socket"||settings.ipc instanceof index_js_1.NetIPCServer){const ipcSettings=Object.assign({ipcName:ipcName,server:settings.ipc instanceof index_js_1.NetIPCServer?settings.ipc:null},ipcSocketSettings&&{maxIdleTime:ipcSocketSettings.maxIdleTime,loggerPluginPath:ipcSocketSettings.loggerPluginPath});this.ipc=new index_js_1.IPCSocketPeer(this,ipcSettings)}else if(settings.ipc){const ipcClientSettings=settings.ipc;if(typeof ipcClientSettings.port!=="number"){throw new Error("IPC port number must be a number")}if(!["master","worker"].includes(ipcClientSettings.role)){throw new Error(`IPC client role must be either "master" or "worker", not "${ipcClientSettings.role}"`)}const ipcSettings=Object.assign({dbname:ipcName},ipcClientSettings);this.ipc=new index_js_1.RemoteIPCPeer(this,ipcSettings)}else{this.ipc=new index_js_1.IPCPeer(this,ipcName)}this.ipc.once("exit",(code=>{if(this.indexes.supported){this.indexes.close()}}));this.nodeLocker={lock:async(path,tid,write,comment)=>{const lock=await this.ipc.lock({path:path,tid:tid,write:write,comment:comment});return lock}};this._lastTid=0}async close(){await this.ipc.exit()}get path(){return`${this.settings.path}/${this.name}.acebase`}valueFitsInline(value){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;const eventPaths=valueSubscribers.map((sub=>({path:sub.dataPath,keys:acebase_core_1.PathInfo.getPathKeys(sub.dataPath)}))).sort(((a,b)=>{if(a.keys.lengthb.keys.length){return 1}return 0}));const first=eventPaths[0];topEventPath=first.path;if(valueSubscribers.filter((sub=>sub.dataPath===topEventPath)).every((sub=>sub.type==="mutated"||sub.type.startsWith("notify_")))){hasValueSubscribers=false}topEventPath=acebase_core_1.PathInfo.fillVariables(topEventPath,path)}const indexes=this.indexes.getAll(path,{childPaths:true,parentPaths:true}).map((index=>({index:index,keys:acebase_core_1.PathInfo.getPathKeys(index.path)}))).sort(((a,b)=>{if(a.keys.lengthb.keys.length){return 1}return 0})).map((obj=>obj.index));const keysFilter=[];if(indexes.length>0){indexes.sort(((a,b)=>{if(typeof a._pathKeys==="undefined"){a._pathKeys=acebase_core_1.PathInfo.getPathKeys(a.path)}if(typeof b._pathKeys==="undefined"){b._pathKeys=acebase_core_1.PathInfo.getPathKeys(b.path)}if(a._pathKeys.lengthb._pathKeys.length){return 1}return 0}));const topIndex=indexes[0];const topIndexPath=topIndex.path===path?path:acebase_core_1.PathInfo.fillVariables(`${topIndex.path}/*`,path);if(topIndexPath.lengthindex.path===topIndex.path)).forEach((index=>{const keys=[index.key].concat(index.includeKeys);keys.forEach((key=>!keysFilter.includes(key)&&keysFilter.push(key)))}))}}return{topEventPath:topEventPath,eventSubscriptions:eventSubscriptions,valueSubscribers:valueSubscribers,hasValueSubscribers:hasValueSubscribers,indexes:indexes,keysFilter:keysFilter}}async _writeNodeWithTracking(path,value,options={merge:false,waitForIndexUpdates:true,suppress_events:false,context:null,impact:null}){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 validation=this.validateSchema(path,value,{updates:options.merge});if(!validation.ok){throw new errors_js_1.SchemaValidationError(validation.reason)}const tid=options.tid;const transaction=options.transaction;let topEventData=null;const updateImpact=options.impact?options.impact:this.getUpdateImpact(path,options.suppress_events);const{topEventPath:topEventPath,eventSubscriptions:eventSubscriptions,hasValueSubscribers:hasValueSubscribers,indexes:indexes}=updateImpact;let{keysFilter:keysFilter}=updateImpact;const writeNode=()=>{if(typeof options._customWriteFunction==="function"){return options._customWriteFunction()}if(topEventData){const pathKeys=acebase_core_1.PathInfo.getPathKeys(path);const eventPathKeys=acebase_core_1.PathInfo.getPathKeys(topEventPath);const trailKeys=pathKeys.slice(eventPathKeys.length);let currentValue=topEventData;while(trailKeys.length>0&¤tValue!==null){const childKey=trailKeys.shift();currentValue=typeof currentValue==="object"&&childKey in currentValue?currentValue[childKey]:null}options.currentValue=currentValue}return this._writeNode(path,value,options)};const transactionLoggingEnabled=this.settings.transactions&&this.settings.transactions.log===true;if(eventSubscriptions.length===0&&indexes.length===0&&!transactionLoggingEnabled){return writeNode()}if(!hasValueSubscribers&&options.merge===true&&keysFilter.length===0){keysFilter=Object.keys(value);if(topEventPath!==path){const trailPath=path.slice(topEventPath.length);keysFilter=keysFilter.map((key=>`${trailPath}/${key}`))}}const eventNodeInfo=await this.getNodeInfo(topEventPath,{transaction:transaction,tid:tid});let currentValue=null;if(eventNodeInfo.exists){const valueOptions={transaction:transaction,tid:tid};if(keysFilter.length>0){valueOptions.include=keysFilter}if(topEventPath===""&&typeof valueOptions.include==="undefined"){this.logger.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. Use "mutated" or "mutations" events instead')}const node=await this.getNode(topEventPath,valueOptions);currentValue=node.value}topEventData=currentValue;const result=await writeNode()||{};let newTopEventData,modifiedData;if(path===topEventPath){if(options.merge){if(topEventData===null){newTopEventData=value instanceof Array?[]:{}}else{newTopEventData=topEventData instanceof Array?[]:{};Object.keys(topEventData).forEach((key=>{newTopEventData[key]=topEventData[key]}))}}else{newTopEventData=value}modifiedData=newTopEventData}else{const trailPath=path.slice(topEventPath.length).replace(/^\//,"");const trailKeys=acebase_core_1.PathInfo.getPathKeys(trailPath);if(topEventData===null){newTopEventData=typeof trailKeys[0]==="number"?[]:{}}else{newTopEventData=topEventData instanceof Array?[]:{};Object.keys(topEventData).forEach((key=>{newTopEventData[key]=topEventData[key]}))}modifiedData=newTopEventData;while(trailKeys.length>0){const childKey=trailKeys.shift();if(!options.merge&&trailKeys.length===0){modifiedData[childKey]=value}else{const original=modifiedData[childKey];const shallowCopy=typeof childKey==="number"?[...original]:Object.assign({},original);modifiedData[childKey]=shallowCopy}modifiedData=modifiedData[childKey]}}if(options.merge){Object.keys(value).forEach((key=>{modifiedData[key]=value[key]}))}const dataChanges=compareValues(topEventData,newTopEventData);if(dataChanges==="identical"){result.mutations=[];return result}function removeNulls(obj){if(obj===null||typeof obj!=="object"){return obj}Object.keys(obj).forEach((prop=>{const val=obj[prop];if(val===null){delete obj[prop];if(obj instanceof Array){obj.length--}}if(typeof val==="object"){removeNulls(val)}}))}removeNulls(newTopEventData);const indexUpdates=[];indexes.map((index=>({index:index,keys:acebase_core_1.PathInfo.getPathKeys(index.path)}))).sort(((a,b)=>{if(a.keys.lengthb.keys.length){return-1}return 0})).forEach((({index:index})=>{const pathKeys=acebase_core_1.PathInfo.getPathKeys(topEventPath);const indexPathKeys=acebase_core_1.PathInfo.getPathKeys(index.path+"/*");const trailKeys=indexPathKeys.slice(pathKeys.length);const oldValue=topEventData;const newValue=newTopEventData;if(trailKeys.length===0){(0,assert_js_1.assert)(pathKeys.length===indexPathKeys.length,"check logic");const p=this.ipc.isMaster?index.handleRecordUpdate(topEventPath,oldValue,newValue):this.ipc.sendRequest({type:"index.update",fileName:index.fileName,path:topEventPath,oldValue:oldValue,newValue:newValue});indexUpdates.push(p);return}const getAllIndexUpdates=(path,oldValue,newValue)=>{if(oldValue===null&&newValue===null){return[]}const pathKeys=acebase_core_1.PathInfo.getPathKeys(path);const indexPathKeys=acebase_core_1.PathInfo.getPathKeys(index.path+"/*");const trailKeys=indexPathKeys.slice(pathKeys.length);if(trailKeys.length===0){(0,assert_js_1.assert)(pathKeys.length===indexPathKeys.length,"check logic");return[{path:path,oldValue:oldValue,newValue:newValue}]}let results=[];let trailPath="";while(trailKeys.length>0){const subKey=trailKeys.shift();if(typeof subKey==="string"&&(subKey==="*"||subKey.startsWith("$"))){const 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 childPath=acebase_core_1.PathInfo.getChildPath(trailPath,key);const childValues=getChildValues(key,oldValue,newValue);const subTrailPath=acebase_core_1.PathInfo.getChildPath(path,childPath);const childResults=getAllIndexUpdates(subTrailPath,childValues.oldValue,childValues.newValue);results=results.concat(childResults)}));break}else{const values=getChildValues(subKey,oldValue,newValue);oldValue=values.oldValue;newValue=values.newValue;if(oldValue===null&&newValue===null){break}trailPath=acebase_core_1.PathInfo.getChildPath(trailPath,subKey)}}return results};const results=getAllIndexUpdates(topEventPath,oldValue,newValue);results.forEach((result=>{const p=this.ipc.isMaster?index.handleRecordUpdate(result.path,result.oldValue,result.newValue):this.ipc.sendRequest({type:"index.update",fileName:index.fileName,path:result.path,oldValue:result.oldValue,newValue: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==="mutated"){return}else if(type==="child_changed"&&(oldValue===null||newValue===null)){trigger=false}else if(type==="value"||type==="child_changed"){const 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}if(!trigger){return}const pathKeys=acebase_core_1.PathInfo.getPathKeys(sub.dataPath);variables.forEach((variable=>{const index=pathKeys.indexOf(variable.name);(0,assert_js_1.assert)(index>=0,`Variable "${variable.name}" not found in subscription dataPath "${sub.dataPath}"`);pathKeys[index]=variable.value}));const dataPath=pathKeys.reduce(((path,key)=>acebase_core_1.PathInfo.getChildPath(path,key)),"");this.subscriptions.trigger(sub.type,sub.subscriptionPath,dataPath,oldValue,newValue,options.context)};const prepareMutationEvents=(currentPath,oldValue,newValue,compareResult)=>{const batch=[];const result=compareResult||compareValues(oldValue,newValue);if(result==="identical"){return batch}else if(typeof result==="string"){batch.push({path:currentPath,oldValue:oldValue,newValue:newValue})}else{result.changed.forEach((info=>{const childPath=acebase_core_1.PathInfo.getChildPath(currentPath,info.key);const childValues=getChildValues(info.key,oldValue,newValue);const childBatch=prepareMutationEvents(childPath,childValues.oldValue,childValues.newValue,info.change);batch.push(...childBatch)}));result.added.forEach((key=>{const childPath=acebase_core_1.PathInfo.getChildPath(currentPath,key);batch.push({path:childPath,oldValue:null,newValue:newValue[key]})}));if(oldValue instanceof Array&&newValue instanceof Array){result.removed.sort(((a,b)=>a{const childPath=acebase_core_1.PathInfo.getChildPath(currentPath,key);batch.push({path:childPath,oldValue:oldValue[key],newValue:null})}))}return batch};if(transactionLoggingEnabled&&this.settings.type!=="transaction"){result.mutations=(()=>{const trailPath=path.slice(topEventPath.length).replace(/^\//,"");const trailKeys=acebase_core_1.PathInfo.getPathKeys(trailPath);let oldValue=topEventData,newValue=newTopEventData;while(trailKeys.length>0){const key=trailKeys.shift();({oldValue:oldValue,newValue:newValue}=getChildValues(key,oldValue,newValue))}const compareResults=compareValues(oldValue,newValue);const batch=prepareMutationEvents(path,oldValue,newValue,compareResults);const mutations=batch.map((m=>({target:acebase_core_1.PathInfo.getPathKeys(m.path.slice(path.length)),prev:m.oldValue,val:m.newValue})));return mutations})()}const triggerAllEvents=()=>{eventSubscriptions.filter((sub=>!["mutated","mutations","notify_mutated","notify_mutations"].includes(sub.type))).map((sub=>{const keys=acebase_core_1.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=[])=>{const trailPath=sub.dataPath.slice(currentPath.length).replace(/^\//,"");const trailKeys=acebase_core_1.PathInfo.getPathKeys(trailPath);while(trailKeys.length>0){const subKey=trailKeys.shift();if(typeof subKey==="string"&&(subKey==="*"||subKey[0]==="$")){const allKeys=oldValue===null?[]:Object.keys(oldValue).map((key=>oldValue instanceof Array?parseInt(key):key));newValue!==null&&Object.keys(newValue).forEach((key=>{const keyOrIndex=newValue instanceof Array?parseInt(key):key;!allKeys.includes(keyOrIndex)&&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(acebase_core_1.PathInfo.getChildPath(currentPath,subKey),childValues.oldValue,childValues.newValue,vars)}}));return}else{currentPath=acebase_core_1.PathInfo.getChildPath(currentPath,subKey);const childValues=getChildValues(subKey,oldValue,newValue);oldValue=childValues.oldValue;newValue=childValues.newValue}}callSubscriberWithValues(sub,oldValue,newValue,variables)};if(sub.type.startsWith("notify_")&&acebase_core_1.PathInfo.get(sub.eventPath).isAncestorOf(topEventPath)){const isOnParentPath=acebase_core_1.PathInfo.get(sub.eventPath).isParentOf(topEventPath);const trigger=sub.type==="notify_value"||sub.type==="notify_child_changed"&&(!isOnParentPath||!["added","removed"].includes(dataChanges))||sub.type==="notify_child_removed"&&dataChanges==="removed"&&isOnParentPath||sub.type==="notify_child_added"&&dataChanges==="added"&&isOnParentPath;trigger&&this.subscriptions.trigger(sub.type,sub.subscriptionPath,sub.dataPath,null,null,options.context)}else{process(topEventPath,topEventData,newTopEventData)}}));const mutationEvents=eventSubscriptions.filter((sub=>["mutated","mutations","notify_mutated","notify_mutations"].includes(sub.type)));mutationEvents.forEach((sub=>{const currentPath=topEventPath;const trailKeys=acebase_core_1.PathInfo.getPathKeys(sub.eventPath).slice(acebase_core_1.PathInfo.getPathKeys(currentPath).length);const events=[];const oldValue=topEventData;const newValue=newTopEventData;const processNextTrailKey=(target,currentTarget,oldValue,newValue,vars)=>{if(target.length===0){return events.push({target:currentTarget,oldValue:oldValue,newValue:newValue,vars:vars})}const subKey=target[0];const keys=new Set;const isWildcardKey=typeof subKey==="string"&&(subKey==="*"||subKey.startsWith("$"));if(isWildcardKey){if(oldValue!==null&&typeof oldValue==="object"){Object.keys(oldValue).forEach((key=>keys.add(key)))}if(newValue!==null&&typeof newValue==="object"){Object.keys(newValue).forEach((key=>keys.add(key)))}}else{keys.add(subKey)}for(const key of keys){const childValues=getChildValues(key,oldValue,newValue);oldValue=childValues.oldValue;newValue=childValues.newValue;processNextTrailKey(target.slice(1),currentTarget.concat(key),oldValue,newValue,isWildcardKey?vars.concat({name:subKey,value:key}):vars)}};processNextTrailKey(trailKeys,[],oldValue,newValue,[]);for(const event of events){const targetPath=acebase_core_1.PathInfo.get(currentPath).child(event.target).path;const batch=prepareMutationEvents(targetPath,event.oldValue,event.newValue);if(batch.length===0){continue}const isNotifyEvent=sub.type.startsWith("notify_");if(["mutated","notify_mutated"].includes(sub.type)){batch.forEach(((mutation,index)=>{const context=options.context;const prevVal=isNotifyEvent?null:mutation.oldValue;const newVal=isNotifyEvent?null:mutation.newValue;this.subscriptions.trigger(sub.type,sub.subscriptionPath,mutation.path,prevVal,newVal,context)}))}else if(["mutations","notify_mutations"].includes(sub.type)){const subscriptionPathKeys=acebase_core_1.PathInfo.getPathKeys(sub.subscriptionPath);const values=isNotifyEvent?null:batch.map((m=>({target:acebase_core_1.PathInfo.getPathKeys(m.path).slice(subscriptionPathKeys.length),prev:m.oldValue,val:m.newValue})));const dataPath=acebase_core_1.PathInfo.get(acebase_core_1.PathInfo.getPathKeys(targetPath).slice(0,subscriptionPathKeys.length)).path;this.subscriptions.trigger(sub.type,sub.subscriptionPath,dataPath,null,values,options.context)}}}))};if(options.waitForIndexUpdates===false){indexUpdates.splice(0)}await Promise.all(indexUpdates);defer(triggerAllEvents);return result}getChildren(path,options){throw new Error("This method must be implemented by subclass")}async getNodeValue(path,options={}){const node=await this.getNode(path,options);return node.value}getNode(path,options){throw new Error("This method must be implemented by subclass")}getNodeInfo(path,options){throw new Error("This method must be implemented by subclass")}setNode(path,value,options){throw new Error("This method must be implemented by subclass")}updateNode(path,updates,options){throw new Error("This method must be implemented by subclass")}async transactNode(path,callback,options={no_lock:false,suppress_events:false,context:null}){const useFakeLock=options&&options.no_lock===true;const tid=this.createTid();const lock=useFakeLock?{tid:tid,release:NOOP}:await this.nodeLocker.lock(path,tid,true,"transactNode");try{let changed=false;const changeCallback=()=>{changed=true};if(useFakeLock){this.subscriptions.add(path,"notify_value",changeCallback)}const node=await this.getNode(path,{tid:tid});const checkRevision=node.revision;let newValue;try{newValue=callback(node.value);if(newValue instanceof Promise){newValue=await newValue.catch((err=>{this.logger.error(`Error in transaction callback: ${err.message}`)}))}}catch(err){this.logger.error(`Error in transaction callback: ${err.message}`)}if(typeof newValue==="undefined"){return}if(useFakeLock){this.subscriptions.remove(path,"notify_value",changeCallback)}if(changed){throw new node_errors_js_1.NodeRevisionError("Node changed")}const cursor=await this.setNode(path,newValue,{assert_revision:checkRevision,tid:lock.tid,suppress_events:options.suppress_events,context:options.context});return cursor}catch(err){if(err instanceof node_errors_js_1.NodeRevisionError){console.warn(`node value changed, running again. Error: ${err.message}`);return this.transactNode(path,callback,options)}else{throw err}}finally{lock.release()}}async matchNode(path,criteria,options){var _a;const tid=(_a=options===null||options===void 0?void 0:options.tid)!==null&&_a!==void 0?_a:acebase_core_1.ID.generate();const checkNode=async(path,criteria)=>{if(criteria.length===0){return Promise.resolve(true)}const criteriaKeys=criteria.reduce(((keys,cr)=>{let key=cr.key;if(typeof key==="string"&&key.includes("/")){key=key.slice(0,key.indexOf("/"))}if(keys.indexOf(key)<0){keys.push(key)}return keys}),[]);const unseenKeys=criteriaKeys.slice();let isMatch=true;const delayedMatchPromises=[];try{await this.getChildren(path,{tid:tid,keyFilter:criteriaKeys}).next((childInfo=>{var _a;const keyOrIndex=(_a=childInfo.key)!==null&&_a!==void 0?_a:childInfo.index;unseenKeys.includes(keyOrIndex)&&unseenKeys.splice(unseenKeys.indexOf(childInfo.key),1);const keyCriteria=criteria.filter((cr=>cr.key===keyOrIndex)).map((cr=>({op:cr.op,compare:cr.compare})));const keyResult=keyCriteria.length>0?checkChild(childInfo,keyCriteria):{isMatch:true,promises:[]};isMatch=keyResult.isMatch;if(isMatch){delayedMatchPromises.push(...keyResult.promises);const childCriteria=criteria.filter((cr=>typeof cr.key==="string"&&cr.key.startsWith(`${typeof keyOrIndex==="number"?`[${keyOrIndex}]`:keyOrIndex}/`))).map((cr=>{const key=cr.key.slice(cr.key.indexOf("/")+1);return{key:key,op:cr.op,compare:cr.compare}}));if(childCriteria.length>0){const childPath=acebase_core_1.PathInfo.getChildPath(path,childInfo.key);const childPromise=checkNode(childPath,childCriteria).then((isMatch=>({isMatch:isMatch})));delayedMatchPromises.push(childPromise)}}if(!isMatch||unseenKeys.length===0){return false}}));if(isMatch){const results=await Promise.all(delayedMatchPromises);isMatch=results.every((res=>res.isMatch))}if(!isMatch){return false}isMatch=unseenKeys.every((keyOrIndex=>{const childInfo=new node_info_js_1.NodeInfo(Object.assign(Object.assign(Object.assign({},typeof keyOrIndex==="number"&&{index:keyOrIndex}),typeof keyOrIndex==="string"&&{key:keyOrIndex}),{exists:false}));const childCriteria=criteria.filter((cr=>typeof cr.key==="string"&&cr.key.startsWith(`${typeof keyOrIndex==="number"?`[${keyOrIndex}]`:keyOrIndex}/`))).map((cr=>({op:cr.op,compare:cr.compare})));if(childCriteria.length>0&&!checkChild(childInfo,childCriteria).isMatch){return false}const keyCriteria=criteria.filter((cr=>cr.key===keyOrIndex)).map((cr=>({op:cr.op,compare:cr.compare})));if(keyCriteria.length===0){return true}const result=checkChild(childInfo,keyCriteria);return result.isMatch}));return isMatch}catch(err){this.logger.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((f.op==="contains"||f.op==="!contains")&&f.compare instanceof Array&&f.compare.length===0){proceed=true}else if(!child.exists){proceed=false}else{if(child.address){if(child.valueType===node_value_types_js_1.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=>({key:child.key,isMatch:isMatch})));promises.push(p);proceed=true}else if(child.valueType===node_value_types_js_1.VALUE_TYPES.ARRAY&&["contains","!contains"].indexOf(f.op)>=0){const p=this.getNode(child.path,{tid:tid}).then((({value: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===node_value_types_js_1.VALUE_TYPES.STRING){const p=this.getNode(child.path,{tid:tid}).then((node=>({key:child.key,isMatch:this.test(node.value,f.op,f.compare)})));promises.push(p);proceed=true}else{proceed=false}}else if(child.type===node_value_types_js_1.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===node_value_types_js_1.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{let 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}async exportNode(path,writeFn,options={format:"json",type_safe:true}){if((options===null||options===void 0?void 0:options.format)!=="json"){throw new Error("Only json output is currently supported")}const write=typeof writeFn!=="function"?writeFn.write.bind(writeFn):writeFn;const stringifyValue=(type,val)=>{const escape=str=>str.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/\r/g,"\\r").replace(/\n/g,"\\n").replace(/\t/g,"\\t").replace(/[\u0000-\u001f]/g,(ch=>`\\u${ch.charCodeAt(0).toString(16).padStart(4,"0")}`));if(type===node_value_types_js_1.VALUE_TYPES.DATETIME){val=`"${val.toISOString()}"`;if(options.type_safe){val=`{".type":"date",".val":${val}}`}}else if(type===node_value_types_js_1.VALUE_TYPES.STRING){val=`"${escape(val)}"`}else if(type===node_value_types_js_1.VALUE_TYPES.ARRAY){val="[]"}else if(type===node_value_types_js_1.VALUE_TYPES.OBJECT){val="{}"}else if(type===node_value_types_js_1.VALUE_TYPES.BINARY){val=`"${escape(acebase_core_1.ascii85.encode(val))}"`;if(options.type_safe){val=`{".type":"binary",".val":${val}}`}}else if(type===node_value_types_js_1.VALUE_TYPES.REFERENCE){val=`"${val.path}"`;if(options.type_safe){val=`{".type":"reference",".val":${val}}`}}else if(type===node_value_types_js_1.VALUE_TYPES.BIGINT){val=`"${val}"`;if(options.type_safe){val=`{".type":"bigint",".val":${val}}`}}return val};let objStart="",objEnd="";const nodeInfo=await this.getNodeInfo(path);if(!nodeInfo.exists){return write("null")}else if(nodeInfo.type===node_value_types_js_1.VALUE_TYPES.OBJECT){objStart="{";objEnd="}"}else if(nodeInfo.type===node_value_types_js_1.VALUE_TYPES.ARRAY){objStart="[";objEnd="]"}else{const node=await this.getNode(path);const val=stringifyValue(nodeInfo.type,node.value);return write(val)}if(objStart){const p=write(objStart);if(p instanceof Promise){await p}}let output="",outputCount=0;const pending=[];await this.getChildren(path).next((childInfo=>{if(childInfo.address){pending.push(childInfo)}else{if(outputCount++>0){output+=","}if(typeof childInfo.key==="string"){output+=`"${childInfo.key}":`}output+=stringifyValue(childInfo.type,childInfo.value)}}));if(output){const p=write(output);if(p instanceof Promise){await p}}while(pending.length>0){const childInfo=pending.shift();let output=outputCount++>0?",":"";const key=typeof childInfo.index==="number"?childInfo.index:childInfo.key;if(typeof key==="string"){output+=`"${key}":`}if(output){const p=write(output);if(p instanceof Promise){await p}}await this.exportNode(acebase_core_1.PathInfo.getChildPath(path,key),write,options)}if(objEnd){const p=write(objEnd);if(p instanceof Promise){await p}}}async importNode(path,read,options={format:"json",method:"set"}){const chunkSize=256*1024;const maxQueueBytes=1024*1024;const state={data:"",index:0,offset:0,queue:[],queueStartByte:0,timesFlushed:0,get processedBytes(){return this.offset+this.index}};const readNextChunk=async(append=false)=>{let data=await read(chunkSize);if(data===null){if(state.data){throw new Error(`Unexpected EOF at index ${state.offset+state.data.length}`)}else{throw new Error("Unable to read data from stream")}}else if(typeof data==="object"){data=acebase_core_1.Utils.decodeString(data)}if(append){state.data+=data}else{state.offset+=state.data.length;state.data=data;state.index=0}};const readBytes=async length=>{let str="";if(state.index+length>=state.data.length){str=state.data.slice(state.index);length-=str.length;await readNextChunk()}str+=state.data.slice(state.index,state.index+length);state.index+=length;return str};const assertBytes=async length=>{if(state.index+length>state.data.length){await readNextChunk(true)}if(state.index+length>state.data.length){throw new Error("Not enough data available from stream")}};const consumeToken=async token=>{const str=await readBytes(token.length);if(str!==token){throw new Error(`Unexpected character "${str[0]}" at index ${state.offset+state.index}, expected "${token}"`)}};const consumeSpaces=async()=>{const spaces=[" ","\t","\r","\n"];while(true){if(state.index>=state.data.length){await readNextChunk()}if(spaces.includes(state.data[state.index])){state.index++}else{break}}};const peekBytes=async length=>{await assertBytes(length);const index=state.index;return state.data.slice(index,index+length)};const peekValueType=async()=>{await consumeSpaces();const ch=await peekBytes(1);switch(ch){case'"':return"string";case"{":return"object";case"[":return"array";case"n":return"null";case"u":return"undefined";case"t":case"f":return"boolean";default:{if(ch==="-"||ch>="0"&&ch<="9"){return"number"}throw new Error(`Unknown value at index ${state.offset+state.index}`)}}};const readString=async()=>{await consumeToken('"');let str="";let i=state.index;while(state.data[i]!=='"'||state.data[i-1]==="\\"){i++;if(i>=state.data.length){str+=state.data.slice(state.index);await readNextChunk();i=0}}str+=state.data.slice(state.index,i);state.index=i+1;return unescape(str)};const readBoolean=async()=>{if(state.data[state.index]==="t"){await consumeToken("true")}else if(state.data[state.index]==="f"){await consumeToken("false")}throw new Error(`Expected true or false at index ${state.offset+state.index}`)};const readNumber=async()=>{let str="";let i=state.index;const nrChars=["-","0","1","2","3","4","5","6","7","8","9",".","e","b","f","x","o","n"];while(nrChars.includes(state.data[i])){i++;if(i>=state.data.length){str+=state.data.slice(state.index);await readNextChunk();i=0}}str+=state.data.slice(state.index,i);state.index=i;const nr=str.endsWith("n")?BigInt(str.slice(0,-1)):str.includes(".")?parseFloat(str):parseInt(str);return nr};const readValue=async()=>{await consumeSpaces();const type=await peekValueType();const value=await(()=>{switch(type){case"string":return readString();case"object":return{};case"array":return[];case"number":return readNumber();case"null":return null;case"undefined":return undefined;case"boolean":return readBoolean()}})();return{type:type,value:value}};const unescape=str=>str.replace(/\\n/g,"\n").replace(/\\"/g,'"');const getTypeSafeValue=(path,obj)=>{const type=obj[".type"];let val=obj[".val"];switch(type){case"Date":case"date":{val=new Date(val);break}case"Buffer":case"binary":{val=unescape(val);if(val.startsWith("<~")){val=acebase_core_1.ascii85.decode(val)}else{throw new Error(`Import error: Unexpected encoding for value for value at path "/${path}"`)}break}case"PathReference":case"reference":{val=new acebase_core_1.PathReference(val);break}case"bigint":{val=BigInt(val);break}default:throw new Error(`Import error: Unsupported type "${type}" for value at path "/${path}"`)}return val};const context={acebase_import_id:acebase_core_1.ID.generate()};const childOptions={suppress_events:options.suppress_events,context:context};const enqueue=async(target,value)=>{state.queue.push({target:target,value:value});if(state.processedBytes>=state.queueStartByte+maxQueueBytes){const operations=state.queue.reduce(((updates,item)=>{if(item.target.path===path){updates.push(Object.assign({op:options.method==="set"&&state.timesFlushed===0?"set":"update"},item))}else{const parent=updates.find((other=>other.target.isParentOf(item.target)));if(parent){parent.value[item.target.key]=item.value}else{updates.push(Object.assign({op:options.method==="merge"?"update":"set"},item))}}return updates}),[]);state.queueStartByte=state.processedBytes;state.queue=[];state.timesFlushed++}if(target.path===path){}};const importObject=async target=>{await consumeToken("{");await consumeSpaces();const nextChar=await peekBytes(1);if(nextChar==="}"){state.index++;return this.setNode(target.path,{},childOptions)}let childCount=0;let obj={};let flushedBefore=false;const flushObject=async()=>{let p;if(!flushedBefore){flushedBefore=true;p=this.setNode(target.path,obj,childOptions)}else if(Object.keys(obj).length>0){p=this.updateNode(target.path,obj,childOptions)}obj={};if(p){await p}};const promises=[];while(true){await consumeSpaces();const property=await readString();await consumeSpaces();await consumeToken(":");await consumeSpaces();const{value:value,type:type}=await readValue();obj[property]=value;childCount++;if(["object","array"].includes(type)){promises.push(flushObject());if(type==="object"){await importObject(target.child(property))}else{await importArray(target.child(property))}}await consumeSpaces();const nextChar=await peekBytes(1);if(nextChar==="}"){state.index++;break}await consumeToken(",")}const isTypedValue=childCount===2&&".type"in obj&&".val"in obj;if(isTypedValue){const val=getTypeSafeValue(target.path,obj);return this.setNode(target.path,val,childOptions)}promises.push(flushObject());await Promise.all(promises)};const importArray=async target=>{await consumeToken("[");await consumeSpaces();const nextChar=await peekBytes(1);if(nextChar==="]"){state.index++;return this.setNode(target.path,[],childOptions)}let flushedBefore=false;let arr=[];let updates={};const flushArray=async()=>{let p;if(!flushedBefore){flushedBefore=true;p=this.setNode(target.path,arr,childOptions);arr=null}else if(Object.keys(updates).length>0){p=this.updateNode(target.path,updates,childOptions);updates={}}if(p){await p}};const pushChild=(value,index)=>{if(flushedBefore){updates[index]=value}else{arr.push(value)}};const promises=[];let index=0;while(true){await consumeSpaces();const{value:value,type:type}=await readValue();pushChild(value,index);if(["object","array"].includes(type)){promises.push(flushArray());if(type==="object"){await importObject(target.child(index))}else{await importArray(target.child(index))}}await consumeSpaces();const nextChar=await peekBytes(1);if(nextChar==="]"){state.index++;break}await consumeToken(",");index++}promises.push(flushArray());await Promise.all(promises)};const start=async()=>{const{value:value,type:type}=await readValue();if(["object","array"].includes(type)){const target=acebase_core_1.PathInfo.get(path);if(type==="object"){await importObject(target)}else{await importArray(target)}}else{await this.setNode(path,value,childOptions)}};return start()}setSchema(path,schema,warnOnly=false){if(typeof schema==="undefined"){throw new TypeError("schema argument must be given")}if(schema===null){const i=this._schemas.findIndex((s=>s.path===path));i>=0&&this._schemas.splice(i,1);return}const definition=new acebase_core_1.SchemaDefinition(schema,{warnOnly:warnOnly,warnCallback:message=>this.logger.warn(message)});const item=this._schemas.find((s=>s.path===path));if(item){item.schema=definition}else{this._schemas.push({path:path,schema:definition});this._schemas.sort(((a,b)=>{const ka=acebase_core_1.PathInfo.getPathKeys(a.path),kb=acebase_core_1.PathInfo.getPathKeys(b.path);if(ka.length===kb.length){return 0}return ka.lengthitem.path===path));return item?{path:path,schema:item.schema.source,text:item.schema.text}:null}getSchemas(){return this._schemas.map((item=>({path:item.path,schema:item.schema.source,text:item.schema.text})))}validateSchema(path,value,options={updates:false}){let result={ok:true};const pathInfo=acebase_core_1.PathInfo.get(path);this._schemas.filter((s=>pathInfo.isOnTrailOf(s.path))).every((s=>{if(pathInfo.isDescendantOf(s.path)){const ancestorPath=acebase_core_1.PathInfo.fillVariables(s.path,path);const trailKeys=pathInfo.keys.slice(acebase_core_1.PathInfo.getPathKeys(s.path).length);result=s.schema.check(ancestorPath,value,options.updates,trailKeys);return result.ok}const trailKeys=acebase_core_1.PathInfo.getPathKeys(s.path).slice(pathInfo.keys.length);if(options.updates===true&&trailKeys.length>0&&!(trailKeys[0]in value)){return result.ok}const partial=options.updates===true&&trailKeys.length===0;const check=(path,value,trailKeys)=>{if(trailKeys.length===0){return s.schema.check(path,value,partial)}else if(value===null){return{ok:true}}const key=trailKeys[0];if(typeof key==="string"&&(key==="*"||key[0]==="$")){if(value===null||typeof value!=="object"){return{ok:true}}let result;Object.keys(value).every((childKey=>{const childPath=acebase_core_1.PathInfo.getChildPath(path,childKey);const childValue=value[childKey];result=check(childPath,childValue,trailKeys.slice(1));return result.ok}));return result}else{const childPath=acebase_core_1.PathInfo.getChildPath(path,key);const childValue=value[key];return check(childPath,childValue,trailKeys.slice(1))}};result=check(path,value,trailKeys);return result.ok}));return result}}exports.Storage=Storage},{"../assert.js":31,"../data-index/index.js":34,"../ipc/index.js":35,"../node-errors.js":38,"../node-info.js":39,"../node-value-types.js":41,"../promise-fs/index.js":43,"./errors.js":55,"./indexes.js":57,"acebase-core":12}],62:[function(require,module,exports){},{}]},{},[33])(33)})); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 9a82223..cb4bdd3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,7 +37,6 @@ "eslint-plugin-jasmine": "^4.1.3", "jasmine": "^3.7.0", "terser": "^5.15.0", - "tsc-esm-fix": "^2.20.5", "typescript": "^5.0.4" } }, @@ -59,112 +58,6 @@ "rxjs": ">= 5.x <= 7.x" } }, - "node_modules/@babel/code-frame": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", - "integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -365,24 +258,12 @@ "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "dev": true }, - "node_modules/@types/minimist": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz", - "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==", - "dev": true - }, "node_modules/@types/node": { "version": "18.16.13", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.13.tgz", "integrity": "sha512-uZRomboV1vBL61EBXneL4j9/hEn+1Yqa4LQdpGrKmXFyJmVfWc9JV9+yb2AlnOnuaDnb2PDO3hC6/LKmzJxP1A==", "dev": true }, - "node_modules/@types/normalize-package-data": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", - "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", - "dev": true - }, "node_modules/@types/semver": { "version": "7.5.0", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", @@ -698,15 +579,6 @@ "node": ">=8" } }, - "node_modules/arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/asn1.js": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", @@ -1053,48 +925,6 @@ "node": ">=6" } }, - "node_modules/camelcase": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", - "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", - "dev": true, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/camelcase-keys": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-8.0.2.tgz", - "integrity": "sha512-qMKdlOfsjlezMqxkUGGMaWWs17i2HoL15tM+wtx8ld4nLrUwU58TFdvyGOz/piNP842KeO8yXvggVQSdQ828NA==", - "dev": true, - "dependencies": { - "camelcase": "^7.0.0", - "map-obj": "^4.3.0", - "quick-lru": "^6.1.1", - "type-fest": "^2.13.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/camelcase-keys/node_modules/type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1304,51 +1134,6 @@ } } }, - "node_modules/decamelize": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.0.tgz", - "integrity": "sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decamelize-keys": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-2.0.1.tgz", - "integrity": "sha512-nrNeSCtU2gV3Apcmn/EZ+aR20zKDuNDStV67jPiupokD3sOAFeMzslLMCFdKv1sPqzwoe5ZUhsSW9IAVgKSL/Q==", - "dev": true, - "dependencies": { - "decamelize": "^6.0.0", - "map-obj": "^4.3.0", - "quick-lru": "^6.1.1", - "type-fest": "^3.1.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decamelize-keys/node_modules/type-fest": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.10.0.tgz", - "integrity": "sha512-hmAPf1datm+gt3c2mvu0sJyhFy6lTkIGf0GzyaZWxRLnabQfPUqg6tF95RPg6sLxKI7nFLGdFxBcf2/7+GXI+A==", - "dev": true, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - }, - "peerDependencies": { - "typescript": ">=4.7.0" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1487,15 +1272,6 @@ "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", "dev": true }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -1850,20 +1626,6 @@ "is-callable": "^1.1.3" } }, - "node_modules/fs-extra": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", - "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -1976,27 +1738,12 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true - }, "node_modules/grapheme-splitter": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", "dev": true }, - "node_modules/hard-rejection": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", - "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -2106,27 +1853,6 @@ "minimalistic-crypto-utils": "^1.0.1" } }, - "node_modules/hosted-git-info": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-6.1.1.tgz", - "integrity": "sha512-r0EI+HBMcXadMrugk0GCQ+6BQV39PiWAZVfq7oIckeGiN7sjRGyQxPdft3nQekFTCQbYxLBH+/axZMeH8UX6+w==", - "dev": true, - "dependencies": { - "lru-cache": "^7.5.1" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/hosted-git-info/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "engines": { - "node": ">=12" - } - }, "node_modules/htmlescape": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/htmlescape/-/htmlescape-1.1.1.tgz", @@ -2196,18 +1922,6 @@ "node": ">=0.8.19" } }, - "node_modules/indent-string": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", - "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -2270,12 +1984,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true - }, "node_modules/is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", @@ -2360,15 +2068,6 @@ "node": ">=8" } }, - "node_modules/is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-typed-array": { "version": "1.1.10", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", @@ -2429,12 +2128,6 @@ "url": "https://opencollective.com/js-sdsl" } }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -2447,12 +2140,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -2465,30 +2152,6 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, "node_modules/jsonparse": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", @@ -2514,15 +2177,6 @@ "node": "*" } }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/labeled-stream-splicer": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/labeled-stream-splicer/-/labeled-stream-splicer-2.0.2.tgz", @@ -2546,12 +2200,6 @@ "node": ">= 0.8.0" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -2591,18 +2239,6 @@ "node": ">=10" } }, - "node_modules/map-obj": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", - "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -2614,47 +2250,6 @@ "safe-buffer": "^5.1.2" } }, - "node_modules/meow": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/meow/-/meow-12.0.1.tgz", - "integrity": "sha512-/QOqMALNoKQcJAOOdIXjNLtfcCdLXbMFyB1fOOPdm6RzfBTlsuodOCTBDjVbeUSmgDQb8UI2oONqYGtq1PKKKA==", - "dev": true, - "dependencies": { - "@types/minimist": "^1.2.2", - "camelcase-keys": "^8.0.2", - "decamelize": "^6.0.0", - "decamelize-keys": "^2.0.1", - "hard-rejection": "^2.1.0", - "minimist-options": "4.1.0", - "normalize-package-data": "^5.0.0", - "read-pkg-up": "^9.1.0", - "redent": "^4.0.0", - "trim-newlines": "^5.0.0", - "type-fest": "^3.9.0", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=16.10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/meow/node_modules/type-fest": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.10.0.tgz", - "integrity": "sha512-hmAPf1datm+gt3c2mvu0sJyhFy6lTkIGf0GzyaZWxRLnabQfPUqg6tF95RPg6sLxKI7nFLGdFxBcf2/7+GXI+A==", - "dev": true, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - }, - "peerDependencies": { - "typescript": ">=4.7.0" - } - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -2696,15 +2291,6 @@ "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", "dev": true }, - "node_modules/min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -2738,20 +2324,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/minimist-options": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", - "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", - "dev": true, - "dependencies": { - "arrify": "^1.0.1", - "is-plain-obj": "^1.1.0", - "kind-of": "^6.0.3" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -2805,21 +2377,6 @@ "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", "dev": true }, - "node_modules/normalize-package-data": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-5.0.0.tgz", - "integrity": "sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q==", - "dev": true, - "dependencies": { - "hosted-git-info": "^6.0.0", - "is-core-module": "^2.8.1", - "semver": "^7.3.5", - "validate-npm-package-license": "^3.0.4" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -2931,24 +2488,6 @@ "safe-buffer": "^5.1.1" } }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", @@ -3123,18 +2662,6 @@ } ] }, - "node_modules/quick-lru": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-6.1.1.tgz", - "integrity": "sha512-S27GBT+F0NTRiehtbrgaSE1idUAJ5bX8dPAQTdylEyNlrdcH5X4Lz7Edz3DYzecbsCluD5zO8ZNEe04z3D3u6Q==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -3163,174 +2690,6 @@ "readable-stream": "^2.0.2" } }, - "node_modules/read-pkg": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-7.1.0.tgz", - "integrity": "sha512-5iOehe+WF75IccPc30bWTbpdDQLOCc3Uu8bi3Dte3Eueij81yx1Mrufk8qBx/YAbR4uL1FdUr+7BKXDwEtisXg==", - "dev": true, - "dependencies": { - "@types/normalize-package-data": "^2.4.1", - "normalize-package-data": "^3.0.2", - "parse-json": "^5.2.0", - "type-fest": "^2.0.0" - }, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-9.1.0.tgz", - "integrity": "sha512-vaMRR1AC1nrd5CQM0PhlRsO5oc2AAigqr7cCrZ/MW/Rsaflz4RlgzkpL4qoU/z1F6wrbd85iFv1OQj/y5RdGvg==", - "dev": true, - "dependencies": { - "find-up": "^6.3.0", - "read-pkg": "^7.1.0", - "type-fest": "^2.5.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up/node_modules/find-up": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", - "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", - "dev": true, - "dependencies": { - "locate-path": "^7.1.0", - "path-exists": "^5.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up/node_modules/locate-path": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", - "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", - "dev": true, - "dependencies": { - "p-locate": "^6.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up/node_modules/p-limit": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", - "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up/node_modules/p-locate": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", - "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", - "dev": true, - "dependencies": { - "p-limit": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up/node_modules/path-exists": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", - "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/read-pkg-up/node_modules/type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up/node_modules/yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg/node_modules/hosted-git-info": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", - "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/read-pkg/node_modules/normalize-package-data": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", - "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", - "dev": true, - "dependencies": { - "hosted-git-info": "^4.0.1", - "is-core-module": "^2.5.0", - "semver": "^7.3.4", - "validate-npm-package-license": "^3.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/read-pkg/node_modules/type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -3361,22 +2720,6 @@ "safe-buffer": "~5.1.0" } }, - "node_modules/redent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-4.0.0.tgz", - "integrity": "sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==", - "dev": true, - "dependencies": { - "indent-string": "^5.0.0", - "strip-indent": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/resolve": { "version": "1.22.2", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", @@ -3611,38 +2954,6 @@ "node": ">=0.10.0" } }, - "node_modules/spdx-correct": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", - "dev": true, - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", - "dev": true - }, - "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-license-ids": { - "version": "3.0.13", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", - "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==", - "dev": true - }, "node_modules/stream-browserify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", @@ -3734,21 +3045,6 @@ "node": ">=8" } }, - "node_modules/strip-indent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.0.0.tgz", - "integrity": "sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==", - "dev": true, - "dependencies": { - "min-indent": "^1.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -3865,75 +3161,7 @@ }, "engines": { "node": ">=8.0" - } - }, - "node_modules/trim-newlines": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-5.0.0.tgz", - "integrity": "sha512-kstfs+hgwmdsOadN3KgA+C68wPJwnZq4DN6WMDCvZapDWEF34W2TyPKN2v2+BJnZgIz5QOfxFeldLyYvdgRAwg==", - "dev": true, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tsc-esm-fix": { - "version": "2.20.14", - "resolved": "https://registry.npmjs.org/tsc-esm-fix/-/tsc-esm-fix-2.20.14.tgz", - "integrity": "sha512-s4hA4FD5ux4OvUDywLGl7jMBuK0kEZTqmLPNVy82OR/q18N5nN8C8NKPyAdw/XKtrm8vG+aqcNkJMXa1wPk8qQ==", - "dev": true, - "dependencies": { - "fs-extra": "^11.1.1", - "globby": "^13.1.4", - "json5": "^2.2.3", - "meow": "^12.0.1", - "tslib": "^2.5.0" - }, - "bin": { - "tsc-esm-fix": "target/es6/cli.mjs" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/tsc-esm-fix/node_modules/globby": { - "version": "13.1.4", - "resolved": "https://registry.npmjs.org/globby/-/globby-13.1.4.tgz", - "integrity": "sha512-iui/IiiW+QrJ1X1hKH5qwlMQyv34wJAYwH1vrf8b9kBA4sNiif3gKsMHa+BrdnOpEudWjpotfa7LrTzB1ERS/g==", - "dev": true, - "dependencies": { - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.11", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tsc-esm-fix/node_modules/slash": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", - "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tslib": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.1.tgz", - "integrity": "sha512-KaI6gPil5m9vF7DKaoXxx1ia9fxS4qG5YveErRRVknPDXXriu5M8h48YRjB6h5ZUOKuAKlSJYb0GaDe8I39fRw==", - "dev": true + } }, "node_modules/tsutils": { "version": "3.21.0", @@ -4038,15 +3266,6 @@ "node": ">= 0.4.12" } }, - "node_modules/universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "dev": true, - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -4100,16 +3319,6 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, "node_modules/vm-browserify": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", @@ -4181,15 +3390,6 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "engines": { - "node": ">=12" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -4204,90 +3404,6 @@ } }, "dependencies": { - "@babel/code-frame": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", - "integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==", - "dev": true, - "requires": { - "@babel/highlight": "^7.18.6" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", - "dev": true - }, - "@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, "@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -4444,24 +3560,12 @@ "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "dev": true }, - "@types/minimist": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz", - "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==", - "dev": true - }, "@types/node": { "version": "18.16.13", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.13.tgz", "integrity": "sha512-uZRomboV1vBL61EBXneL4j9/hEn+1Yqa4LQdpGrKmXFyJmVfWc9JV9+yb2AlnOnuaDnb2PDO3hC6/LKmzJxP1A==", "dev": true }, - "@types/normalize-package-data": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", - "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", - "dev": true - }, "@types/semver": { "version": "7.5.0", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", @@ -4668,12 +3772,6 @@ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true }, - "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", - "dev": true - }, "asn1.js": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", @@ -4985,32 +4083,6 @@ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true }, - "camelcase": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", - "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", - "dev": true - }, - "camelcase-keys": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-8.0.2.tgz", - "integrity": "sha512-qMKdlOfsjlezMqxkUGGMaWWs17i2HoL15tM+wtx8ld4nLrUwU58TFdvyGOz/piNP842KeO8yXvggVQSdQ828NA==", - "dev": true, - "requires": { - "camelcase": "^7.0.0", - "map-obj": "^4.3.0", - "quick-lru": "^6.1.1", - "type-fest": "^2.13.0" - }, - "dependencies": { - "type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", - "dev": true - } - } - }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -5196,33 +4268,6 @@ "ms": "2.1.2" } }, - "decamelize": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.0.tgz", - "integrity": "sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA==", - "dev": true - }, - "decamelize-keys": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-2.0.1.tgz", - "integrity": "sha512-nrNeSCtU2gV3Apcmn/EZ+aR20zKDuNDStV67jPiupokD3sOAFeMzslLMCFdKv1sPqzwoe5ZUhsSW9IAVgKSL/Q==", - "dev": true, - "requires": { - "decamelize": "^6.0.0", - "map-obj": "^4.3.0", - "quick-lru": "^6.1.1", - "type-fest": "^3.1.0" - }, - "dependencies": { - "type-fest": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.10.0.tgz", - "integrity": "sha512-hmAPf1datm+gt3c2mvu0sJyhFy6lTkIGf0GzyaZWxRLnabQfPUqg6tF95RPg6sLxKI7nFLGdFxBcf2/7+GXI+A==", - "dev": true, - "requires": {} - } - } - }, "deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -5343,15 +4388,6 @@ } } }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "requires": { - "is-arrayish": "^0.2.1" - } - }, "escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -5629,17 +4665,6 @@ "is-callable": "^1.1.3" } }, - "fs-extra": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", - "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - } - }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -5725,24 +4750,12 @@ "get-intrinsic": "^1.1.3" } }, - "graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true - }, "grapheme-splitter": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", "dev": true }, - "hard-rejection": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", - "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", - "dev": true - }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -5824,23 +4837,6 @@ "minimalistic-crypto-utils": "^1.0.1" } }, - "hosted-git-info": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-6.1.1.tgz", - "integrity": "sha512-r0EI+HBMcXadMrugk0GCQ+6BQV39PiWAZVfq7oIckeGiN7sjRGyQxPdft3nQekFTCQbYxLBH+/axZMeH8UX6+w==", - "dev": true, - "requires": { - "lru-cache": "^7.5.1" - }, - "dependencies": { - "lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true - } - } - }, "htmlescape": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/htmlescape/-/htmlescape-1.1.1.tgz", @@ -5881,12 +4877,6 @@ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true }, - "indent-string": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", - "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", - "dev": true - }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -5940,12 +4930,6 @@ "has-tostringtag": "^1.0.0" } }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true - }, "is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", @@ -6003,12 +4987,6 @@ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true }, - "is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", - "dev": true - }, "is-typed-array": { "version": "1.1.10", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", @@ -6056,12 +5034,6 @@ "integrity": "sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==", "dev": true }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, "js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -6071,12 +5043,6 @@ "argparse": "^2.0.1" } }, - "json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -6089,22 +5055,6 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, - "json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true - }, - "jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6", - "universalify": "^2.0.0" - } - }, "jsonparse": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", @@ -6121,12 +5071,6 @@ "through": ">=2.2.7 <3" } }, - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - }, "labeled-stream-splicer": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/labeled-stream-splicer/-/labeled-stream-splicer-2.0.2.tgz", @@ -6147,12 +5091,6 @@ "type-check": "~0.4.0" } }, - "lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true - }, "locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -6183,12 +5121,6 @@ "yallist": "^4.0.0" } }, - "map-obj": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", - "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", - "dev": true - }, "md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -6200,35 +5132,6 @@ "safe-buffer": "^5.1.2" } }, - "meow": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/meow/-/meow-12.0.1.tgz", - "integrity": "sha512-/QOqMALNoKQcJAOOdIXjNLtfcCdLXbMFyB1fOOPdm6RzfBTlsuodOCTBDjVbeUSmgDQb8UI2oONqYGtq1PKKKA==", - "dev": true, - "requires": { - "@types/minimist": "^1.2.2", - "camelcase-keys": "^8.0.2", - "decamelize": "^6.0.0", - "decamelize-keys": "^2.0.1", - "hard-rejection": "^2.1.0", - "minimist-options": "4.1.0", - "normalize-package-data": "^5.0.0", - "read-pkg-up": "^9.1.0", - "redent": "^4.0.0", - "trim-newlines": "^5.0.0", - "type-fest": "^3.9.0", - "yargs-parser": "^21.1.1" - }, - "dependencies": { - "type-fest": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.10.0.tgz", - "integrity": "sha512-hmAPf1datm+gt3c2mvu0sJyhFy6lTkIGf0GzyaZWxRLnabQfPUqg6tF95RPg6sLxKI7nFLGdFxBcf2/7+GXI+A==", - "dev": true, - "requires": {} - } - } - }, "merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -6263,12 +5166,6 @@ } } }, - "min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "dev": true - }, "minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -6296,17 +5193,6 @@ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true }, - "minimist-options": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", - "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", - "dev": true, - "requires": { - "arrify": "^1.0.1", - "is-plain-obj": "^1.1.0", - "kind-of": "^6.0.3" - } - }, "mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -6354,18 +5240,6 @@ "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", "dev": true }, - "normalize-package-data": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-5.0.0.tgz", - "integrity": "sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q==", - "dev": true, - "requires": { - "hosted-git-info": "^6.0.0", - "is-core-module": "^2.8.1", - "semver": "^7.3.5", - "validate-npm-package-license": "^3.0.4" - } - }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -6456,18 +5330,6 @@ "safe-buffer": "^5.1.1" } }, - "parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - } - }, "path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", @@ -6593,12 +5455,6 @@ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true }, - "quick-lru": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-6.1.1.tgz", - "integrity": "sha512-S27GBT+F0NTRiehtbrgaSE1idUAJ5bX8dPAQTdylEyNlrdcH5X4Lz7Edz3DYzecbsCluD5zO8ZNEe04z3D3u6Q==", - "dev": true - }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -6627,115 +5483,6 @@ "readable-stream": "^2.0.2" } }, - "read-pkg": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-7.1.0.tgz", - "integrity": "sha512-5iOehe+WF75IccPc30bWTbpdDQLOCc3Uu8bi3Dte3Eueij81yx1Mrufk8qBx/YAbR4uL1FdUr+7BKXDwEtisXg==", - "dev": true, - "requires": { - "@types/normalize-package-data": "^2.4.1", - "normalize-package-data": "^3.0.2", - "parse-json": "^5.2.0", - "type-fest": "^2.0.0" - }, - "dependencies": { - "hosted-git-info": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", - "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "normalize-package-data": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", - "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", - "dev": true, - "requires": { - "hosted-git-info": "^4.0.1", - "is-core-module": "^2.5.0", - "semver": "^7.3.4", - "validate-npm-package-license": "^3.0.1" - } - }, - "type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", - "dev": true - } - } - }, - "read-pkg-up": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-9.1.0.tgz", - "integrity": "sha512-vaMRR1AC1nrd5CQM0PhlRsO5oc2AAigqr7cCrZ/MW/Rsaflz4RlgzkpL4qoU/z1F6wrbd85iFv1OQj/y5RdGvg==", - "dev": true, - "requires": { - "find-up": "^6.3.0", - "read-pkg": "^7.1.0", - "type-fest": "^2.5.0" - }, - "dependencies": { - "find-up": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", - "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", - "dev": true, - "requires": { - "locate-path": "^7.1.0", - "path-exists": "^5.0.0" - } - }, - "locate-path": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", - "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", - "dev": true, - "requires": { - "p-locate": "^6.0.0" - } - }, - "p-limit": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", - "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", - "dev": true, - "requires": { - "yocto-queue": "^1.0.0" - } - }, - "p-locate": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", - "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", - "dev": true, - "requires": { - "p-limit": "^4.0.0" - } - }, - "path-exists": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", - "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", - "dev": true - }, - "type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", - "dev": true - }, - "yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", - "dev": true - } - } - }, "readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -6768,16 +5515,6 @@ } } }, - "redent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-4.0.0.tgz", - "integrity": "sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==", - "dev": true, - "requires": { - "indent-string": "^5.0.0", - "strip-indent": "^4.0.0" - } - }, "resolve": { "version": "1.22.2", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", @@ -6926,38 +5663,6 @@ } } }, - "spdx-correct": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", - "dev": true, - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", - "dev": true - }, - "spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.13", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", - "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==", - "dev": true - }, "stream-browserify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", @@ -7044,15 +5749,6 @@ "ansi-regex": "^5.0.1" } }, - "strip-indent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.0.0.tgz", - "integrity": "sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==", - "dev": true, - "requires": { - "min-indent": "^1.0.1" - } - }, "strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -7144,52 +5840,6 @@ "is-number": "^7.0.0" } }, - "trim-newlines": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-5.0.0.tgz", - "integrity": "sha512-kstfs+hgwmdsOadN3KgA+C68wPJwnZq4DN6WMDCvZapDWEF34W2TyPKN2v2+BJnZgIz5QOfxFeldLyYvdgRAwg==", - "dev": true - }, - "tsc-esm-fix": { - "version": "2.20.14", - "resolved": "https://registry.npmjs.org/tsc-esm-fix/-/tsc-esm-fix-2.20.14.tgz", - "integrity": "sha512-s4hA4FD5ux4OvUDywLGl7jMBuK0kEZTqmLPNVy82OR/q18N5nN8C8NKPyAdw/XKtrm8vG+aqcNkJMXa1wPk8qQ==", - "dev": true, - "requires": { - "fs-extra": "^11.1.1", - "globby": "^13.1.4", - "json5": "^2.2.3", - "meow": "^12.0.1", - "tslib": "^2.5.0" - }, - "dependencies": { - "globby": { - "version": "13.1.4", - "resolved": "https://registry.npmjs.org/globby/-/globby-13.1.4.tgz", - "integrity": "sha512-iui/IiiW+QrJ1X1hKH5qwlMQyv34wJAYwH1vrf8b9kBA4sNiif3gKsMHa+BrdnOpEudWjpotfa7LrTzB1ERS/g==", - "dev": true, - "requires": { - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.11", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^4.0.0" - } - }, - "slash": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", - "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", - "dev": true - } - } - }, - "tslib": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.1.tgz", - "integrity": "sha512-KaI6gPil5m9vF7DKaoXxx1ia9fxS4qG5YveErRRVknPDXXriu5M8h48YRjB6h5ZUOKuAKlSJYb0GaDe8I39fRw==", - "dev": true - }, "tsutils": { "version": "3.21.0", "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", @@ -7264,12 +5914,6 @@ "resolved": "https://registry.npmjs.org/unidecode/-/unidecode-0.1.8.tgz", "integrity": "sha512-SdoZNxCWpN2tXTCrGkPF/0rL2HEq+i2gwRG1ReBvx8/0yTzC3enHfugOf8A9JBShVwwrRIkLX0YcDUGbzjbVCA==" }, - "universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "dev": true - }, "uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -7324,16 +5968,6 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, - "validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, "vm-browserify": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", @@ -7387,12 +6021,6 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, - "yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true - }, "yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index f6b765a..03690ce 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "lint:fix": "eslint . --fix", "build": "npm run build:clean && npm run build:esm && npm run build:cjs && npm run build:packages && npm run browserify && echo Done!", "build:clean": "(rm -r ./dist/esm || true) && (rm -r ./dist/cjs || true) && (rm -r ./dist/types || true)", - "build:esm": "tsc -p tsconfig.json && npx tsc-esm-fix ---target='dist/esm'", + "build:esm": "tsc -p tsconfig.json && node scripts/esm-fix-dirname.mjs", "build:cjs": "tsc -p tsconfig-cjs.json", "build:packages": "bash ./create-package-files", "browserify": "browserify ./dist/cjs/browser.js -o ./dist/browser.js --standalone acebase --ignore buffer --ignore rxjs && terser ./dist/browser.js -o ./dist/browser.min.js", @@ -106,7 +106,6 @@ "eslint-plugin-jasmine": "^4.1.3", "jasmine": "^3.7.0", "terser": "^5.15.0", - "tsc-esm-fix": "^2.20.5", "typescript": "^5.0.4" }, "funding": [ diff --git a/scripts/esm-fix-dirname.mjs b/scripts/esm-fix-dirname.mjs new file mode 100644 index 0000000..bb1fc63 --- /dev/null +++ b/scripts/esm-fix-dirname.mjs @@ -0,0 +1,25 @@ +// Injects ESM-compatible __dirname shim into compiled ESM output files. +// TypeScript's @types/node declares __dirname globally so it type-checks fine, +// but Node.js does not provide it in ES modules at runtime. +import { readdir, readFile, writeFile } from 'fs/promises'; +import { join } from 'path'; + +// Uses the global URL constructor (no import needed) to derive the directory. +// new URL('.', import.meta.url) => file:///path/to/dir/ (trailing slash) +const SHIM = 'const __dirname = new URL(\'.\', import.meta.url).pathname.slice(0, -1);\n'; + +async function fix(dir) { + for (const entry of await readdir(dir, { withFileTypes: true })) { + const fullPath = join(dir, entry.name); + if (entry.isDirectory()) { + await fix(fullPath); + } else if (entry.name.endsWith('.js')) { + const src = await readFile(fullPath, 'utf8'); + if (src.includes('__dirname')) { + await writeFile(fullPath, SHIM + src); + } + } + } +} + +fix('./dist/esm').catch(err => { console.error(err); process.exit(1); }); diff --git a/src/acebase-browser.ts b/src/acebase-browser.ts index 5bf2b9a..fbf08f7 100644 --- a/src/acebase-browser.ts +++ b/src/acebase-browser.ts @@ -1,6 +1,6 @@ -import { AceBase, AceBaseLocalSettings } from './acebase-local'; -import { createIndexedDBInstance } from './storage/custom/indexed-db'; -import { IndexedDBStorageSettings } from './storage/custom/indexed-db/settings'; +import { AceBase, AceBaseLocalSettings } from './acebase-local.js'; +import { createIndexedDBInstance } from './storage/custom/indexed-db/index.js'; +import { IndexedDBStorageSettings } from './storage/custom/indexed-db/settings.js'; const deprecatedConstructorError = `Using AceBase constructor in the browser to use localStorage is deprecated! Switch to: diff --git a/src/acebase-local.ts b/src/acebase-local.ts index c79708c..014dd12 100644 --- a/src/acebase-local.ts +++ b/src/acebase-local.ts @@ -1,9 +1,9 @@ import { AceBaseBase, AceBaseBaseSettings } from 'acebase-core'; -import { AceBaseStorage } from './storage/binary'; -import { LocalApi } from './api-local'; -import { IPCClientSettings, StorageSettings, TransactionLogSettings } from './storage'; -import { createLocalStorageInstance, LocalStorageSettings } from './storage/custom/local-storage'; -import { IndexedDBStorageSettings } from './storage/custom/indexed-db/settings'; +import { AceBaseStorage } from './storage/binary/index.js'; +import { LocalApi } from './api-local.js'; +import { IPCClientSettings, StorageSettings, TransactionLogSettings } from './storage/index.js'; +import { createLocalStorageInstance, LocalStorageSettings } from './storage/custom/local-storage/index.js'; +import { IndexedDBStorageSettings } from './storage/custom/indexed-db/settings.js'; export { LocalStorageSettings, IndexedDBStorageSettings }; diff --git a/src/api-local.ts b/src/api-local.ts index d5ff71c..37c967c 100644 --- a/src/api-local.ts +++ b/src/api-local.ts @@ -2,17 +2,17 @@ import { AceBaseBase, IStreamLike, Api, EventSubscriptionCallback, ReflectionType, IReflectionNodeInfo, IReflectionChildrenInfo, StreamReadFunction, StreamWriteFunction, TransactionLogFilter, LoggingLevel, Query, QueryOptions, LoggerPlugin } from 'acebase-core'; -import { AceBaseStorage, AceBaseStorageSettings } from './storage/binary'; -import { SQLiteStorage, SQLiteStorageSettings } from './storage/sqlite'; -import { MSSQLStorage, MSSQLStorageSettings } from './storage/mssql'; -import { CustomStorage, CustomStorageSettings } from './storage/custom'; -import { VALUE_TYPES } from './node-value-types'; -import { executeQuery } from './query'; -import { Storage, StorageEnv } from './storage'; -import { CreateIndexOptions } from './storage/indexes'; -import type { BinaryNodeAddress } from './storage/binary/node-address'; -import { AceBaseLocalSettings } from '.'; -import { NodeNotFoundError } from './node-errors'; +import { AceBaseStorage, AceBaseStorageSettings } from './storage/binary/index.js'; +import { SQLiteStorage, SQLiteStorageSettings } from './storage/sqlite/index.js'; +import { MSSQLStorage, MSSQLStorageSettings } from './storage/mssql/index.js'; +import { CustomStorage, CustomStorageSettings } from './storage/custom/index.js'; +import { VALUE_TYPES } from './node-value-types.js'; +import { executeQuery } from './query.js'; +import { Storage, StorageEnv } from './storage/index.js'; +import { CreateIndexOptions } from './storage/indexes.js'; +import type { BinaryNodeAddress } from './storage/binary/node-address.js'; +import { AceBaseLocalSettings } from './acebase-local.js'; +import { NodeNotFoundError } from './node-errors.js'; export class LocalApi extends Api { // All api methods for local database instance diff --git a/src/async-task-batch.spec.ts b/src/async-task-batch.spec.ts index 131d73c..830a999 100644 --- a/src/async-task-batch.spec.ts +++ b/src/async-task-batch.spec.ts @@ -1,4 +1,4 @@ -import { AsyncTaskBatch } from './async-task-batch'; +import { AsyncTaskBatch } from './async-task-batch.js'; describe('Async task batches', () => { it('works', async () => { diff --git a/src/binary.spec.ts b/src/binary.spec.ts index 38088de..4e2dcf0 100644 --- a/src/binary.spec.ts +++ b/src/binary.spec.ts @@ -1,4 +1,4 @@ -import { Uint8ArrayBuilder } from './binary'; +import { Uint8ArrayBuilder } from './binary.js'; describe('Uint8ArrayBuilder', () => { it('write grows the buffer', () => { diff --git a/src/browser.ts b/src/browser.ts index 31ca6dd..d2bbaea 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -21,9 +21,9 @@ import { DataReference, DataSnapshot, EventSubscription, PathReference, TypeMapp DataSnapshotsArray, ObjectCollection, DataReferencesArray, EventStream, TypeMappingOptions, IReflectionNodeInfo, IReflectionChildrenInfo, IStreamLike, ILiveDataProxy, ILiveDataProxyValue, IObjectCollection, PartialArray } from 'acebase-core'; -import { AceBaseLocalSettings } from './acebase-local'; -import { BrowserAceBase } from './acebase-browser'; -import { CustomStorageSettings, CustomStorageTransaction, CustomStorageHelpers } from './storage/custom'; +import { AceBaseLocalSettings } from './acebase-local.js'; +import { BrowserAceBase } from './acebase-browser.js'; +import { CustomStorageSettings, CustomStorageTransaction, CustomStorageHelpers } from './storage/custom/index.js'; const acebase = { AceBase: BrowserAceBase, @@ -83,11 +83,11 @@ export { AceBaseLocalSettings, LocalStorageSettings, IndexedDBStorageSettings, -} from './acebase-local'; +} from './acebase-local.js'; -export { AceBaseStorageSettings } from './storage/binary'; -export { SQLiteStorageSettings } from './storage/sqlite'; -export { MSSQLStorageSettings } from './storage/mssql'; +export { AceBaseStorageSettings } from './storage/binary/index.js'; +export { SQLiteStorageSettings } from './storage/sqlite/index.js'; +export { MSSQLStorageSettings } from './storage/mssql/index.js'; export { CustomStorageTransaction, @@ -95,11 +95,11 @@ export { CustomStorageHelpers, ICustomStorageNode, ICustomStorageNodeMetaData, -} from './storage/custom'; +} from './storage/custom/index.js'; export { StorageSettings, TransactionLogSettings, IPCClientSettings, SchemaValidationError, -} from './storage'; +} from './storage/index.js'; diff --git a/src/btree/binary-pointer.ts b/src/btree/binary-pointer.ts index 89e0f31..381a088 100644 --- a/src/btree/binary-pointer.ts +++ b/src/btree/binary-pointer.ts @@ -1,4 +1,4 @@ -import { BPlusTreeLeaf } from './tree-leaf'; +import { BPlusTreeLeaf } from './tree-leaf.js'; export type BinaryPointer = { name: string; diff --git a/src/btree/binary-reader.ts b/src/btree/binary-reader.ts index c809292..346f91c 100644 --- a/src/btree/binary-reader.ts +++ b/src/btree/binary-reader.ts @@ -1,9 +1,9 @@ import { Utils } from 'acebase-core'; -import { readByteLength, readSignedNumber } from '../binary'; -import { DetailedError } from '../detailed-error'; -import { pfs } from '../promise-fs'; -import { assert } from '../assert'; -import { BPlusTree } from './tree'; +import { readByteLength, readSignedNumber } from '../binary.js'; +import { DetailedError } from '../detailed-error.js'; +import { pfs } from '../promise-fs/index.js'; +import { assert } from '../assert.js'; +import { BPlusTree } from './tree.js'; const { bytesToNumber } = Utils; export type ReadFunction = (index: number, length: number) => Promise; diff --git a/src/btree/binary-reference.ts b/src/btree/binary-reference.ts index acc4e4a..c27e967 100644 --- a/src/btree/binary-reference.ts +++ b/src/btree/binary-reference.ts @@ -1,5 +1,5 @@ -import { BPlusTreeLeaf } from './tree-leaf'; -import { BPlusTreeNode } from './tree-node'; +import { BPlusTreeLeaf } from './tree-leaf.js'; +import { BPlusTreeNode } from './tree-node.js'; export type BinaryReference = { name: string; diff --git a/src/btree/binary-tree-builder.ts b/src/btree/binary-tree-builder.ts index 8fde3f6..5f44743 100644 --- a/src/btree/binary-tree-builder.ts +++ b/src/btree/binary-tree-builder.ts @@ -1,14 +1,14 @@ -import { Uint8ArrayBuilder, writeByteLength, writeSignedOffset } from '../binary'; -import { DetailedError } from '../detailed-error'; -import { MAX_SMALL_LEAF_VALUE_LENGTH, WRITE_SMALL_LEAFS } from './config'; -import { BPlusTree } from './tree'; -import { BPlusTreeLeafEntryValue } from './tree-leaf-entry-value'; -import { BinaryBPlusTreeLeafEntry } from './binary-tree-leaf-entry'; +import { Uint8ArrayBuilder, writeByteLength, writeSignedOffset } from '../binary.js'; +import { DetailedError } from '../detailed-error.js'; +import { MAX_SMALL_LEAF_VALUE_LENGTH, WRITE_SMALL_LEAFS } from './config.js'; +import { BPlusTree } from './tree.js'; +import { BPlusTreeLeafEntryValue } from './tree-leaf-entry-value.js'; +import { BinaryBPlusTreeLeafEntry } from './binary-tree-leaf-entry.js'; import { Utils } from 'acebase-core'; -import { LeafEntryRecordPointer } from './leaf-entry-recordpointer'; -import { LeafEntryMetaData } from './leaf-entry-metadata'; -import { NodeEntryKeyType } from './entry-key-type'; -import { assert } from '../assert'; +import { LeafEntryRecordPointer } from './leaf-entry-recordpointer.js'; +import { LeafEntryMetaData } from './leaf-entry-metadata.js'; +import { NodeEntryKeyType } from './entry-key-type.js'; +import { assert } from '../assert.js'; const { bigintToBytes, encodeString, numberToBytes } = Utils; diff --git a/src/btree/binary-tree-leaf-entry-extdata.ts b/src/btree/binary-tree-leaf-entry-extdata.ts index 1e05cb0..dfe2091 100644 --- a/src/btree/binary-tree-leaf-entry-extdata.ts +++ b/src/btree/binary-tree-leaf-entry-extdata.ts @@ -1,7 +1,7 @@ -import { ThreadSafeLock } from '../thread-safe'; -import { BinaryBPlusTreeLeafEntryValue } from './binary-tree-leaf-entry-value'; -import { LeafEntryMetaData } from './leaf-entry-metadata'; -import { LeafEntryRecordPointer } from './leaf-entry-recordpointer'; +import { ThreadSafeLock } from '../thread-safe.js'; +import { BinaryBPlusTreeLeafEntryValue } from './binary-tree-leaf-entry-value.js'; +import { LeafEntryMetaData } from './leaf-entry-metadata.js'; +import { LeafEntryRecordPointer } from './leaf-entry-recordpointer.js'; export interface IBinaryBPlusTreeLeafEntryExtData { length: number; diff --git a/src/btree/binary-tree-leaf-entry-value.ts b/src/btree/binary-tree-leaf-entry-value.ts index 2373c66..7567567 100644 --- a/src/btree/binary-tree-leaf-entry-value.ts +++ b/src/btree/binary-tree-leaf-entry-value.ts @@ -1,5 +1,5 @@ -import { LeafEntryMetaData } from './leaf-entry-metadata'; -import { LeafEntryRecordPointer } from './leaf-entry-recordpointer'; +import { LeafEntryMetaData } from './leaf-entry-metadata.js'; +import { LeafEntryRecordPointer } from './leaf-entry-recordpointer.js'; export class BinaryBPlusTreeLeafEntryValue { /** diff --git a/src/btree/binary-tree-leaf-entry.ts b/src/btree/binary-tree-leaf-entry.ts index dd8719e..ec354f6 100644 --- a/src/btree/binary-tree-leaf-entry.ts +++ b/src/btree/binary-tree-leaf-entry.ts @@ -1,6 +1,6 @@ -import { IBinaryBPlusTreeLeafEntryExtData } from './binary-tree-leaf-entry-extdata'; -import { BinaryBPlusTreeLeafEntryValue } from './binary-tree-leaf-entry-value'; -import { NodeEntryKeyType } from './entry-key-type'; +import { IBinaryBPlusTreeLeafEntryExtData } from './binary-tree-leaf-entry-extdata.js'; +import { BinaryBPlusTreeLeafEntryValue } from './binary-tree-leaf-entry-value.js'; +import { NodeEntryKeyType } from './entry-key-type.js'; export class BinaryBPlusTreeLeafEntry { diff --git a/src/btree/binary-tree-leaf.ts b/src/btree/binary-tree-leaf.ts index a4fefdc..63ab6d7 100644 --- a/src/btree/binary-tree-leaf.ts +++ b/src/btree/binary-tree-leaf.ts @@ -1,9 +1,9 @@ -import { assert } from '../assert'; -import { DetailedError } from '../detailed-error'; -import { BinaryBPlusTreeLeafEntry } from './binary-tree-leaf-entry'; -import { BinaryBPlusTreeNodeInfo } from './binary-tree-node-info'; -import { NodeEntryKeyType } from './entry-key-type'; -import { _isEqual } from './typesafe-compare'; +import { assert } from '../assert.js'; +import { DetailedError } from '../detailed-error.js'; +import { BinaryBPlusTreeLeafEntry } from './binary-tree-leaf-entry.js'; +import { BinaryBPlusTreeNodeInfo } from './binary-tree-node-info.js'; +import { NodeEntryKeyType } from './entry-key-type.js'; +import { _isEqual } from './typesafe-compare.js'; export class BinaryBPlusTreeLeaf extends BinaryBPlusTreeNodeInfo { diff --git a/src/btree/binary-tree-node-entry.ts b/src/btree/binary-tree-node-entry.ts index 1c74472..36cb644 100644 --- a/src/btree/binary-tree-node-entry.ts +++ b/src/btree/binary-tree-node-entry.ts @@ -1,6 +1,6 @@ -import { DetailedError } from '../detailed-error'; -import { BinaryBPlusTreeNodeInfo } from './binary-tree-node-info'; -import { NodeEntryKeyType } from './entry-key-type'; +import { DetailedError } from '../detailed-error.js'; +import { BinaryBPlusTreeNodeInfo } from './binary-tree-node-info.js'; +import { NodeEntryKeyType } from './entry-key-type.js'; export class BinaryBPlusTreeNodeEntry { ltChildOffset: number = null; diff --git a/src/btree/binary-tree-node-info.ts b/src/btree/binary-tree-node-info.ts index 5cef5ef..29d2aaa 100644 --- a/src/btree/binary-tree-node-info.ts +++ b/src/btree/binary-tree-node-info.ts @@ -1,6 +1,6 @@ -import { BinaryBPlusTree } from './binary-tree'; -import { BinaryBPlusTreeNode } from './binary-tree-node'; -import { BinaryBPlusTreeNodeEntry } from './binary-tree-node-entry'; +import { BinaryBPlusTree } from './binary-tree.js'; +import { BinaryBPlusTreeNode } from './binary-tree-node.js'; +import { BinaryBPlusTreeNodeEntry } from './binary-tree-node-entry.js'; export class BinaryBPlusTreeNodeInfo { tree?: BinaryBPlusTree; diff --git a/src/btree/binary-tree-node.ts b/src/btree/binary-tree-node.ts index b97d8a1..2123845 100644 --- a/src/btree/binary-tree-node.ts +++ b/src/btree/binary-tree-node.ts @@ -1,6 +1,6 @@ -import { DetailedError } from '../detailed-error'; -import { BinaryBPlusTreeNodeEntry } from './binary-tree-node-entry'; -import { BinaryBPlusTreeNodeInfo } from './binary-tree-node-info'; +import { DetailedError } from '../detailed-error.js'; +import { BinaryBPlusTreeNodeEntry } from './binary-tree-node-entry.js'; +import { BinaryBPlusTreeNodeInfo } from './binary-tree-node-info.js'; export class BinaryBPlusTreeNode extends BinaryBPlusTreeNodeInfo { diff --git a/src/btree/binary-tree-transaction-operation.ts b/src/btree/binary-tree-transaction-operation.ts index b8317d5..57973b7 100644 --- a/src/btree/binary-tree-transaction-operation.ts +++ b/src/btree/binary-tree-transaction-operation.ts @@ -1,7 +1,7 @@ -import { BinaryBPlusTreeLeafEntryValue } from './binary-tree-leaf-entry-value'; -import { NodeEntryKeyType } from './entry-key-type'; -import { LeafEntryMetaData } from './leaf-entry-metadata'; -import { LeafEntryRecordPointer } from './leaf-entry-recordpointer'; +import { BinaryBPlusTreeLeafEntryValue } from './binary-tree-leaf-entry-value.js'; +import { NodeEntryKeyType } from './entry-key-type.js'; +import { LeafEntryMetaData } from './leaf-entry-metadata.js'; +import { LeafEntryRecordPointer } from './leaf-entry-recordpointer.js'; export class BinaryBPlusTreeTransactionOperation { static add(key: NodeEntryKeyType, recordPointer: LeafEntryRecordPointer, metadata?: LeafEntryMetaData) { diff --git a/src/btree/binary-tree.spec.ts b/src/btree/binary-tree.spec.ts index e455599..422b92c 100644 --- a/src/btree/binary-tree.spec.ts +++ b/src/btree/binary-tree.spec.ts @@ -1,365 +1,365 @@ -import { BPlusTree, BinaryWriter, BinaryBPlusTree, BlacklistingSearchOperator, BinaryBPlusTreeLeafEntry } from '.'; -import { DebugLogger, ID } from 'acebase-core'; -import { BinaryBPlusTreeLeafEntryValue } from './binary-tree-leaf-entry-value'; - -describe('Unique Binary B+Tree', () => { - // Tests basic operations of the BinaryBPlusTree implementation - const FILL_FACTOR = 95; // AceBase uses 95% fill factor for key indexes - const AUTO_GROW = false; // autoGrow is not used by AceBase atm - const debug = new DebugLogger('log', 'B+Tree'); - const createBinaryTree = async () => { - const tree = new BPlusTree(100, true); - - const bytes = [] as number[]; - await tree.toBinary(true, BinaryWriter.forArray(bytes)); - const binaryTree = new BinaryBPlusTree({ readFn: bytes, logger: debug }); - binaryTree.id = ID.generate(); // Assign an id to allow edits (is enforced by tree to make sure multiple concurrent edits to the same source are sync locked) - binaryTree.autoGrow = AUTO_GROW; - return binaryTree; - }; - - const rebuildTree = async (tree: BinaryBPlusTree) => { - const bytes = [] as number[]; - const id = tree.id; - await tree.rebuild(BinaryWriter.forArray(bytes), { fillFactor: FILL_FACTOR, keepFreeSpace: true, increaseMaxEntries: true }); - tree = new BinaryBPlusTree({ readFn: bytes, logger: debug }); - tree.id = id; - tree.autoGrow = AUTO_GROW; - return tree; - }; - - it('is an instance', async () => { - const tree = await createBinaryTree(); - expect(tree).toBeInstanceOf(BinaryBPlusTree); - }); - - it('entries can added & found', async () => { - const tree = await createBinaryTree(); - - // Add 1 key - const testRecordPointer = [1,2,3,4]; - await tree.add('key', testRecordPointer); - - // Lookup the entry & check its value - const value = await tree.find('key') as BinaryBPlusTreeLeafEntryValue; - expect(value).not.toBeNull(); - for (let i = 0; i < testRecordPointer.length; i++) { - expect(value.recordPointer[i]).toEqual(testRecordPointer[i]); - } - }); - - describe('entries', () => { - - const TEST_KEYS = 1000; // This simulates the amount of children to be added to an AceBase node - const keys = [] as string[]; - // Create random keys - for (let i = 0; i < TEST_KEYS; i++) { - keys.push(ID.generate()); - } - - let tree: BinaryBPlusTree; - beforeAll(async () => { - // Create tree - tree = await createBinaryTree(); - - let rebuilds = 0; - - // Add keys 1 by 1 - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - const recordPointer = Array.from(key).map(ch => ch.charCodeAt(0)); // Fake (unique) recordpointer - try { - await tree.add(key, recordPointer); - } - catch(err) { - // While the tree grows, this happens. Rebuild the tree and try again - rebuilds++; - tree = await rebuildTree(tree); - await tree.add(key, recordPointer); // Retry add - } - } - - console.log(`Created a tree with ${keys.length} entries, ${rebuilds} rebuilds were needed`); - }); - - // Lookup all added entries - it('can be found', async () => { - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - const value = await tree.find(key); - expect(value).not.toBeNull(); - } - }); - - // Iterate the leafs from start to end, confirm the right order - it('can be iterated in ascending order', async () => { - let leaf = await tree.getFirstLeaf(); - expect(leaf).not.toBeNull(); - let lastEntry, count = 0; - while (leaf) { - for (let i = 0; i < leaf.entries.length; i++) { - count++; - const entry = leaf.entries[i]; - if (i > 0) { - // key > last - expect(entry.key > lastEntry.key).toBeTrue(); - } - lastEntry = entry; - } - leaf = leaf.getNext ? await leaf.getNext() : null; - } - expect(count).toEqual(keys.length); - }); - - // Iterate the leafs from end to start - it('can be iterated in descending order', async () => { - let leaf = await tree.getLastLeaf(); - expect(leaf).not.toBeNull(); - let count = 0; - let lastEntry: BinaryBPlusTreeLeafEntry; - while (leaf) { - for (let i = leaf.entries.length - 1; i >= 0 ; i--) { - count++; - const entry = leaf.entries[i]; - if (i < leaf.entries.length - 1) { - // key < last - expect(entry.key < lastEntry.key).toBeTrue(); - } - lastEntry = entry; - } - leaf = leaf.getPrevious ? await leaf.getPrevious() : null; - } - expect(count).toEqual(keys.length); - }); - - describe('can be queried', () => { - - const options = { entries: true, keys: true, values: true, count: true }; - - const checkResults = ( - result: Awaited>, - expectedKeys: string[], - log: string - ) => { - log && console.log(log); - expect(result.keyCount).toEqual(expectedKeys.length); - expect(result.valueCount).toEqual(expectedKeys.length); // unique tree, 1 value per key - expect(result.entries.length).toEqual(expectedKeys.length); - expect(result.keys.length).toEqual(expectedKeys.length); - expect(result.values.length).toEqual(expectedKeys.length); - const allFound = expectedKeys.every(key => result.keys.includes(key)); - expect(allFound).toBeTrue(); - }; - - it('with "==" operator', async () => { - // Find first entry - let result = await tree.search('==', keys[0], options); - checkResults(result, [keys[0]], `== "${keys[0]}": expecting 1 result`); - - // Find a random entry - const randomKey = keys[Math.floor(Math.random() * keys.length)]; - result = await tree.search('==', randomKey, options); - checkResults(result, [randomKey], `== "${randomKey}": expecting 1 result`); - }); - - it('with "!=" operator', async () => { - // Find all except 1 random entry - const excludeIndex = Math.floor(Math.random() * keys.length); - const excludeKey = keys[excludeIndex]; - const expectedKeys = keys.slice(0, excludeIndex).concat(keys.slice(excludeIndex+1)); - const result = await tree.search('!=', excludeKey, options); - checkResults(result, expectedKeys, `!= "${excludeKey}": expecting ${expectedKeys.length} results`); - }); - - it('with "<" operator', async () => { - // Find first 10 keys - const expectedKeys = keys.slice(0, 11); // Take 11, use last as < - const lessThanKey = expectedKeys.pop(); - const result = await tree.search('<', lessThanKey, options); - checkResults(result, expectedKeys, `< "${lessThanKey}": expecting ${expectedKeys.length} results`); - }); - - it('with "<=" operator', async () => { - // Find first 10 keys - const expectedKeys = keys.slice(0, 10); - const key = expectedKeys.slice(-1)[0]; - const result = await tree.search('<=', key, options); - checkResults(result, expectedKeys, `<= "${key}": expecting ${expectedKeys.length} results`); - }); - - it('with ">" operator', async () => { - // Find last 10 keys - const expectedKeys = keys.slice(-11); // Take 11, use first as > - const greaterThanKey = expectedKeys.shift(); - const result = await tree.search('>', greaterThanKey, options); - checkResults(result, expectedKeys, `> "${greaterThanKey}": expecting ${expectedKeys.length} results`); - }); - - it('with ">=" operator', async () => { - // Find last 10 keys - const expectedKeys = keys.slice(-10); - const result = await tree.search('>=', expectedKeys[0], options); - checkResults(result, expectedKeys, `>= "${expectedKeys[0]}": expecting ${expectedKeys.length} results`); - }); - - it('with "like" operator', async () => { - // All keys that start with the same 10 characters as the first key - let str = keys[0].slice(0, 10); - let expectedKeys = keys.filter(key => key.startsWith(str)); - let result = await tree.search('like', `${str}*`, options); - checkResults(result, expectedKeys, `like "${str}*": expecting ${expectedKeys.length} keys to start with "${str}"`); - - // All keys that end with the same 3 last characters of the first key - str = keys[0].slice(-3); - expectedKeys = keys.filter(key => key.endsWith(str)); - result = await tree.search('like', `*${str}`, options); - checkResults(result, expectedKeys, `like "*${str}": expecting ${expectedKeys.length} keys to end with "${str}"`); - - // All keys that contain the last 2 characters of the first key - str = keys[0].slice(-2); - expectedKeys = keys.filter(key => key.includes(str)); - result = await tree.search('like', `*${str}*`, options); - checkResults(result, expectedKeys, `like "*${str}*": expecting ${expectedKeys.length} keys to contain "${str}"`); - }); - - it('with "between" operator', async () => { - // Find custom range of keys - const [startIndex, endIndex] = [Math.floor(Math.random() * (keys.length-1)), Math.floor(Math.random() * (keys.length-1))].sort((a,b) => a < b ? -1 : 1); - const expectedKeys = startIndex === endIndex ? [keys[startIndex]] : keys.slice(startIndex, endIndex); - const firstKey = expectedKeys[0], lastKey = expectedKeys.slice(-1)[0]; - - let result = await tree.search('between', [firstKey, lastKey], options); - checkResults(result, expectedKeys, `between "${firstKey}" and "${lastKey}": expecting ${expectedKeys.length} results`); - - result = await tree.search('between', [lastKey, firstKey], options); - checkResults(result, expectedKeys, `between "${lastKey}" and "${firstKey}" (reversed): expecting ${expectedKeys.length} results`); - }); - - it('with "!between" operator', async () => { - // Find custom range of keys (before and after given indexes) - const [startIndex, endIndex] = [Math.floor(Math.random() * keys.length), Math.floor(Math.random() * keys.length)].sort((a,b) => a-b); // eg: [2,6] - const expectedKeys = keys.filter((key, index) => index < startIndex || index > endIndex); // eg: expect [1,2, 7,8,9] for indexes 2 and 6 of keys [1,2,3,4,5,6,7,8,9] - const firstKey = keys[startIndex], lastKey = keys[endIndex]; // eg: 3 and 6 - - let result = await tree.search('!between', [firstKey, lastKey], options); - checkResults(result, expectedKeys, `!between "${firstKey}" and "${lastKey}": expecting ${expectedKeys.length} results`); - - result = await tree.search('!between', [lastKey, firstKey], options); - checkResults(result, expectedKeys, `!between "${lastKey}" and "${firstKey}" (reversed): expecting ${expectedKeys.length} results`); - }); - - it('with "in" operator', async () => { - // Find 5 random keys - const r = () => Math.floor(Math.random() * keys.length); - const randomIndexes = [r(), r(), r(), r(), r()].reduce((indexes, index) => ((!indexes.includes(index) ? indexes.push(index) : 1), indexes), []); - const expectedKeys = randomIndexes.map(index => keys[index]); - const result = await tree.search('in', expectedKeys, options); - checkResults(result, expectedKeys, `in [${expectedKeys.map(key => `"${key}"`).join(',')}]: expecting ${expectedKeys.length} results`); - }); - - it('with "!in" operator', async () => { - // Find 5 random keys - const r = () => Math.floor(Math.random() * keys.length); - const randomIndexes = [r(), r(), r(), r(), r()].reduce((indexes, index) => ((!indexes.includes(index) ? indexes.push(index) : 1), indexes), []); - const blacklistedKeys = randomIndexes.map(index => keys[index]); - const expectedKeys = keys.reduce((allowed, key) => (!blacklistedKeys.includes(key) ? allowed.push(key) : 1) && allowed, []); - const result = await tree.search('!in', blacklistedKeys, options); - checkResults(result, expectedKeys, `!in [${blacklistedKeys.map(key => `"${key}"`).join(',')}]: expecting ${expectedKeys.length} results`); - }); - - it('with "exists" operator', async () => { - // Finds all keys with a defined value, same as search("!=", undefined) - // --> all keys in our test - const result = await tree.search('exists', undefined, options); - checkResults(result, keys, `exists: expecting ${keys.length} (all) results`); - }); - - it('with "!exists" operator', async () => { - // Finds results for key with undefined value, same as search("==", undefined) - // --> no keys in our test - const result = await tree.search('!exists', undefined, options); - checkResults(result, [], `!exists: expecting NO results`); - }); - - it('with BlacklistingSearchOperator', async () => { - const keysToBlacklist = keys.filter(() => Math.random() > 0.25); // blacklist ~75% - const expectedKeys = keys.filter(key => !keysToBlacklist.includes(key)); - - const blacklisted = [] as BinaryBPlusTreeLeafEntry[]; - const op = new BlacklistingSearchOperator(entry => { - if (keysToBlacklist.includes(entry.key as string)) { - blacklisted.push(entry); - return entry.values; // Return all values (1) as array to be blacklisted - } - }); - - let result = await tree.search(op, undefined, options); - checkResults(result, expectedKeys, `BlacklistingSearchOperator: expecting ${expectedKeys.length} results`); - expect(blacklisted.length).toEqual(keysToBlacklist.length); - - // Run again, using the previous results as filter. This should yield the same results - // No additional entries should have been blacklisted (blacklisted.length should remain the same!) - const filteredOptions = { filter: result.entries }; - Object.assign(filteredOptions, options); - result = await tree.search(op, undefined, filteredOptions); - expect(blacklisted.length).toEqual(keysToBlacklist.length); - checkResults(result, expectedKeys, `BlacklistingSearchOperator + filter: expecting ${expectedKeys.length} results`); - - // Run again, using blacklisted results as filter. This should yield no results - filteredOptions.filter = blacklisted; - result = await tree.search(op, undefined, filteredOptions); - expect(blacklisted.length).toEqual(keysToBlacklist.length); - checkResults(result, [], `BlacklistingSearchOperator + blacklist filter: expecting 0 results`); - }); - - it('with "matches" operator', async () => { - const regex = /[a-z]{6}/; - const expectedKeys = keys.filter(key => regex.test(key)); - const result = await tree.search('matches', regex, options); - checkResults(result, expectedKeys, `matches /${regex.source}/${regex.flags}: expecting ${expectedKeys.length} results`); - }); - - it('with "!matches" operator', async () => { - const regex = /[a-z]{6}/; - const expectedKeys = keys.filter(key => !regex.test(key)); - const result = await tree.search('!matches', regex, options); - checkResults(result, expectedKeys, `!matches /${regex.source}/${regex.flags}: expecting ${expectedKeys.length} results`); - }); - - }); - - afterAll(async () => { - // Remove all entries - let rebuilds = 0; - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - try { - await tree.remove(key); - } - catch(err) { - rebuilds++; - tree = await rebuildTree(tree); - await tree.remove(key); // Try again - } - } - - console.log(`Removed ${keys.length} entries from tree, ${rebuilds} rebuilds were needed`); - - // Expect the tree to be empty now - const leafStats = await tree.getFirstLeaf({ stats: true }); - expect(leafStats.entries.length).toEqual(0); - }); - }); - - it('returns null for keys not present', async () => { - const tree = await createBinaryTree(); - const value = await tree.find('unknown'); - expect(value).toBeNull(); - }); - - it('must not accept duplicate keys', async () => { - const tree = await createBinaryTree(); - await tree.add('unique_key', [1]); - await expectAsync(tree.add('unique_key', [2])).toBeRejected(); - }); -}); +import { BPlusTree, BinaryWriter, BinaryBPlusTree, BlacklistingSearchOperator, BinaryBPlusTreeLeafEntry } from './index.js'; +import { DebugLogger, ID } from 'acebase-core'; +import { BinaryBPlusTreeLeafEntryValue } from './binary-tree-leaf-entry-value.js'; + +describe('Unique Binary B+Tree', () => { + // Tests basic operations of the BinaryBPlusTree implementation + const FILL_FACTOR = 95; // AceBase uses 95% fill factor for key indexes + const AUTO_GROW = false; // autoGrow is not used by AceBase atm + const debug = new DebugLogger('log', 'B+Tree'); + const createBinaryTree = async () => { + const tree = new BPlusTree(100, true); + + const bytes = [] as number[]; + await tree.toBinary(true, BinaryWriter.forArray(bytes)); + const binaryTree = new BinaryBPlusTree({ readFn: bytes, logger: debug }); + binaryTree.id = ID.generate(); // Assign an id to allow edits (is enforced by tree to make sure multiple concurrent edits to the same source are sync locked) + binaryTree.autoGrow = AUTO_GROW; + return binaryTree; + }; + + const rebuildTree = async (tree: BinaryBPlusTree) => { + const bytes = [] as number[]; + const id = tree.id; + await tree.rebuild(BinaryWriter.forArray(bytes), { fillFactor: FILL_FACTOR, keepFreeSpace: true, increaseMaxEntries: true }); + tree = new BinaryBPlusTree({ readFn: bytes, logger: debug }); + tree.id = id; + tree.autoGrow = AUTO_GROW; + return tree; + }; + + it('is an instance', async () => { + const tree = await createBinaryTree(); + expect(tree).toBeInstanceOf(BinaryBPlusTree); + }); + + it('entries can added & found', async () => { + const tree = await createBinaryTree(); + + // Add 1 key + const testRecordPointer = [1,2,3,4]; + await tree.add('key', testRecordPointer); + + // Lookup the entry & check its value + const value = await tree.find('key') as BinaryBPlusTreeLeafEntryValue; + expect(value).not.toBeNull(); + for (let i = 0; i < testRecordPointer.length; i++) { + expect(value.recordPointer[i]).toEqual(testRecordPointer[i]); + } + }); + + describe('entries', () => { + + const TEST_KEYS = 1000; // This simulates the amount of children to be added to an AceBase node + const keys = [] as string[]; + // Create random keys + for (let i = 0; i < TEST_KEYS; i++) { + keys.push(ID.generate()); + } + + let tree: BinaryBPlusTree; + beforeAll(async () => { + // Create tree + tree = await createBinaryTree(); + + let rebuilds = 0; + + // Add keys 1 by 1 + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const recordPointer = Array.from(key).map(ch => ch.charCodeAt(0)); // Fake (unique) recordpointer + try { + await tree.add(key, recordPointer); + } + catch(err) { + // While the tree grows, this happens. Rebuild the tree and try again + rebuilds++; + tree = await rebuildTree(tree); + await tree.add(key, recordPointer); // Retry add + } + } + + console.log(`Created a tree with ${keys.length} entries, ${rebuilds} rebuilds were needed`); + }); + + // Lookup all added entries + it('can be found', async () => { + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const value = await tree.find(key); + expect(value).not.toBeNull(); + } + }); + + // Iterate the leafs from start to end, confirm the right order + it('can be iterated in ascending order', async () => { + let leaf = await tree.getFirstLeaf(); + expect(leaf).not.toBeNull(); + let lastEntry, count = 0; + while (leaf) { + for (let i = 0; i < leaf.entries.length; i++) { + count++; + const entry = leaf.entries[i]; + if (i > 0) { + // key > last + expect(entry.key > lastEntry.key).toBeTrue(); + } + lastEntry = entry; + } + leaf = leaf.getNext ? await leaf.getNext() : null; + } + expect(count).toEqual(keys.length); + }); + + // Iterate the leafs from end to start + it('can be iterated in descending order', async () => { + let leaf = await tree.getLastLeaf(); + expect(leaf).not.toBeNull(); + let count = 0; + let lastEntry: BinaryBPlusTreeLeafEntry; + while (leaf) { + for (let i = leaf.entries.length - 1; i >= 0 ; i--) { + count++; + const entry = leaf.entries[i]; + if (i < leaf.entries.length - 1) { + // key < last + expect(entry.key < lastEntry.key).toBeTrue(); + } + lastEntry = entry; + } + leaf = leaf.getPrevious ? await leaf.getPrevious() : null; + } + expect(count).toEqual(keys.length); + }); + + describe('can be queried', () => { + + const options = { entries: true, keys: true, values: true, count: true }; + + const checkResults = ( + result: Awaited>, + expectedKeys: string[], + log: string + ) => { + log && console.log(log); + expect(result.keyCount).toEqual(expectedKeys.length); + expect(result.valueCount).toEqual(expectedKeys.length); // unique tree, 1 value per key + expect(result.entries.length).toEqual(expectedKeys.length); + expect(result.keys.length).toEqual(expectedKeys.length); + expect(result.values.length).toEqual(expectedKeys.length); + const allFound = expectedKeys.every(key => result.keys.includes(key)); + expect(allFound).toBeTrue(); + }; + + it('with "==" operator', async () => { + // Find first entry + let result = await tree.search('==', keys[0], options); + checkResults(result, [keys[0]], `== "${keys[0]}": expecting 1 result`); + + // Find a random entry + const randomKey = keys[Math.floor(Math.random() * keys.length)]; + result = await tree.search('==', randomKey, options); + checkResults(result, [randomKey], `== "${randomKey}": expecting 1 result`); + }); + + it('with "!=" operator', async () => { + // Find all except 1 random entry + const excludeIndex = Math.floor(Math.random() * keys.length); + const excludeKey = keys[excludeIndex]; + const expectedKeys = keys.slice(0, excludeIndex).concat(keys.slice(excludeIndex+1)); + const result = await tree.search('!=', excludeKey, options); + checkResults(result, expectedKeys, `!= "${excludeKey}": expecting ${expectedKeys.length} results`); + }); + + it('with "<" operator', async () => { + // Find first 10 keys + const expectedKeys = keys.slice(0, 11); // Take 11, use last as < + const lessThanKey = expectedKeys.pop(); + const result = await tree.search('<', lessThanKey, options); + checkResults(result, expectedKeys, `< "${lessThanKey}": expecting ${expectedKeys.length} results`); + }); + + it('with "<=" operator', async () => { + // Find first 10 keys + const expectedKeys = keys.slice(0, 10); + const key = expectedKeys.slice(-1)[0]; + const result = await tree.search('<=', key, options); + checkResults(result, expectedKeys, `<= "${key}": expecting ${expectedKeys.length} results`); + }); + + it('with ">" operator', async () => { + // Find last 10 keys + const expectedKeys = keys.slice(-11); // Take 11, use first as > + const greaterThanKey = expectedKeys.shift(); + const result = await tree.search('>', greaterThanKey, options); + checkResults(result, expectedKeys, `> "${greaterThanKey}": expecting ${expectedKeys.length} results`); + }); + + it('with ">=" operator', async () => { + // Find last 10 keys + const expectedKeys = keys.slice(-10); + const result = await tree.search('>=', expectedKeys[0], options); + checkResults(result, expectedKeys, `>= "${expectedKeys[0]}": expecting ${expectedKeys.length} results`); + }); + + it('with "like" operator', async () => { + // All keys that start with the same 10 characters as the first key + let str = keys[0].slice(0, 10); + let expectedKeys = keys.filter(key => key.startsWith(str)); + let result = await tree.search('like', `${str}*`, options); + checkResults(result, expectedKeys, `like "${str}*": expecting ${expectedKeys.length} keys to start with "${str}"`); + + // All keys that end with the same 3 last characters of the first key + str = keys[0].slice(-3); + expectedKeys = keys.filter(key => key.endsWith(str)); + result = await tree.search('like', `*${str}`, options); + checkResults(result, expectedKeys, `like "*${str}": expecting ${expectedKeys.length} keys to end with "${str}"`); + + // All keys that contain the last 2 characters of the first key + str = keys[0].slice(-2); + expectedKeys = keys.filter(key => key.includes(str)); + result = await tree.search('like', `*${str}*`, options); + checkResults(result, expectedKeys, `like "*${str}*": expecting ${expectedKeys.length} keys to contain "${str}"`); + }); + + it('with "between" operator', async () => { + // Find custom range of keys + const [startIndex, endIndex] = [Math.floor(Math.random() * (keys.length-1)), Math.floor(Math.random() * (keys.length-1))].sort((a,b) => a < b ? -1 : 1); + const expectedKeys = startIndex === endIndex ? [keys[startIndex]] : keys.slice(startIndex, endIndex); + const firstKey = expectedKeys[0], lastKey = expectedKeys.slice(-1)[0]; + + let result = await tree.search('between', [firstKey, lastKey], options); + checkResults(result, expectedKeys, `between "${firstKey}" and "${lastKey}": expecting ${expectedKeys.length} results`); + + result = await tree.search('between', [lastKey, firstKey], options); + checkResults(result, expectedKeys, `between "${lastKey}" and "${firstKey}" (reversed): expecting ${expectedKeys.length} results`); + }); + + it('with "!between" operator', async () => { + // Find custom range of keys (before and after given indexes) + const [startIndex, endIndex] = [Math.floor(Math.random() * keys.length), Math.floor(Math.random() * keys.length)].sort((a,b) => a-b); // eg: [2,6] + const expectedKeys = keys.filter((key, index) => index < startIndex || index > endIndex); // eg: expect [1,2, 7,8,9] for indexes 2 and 6 of keys [1,2,3,4,5,6,7,8,9] + const firstKey = keys[startIndex], lastKey = keys[endIndex]; // eg: 3 and 6 + + let result = await tree.search('!between', [firstKey, lastKey], options); + checkResults(result, expectedKeys, `!between "${firstKey}" and "${lastKey}": expecting ${expectedKeys.length} results`); + + result = await tree.search('!between', [lastKey, firstKey], options); + checkResults(result, expectedKeys, `!between "${lastKey}" and "${firstKey}" (reversed): expecting ${expectedKeys.length} results`); + }); + + it('with "in" operator', async () => { + // Find 5 random keys + const r = () => Math.floor(Math.random() * keys.length); + const randomIndexes = [r(), r(), r(), r(), r()].reduce((indexes, index) => ((!indexes.includes(index) ? indexes.push(index) : 1), indexes), []); + const expectedKeys = randomIndexes.map(index => keys[index]); + const result = await tree.search('in', expectedKeys, options); + checkResults(result, expectedKeys, `in [${expectedKeys.map(key => `"${key}"`).join(',')}]: expecting ${expectedKeys.length} results`); + }); + + it('with "!in" operator', async () => { + // Find 5 random keys + const r = () => Math.floor(Math.random() * keys.length); + const randomIndexes = [r(), r(), r(), r(), r()].reduce((indexes, index) => ((!indexes.includes(index) ? indexes.push(index) : 1), indexes), []); + const blacklistedKeys = randomIndexes.map(index => keys[index]); + const expectedKeys = keys.reduce((allowed, key) => (!blacklistedKeys.includes(key) ? allowed.push(key) : 1) && allowed, []); + const result = await tree.search('!in', blacklistedKeys, options); + checkResults(result, expectedKeys, `!in [${blacklistedKeys.map(key => `"${key}"`).join(',')}]: expecting ${expectedKeys.length} results`); + }); + + it('with "exists" operator', async () => { + // Finds all keys with a defined value, same as search("!=", undefined) + // --> all keys in our test + const result = await tree.search('exists', undefined, options); + checkResults(result, keys, `exists: expecting ${keys.length} (all) results`); + }); + + it('with "!exists" operator', async () => { + // Finds results for key with undefined value, same as search("==", undefined) + // --> no keys in our test + const result = await tree.search('!exists', undefined, options); + checkResults(result, [], `!exists: expecting NO results`); + }); + + it('with BlacklistingSearchOperator', async () => { + const keysToBlacklist = keys.filter(() => Math.random() > 0.25); // blacklist ~75% + const expectedKeys = keys.filter(key => !keysToBlacklist.includes(key)); + + const blacklisted = [] as BinaryBPlusTreeLeafEntry[]; + const op = new BlacklistingSearchOperator(entry => { + if (keysToBlacklist.includes(entry.key as string)) { + blacklisted.push(entry); + return entry.values; // Return all values (1) as array to be blacklisted + } + }); + + let result = await tree.search(op, undefined, options); + checkResults(result, expectedKeys, `BlacklistingSearchOperator: expecting ${expectedKeys.length} results`); + expect(blacklisted.length).toEqual(keysToBlacklist.length); + + // Run again, using the previous results as filter. This should yield the same results + // No additional entries should have been blacklisted (blacklisted.length should remain the same!) + const filteredOptions = { filter: result.entries }; + Object.assign(filteredOptions, options); + result = await tree.search(op, undefined, filteredOptions); + expect(blacklisted.length).toEqual(keysToBlacklist.length); + checkResults(result, expectedKeys, `BlacklistingSearchOperator + filter: expecting ${expectedKeys.length} results`); + + // Run again, using blacklisted results as filter. This should yield no results + filteredOptions.filter = blacklisted; + result = await tree.search(op, undefined, filteredOptions); + expect(blacklisted.length).toEqual(keysToBlacklist.length); + checkResults(result, [], `BlacklistingSearchOperator + blacklist filter: expecting 0 results`); + }); + + it('with "matches" operator', async () => { + const regex = /[a-z]{6}/; + const expectedKeys = keys.filter(key => regex.test(key)); + const result = await tree.search('matches', regex, options); + checkResults(result, expectedKeys, `matches /${regex.source}/${regex.flags}: expecting ${expectedKeys.length} results`); + }); + + it('with "!matches" operator', async () => { + const regex = /[a-z]{6}/; + const expectedKeys = keys.filter(key => !regex.test(key)); + const result = await tree.search('!matches', regex, options); + checkResults(result, expectedKeys, `!matches /${regex.source}/${regex.flags}: expecting ${expectedKeys.length} results`); + }); + + }); + + afterAll(async () => { + // Remove all entries + let rebuilds = 0; + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + try { + await tree.remove(key); + } + catch(err) { + rebuilds++; + tree = await rebuildTree(tree); + await tree.remove(key); // Try again + } + } + + console.log(`Removed ${keys.length} entries from tree, ${rebuilds} rebuilds were needed`); + + // Expect the tree to be empty now + const leafStats = await tree.getFirstLeaf({ stats: true }); + expect(leafStats.entries.length).toEqual(0); + }); + }); + + it('returns null for keys not present', async () => { + const tree = await createBinaryTree(); + const value = await tree.find('unknown'); + expect(value).toBeNull(); + }); + + it('must not accept duplicate keys', async () => { + const tree = await createBinaryTree(); + await tree.add('unique_key', [1]); + await expectAsync(tree.add('unique_key', [2])).toBeRejected(); + }); +}); diff --git a/src/btree/binary-tree.ts b/src/btree/binary-tree.ts index a224d3b..77cc080 100644 --- a/src/btree/binary-tree.ts +++ b/src/btree/binary-tree.ts @@ -1,29 +1,29 @@ import { DebugLogger, LoggerPlugin, Utils } from 'acebase-core'; -import { readByteLength, readSignedOffset, Uint8ArrayBuilder, writeByteLength, writeSignedOffset } from '../binary'; -import { DetailedError } from '../detailed-error'; -import { ThreadSafe, ThreadSafeLock } from '../thread-safe'; -import { assert } from '../assert'; -import { BinaryReader, ReadFunction } from './binary-reader'; -import { BinaryBPlusTreeBuilder, FLAGS } from './binary-tree-builder'; -import { BinaryBPlusTreeLeaf } from './binary-tree-leaf'; -import { BinaryBPlusTreeLeafEntry } from './binary-tree-leaf-entry'; -import { IBinaryBPlusTreeLeafEntryExtData } from './binary-tree-leaf-entry-extdata'; -import { BinaryBPlusTreeLeafEntryValue } from './binary-tree-leaf-entry-value'; -import { BinaryBPlusTreeNode } from './binary-tree-node'; -import { BinaryBPlusTreeNodeEntry } from './binary-tree-node-entry'; -import { BinaryBPlusTreeNodeInfo } from './binary-tree-node-info'; -import { BinaryBPlusTreeTransactionOperation } from './binary-tree-transaction-operation'; -import { BinaryWriter } from './binary-writer'; -import { WRITE_SMALL_LEAFS } from './config'; -import { NodeEntryKeyType } from './entry-key-type'; -import { LeafEntryMetaData } from './leaf-entry-metadata'; -import { LeafEntryRecordPointer } from './leaf-entry-recordpointer'; -import { BPlusTree } from './tree'; -import { BPlusTreeBuilder } from './tree-builder'; -import { BPlusTreeLeafEntryValue } from './tree-leaf-entry-value'; -import { TX } from './tx'; -import { _compareBinary, _isEqual, _isLess, _isLessOrEqual, _isMore, _isMoreOrEqual, _isNotEqual } from './typesafe-compare'; -import { _appendToArray, _checkNewEntryArgs } from './utils'; +import { readByteLength, readSignedOffset, Uint8ArrayBuilder, writeByteLength, writeSignedOffset } from '../binary.js'; +import { DetailedError } from '../detailed-error.js'; +import { ThreadSafe, ThreadSafeLock } from '../thread-safe.js'; +import { assert } from '../assert.js'; +import { BinaryReader, ReadFunction } from './binary-reader.js'; +import { BinaryBPlusTreeBuilder, FLAGS } from './binary-tree-builder.js'; +import { BinaryBPlusTreeLeaf } from './binary-tree-leaf.js'; +import { BinaryBPlusTreeLeafEntry } from './binary-tree-leaf-entry.js'; +import { IBinaryBPlusTreeLeafEntryExtData } from './binary-tree-leaf-entry-extdata.js'; +import { BinaryBPlusTreeLeafEntryValue } from './binary-tree-leaf-entry-value.js'; +import { BinaryBPlusTreeNode } from './binary-tree-node.js'; +import { BinaryBPlusTreeNodeEntry } from './binary-tree-node-entry.js'; +import { BinaryBPlusTreeNodeInfo } from './binary-tree-node-info.js'; +import { BinaryBPlusTreeTransactionOperation } from './binary-tree-transaction-operation.js'; +import { BinaryWriter } from './binary-writer.js'; +import { WRITE_SMALL_LEAFS } from './config.js'; +import { NodeEntryKeyType } from './entry-key-type.js'; +import { LeafEntryMetaData } from './leaf-entry-metadata.js'; +import { LeafEntryRecordPointer } from './leaf-entry-recordpointer.js'; +import { BPlusTree } from './tree.js'; +import { BPlusTreeBuilder } from './tree-builder.js'; +import { BPlusTreeLeafEntryValue } from './tree-leaf-entry-value.js'; +import { TX } from './tx.js'; +import { _compareBinary, _isEqual, _isLess, _isLessOrEqual, _isMore, _isMoreOrEqual, _isNotEqual } from './typesafe-compare.js'; +import { _appendToArray, _checkNewEntryArgs } from './utils.js'; const { bigintToBytes } = Utils; // type ReadFunction = (index: number, length: number) => Buffer | Promise; diff --git a/src/btree/binary-writer.ts b/src/btree/binary-writer.ts index a69695e..a6f3dd6 100644 --- a/src/btree/binary-writer.ts +++ b/src/btree/binary-writer.ts @@ -1,9 +1,9 @@ import type { WriteStream } from 'fs'; -import { Uint8ArrayBuilder, writeByteLength, writeSignedNumber, BufferLike } from '../binary'; -import { BinaryBPlusTreeBuilder } from './binary-tree-builder'; +import { Uint8ArrayBuilder, writeByteLength, writeSignedNumber, BufferLike } from '../binary.js'; +import { BinaryBPlusTreeBuilder } from './binary-tree-builder.js'; import { Utils } from 'acebase-core'; -import { NodeEntryKeyType } from './entry-key-type'; -import { assert } from '../assert'; +import { NodeEntryKeyType } from './entry-key-type.js'; +import { assert } from '../assert.js'; const { numberToBytes, bytesToNumber } = Utils; type WriteStreamLike = Pick; diff --git a/src/btree/browser.ts b/src/btree/browser.ts index 6b81ec7..6378150 100644 --- a/src/btree/browser.ts +++ b/src/btree/browser.ts @@ -1,4 +1,4 @@ -import { NotSupported } from '../not-supported'; +import { NotSupported } from '../not-supported.js'; /** * Not supported in browser context diff --git a/src/btree/index.ts b/src/btree/index.ts index c00f132..b93e2bf 100644 --- a/src/btree/index.ts +++ b/src/btree/index.ts @@ -1,10 +1,10 @@ -import { BinaryReader } from './binary-reader'; -import { BinaryBPlusTree, BlacklistingSearchOperator } from './binary-tree'; -import { BinaryBPlusTreeLeaf } from './binary-tree-leaf'; -import { BinaryBPlusTreeLeafEntry } from './binary-tree-leaf-entry'; -import { BinaryWriter } from './binary-writer'; -import { BPlusTree } from './tree'; -import { BPlusTreeBuilder } from './tree-builder'; +import { BinaryReader } from './binary-reader.js'; +import { BinaryBPlusTree, BlacklistingSearchOperator } from './binary-tree.js'; +import { BinaryBPlusTreeLeaf } from './binary-tree-leaf.js'; +import { BinaryBPlusTreeLeafEntry } from './binary-tree-leaf-entry.js'; +import { BinaryWriter } from './binary-writer.js'; +import { BPlusTree } from './tree.js'; +import { BPlusTreeBuilder } from './tree-builder.js'; export { BPlusTree, diff --git a/src/btree/leaf-entry-metadata.ts b/src/btree/leaf-entry-metadata.ts index 3108670..124818b 100644 --- a/src/btree/leaf-entry-metadata.ts +++ b/src/btree/leaf-entry-metadata.ts @@ -1,3 +1,3 @@ -import { NodeEntryKeyType } from './entry-key-type'; +import { NodeEntryKeyType } from './entry-key-type.js'; export type LeafEntryMetaData = Record; diff --git a/src/btree/tree-builder.ts b/src/btree/tree-builder.ts index 13b1e2a..76f7305 100644 --- a/src/btree/tree-builder.ts +++ b/src/btree/tree-builder.ts @@ -1,16 +1,16 @@ -import { assert } from '../assert'; -import { DetailedError } from '../detailed-error'; -import { NodeEntryKeyType, NodeEntryValueType } from './entry-key-type'; -import { LeafEntryMetaData } from './leaf-entry-metadata'; -import { LeafEntryRecordPointer } from './leaf-entry-recordpointer'; -import { BPlusTree } from './tree'; -import { BPlusTreeLeaf } from './tree-leaf'; -import { BPlusTreeLeafEntry } from './tree-leaf-entry'; -import { BPlusTreeLeafEntryValue } from './tree-leaf-entry-value'; -import { BPlusTreeNode } from './tree-node'; -import { BPlusTreeNodeEntry } from './tree-node-entry'; -import { _sortCompare } from './typesafe-compare'; -import { _checkNewEntryArgs } from './utils'; +import { assert } from '../assert.js'; +import { DetailedError } from '../detailed-error.js'; +import { NodeEntryKeyType, NodeEntryValueType } from './entry-key-type.js'; +import { LeafEntryMetaData } from './leaf-entry-metadata.js'; +import { LeafEntryRecordPointer } from './leaf-entry-recordpointer.js'; +import { BPlusTree } from './tree.js'; +import { BPlusTreeLeaf } from './tree-leaf.js'; +import { BPlusTreeLeafEntry } from './tree-leaf-entry.js'; +import { BPlusTreeLeafEntryValue } from './tree-leaf-entry-value.js'; +import { BPlusTreeNode } from './tree-node.js'; +import { BPlusTreeNodeEntry } from './tree-node-entry.js'; +import { _sortCompare } from './typesafe-compare.js'; +import { _checkNewEntryArgs } from './utils.js'; export class BPlusTreeBuilder { list = new Map(); diff --git a/src/btree/tree-leaf-entry-value.ts b/src/btree/tree-leaf-entry-value.ts index 7fb2c53..e70c361 100644 --- a/src/btree/tree-leaf-entry-value.ts +++ b/src/btree/tree-leaf-entry-value.ts @@ -1,5 +1,5 @@ -import { LeafEntryMetaData } from './leaf-entry-metadata'; -import { LeafEntryRecordPointer } from './leaf-entry-recordpointer'; +import { LeafEntryMetaData } from './leaf-entry-metadata.js'; +import { LeafEntryRecordPointer } from './leaf-entry-recordpointer.js'; export class BPlusTreeLeafEntryValue { /** diff --git a/src/btree/tree-leaf-entry.ts b/src/btree/tree-leaf-entry.ts index 8b8aa29..62171ae 100644 --- a/src/btree/tree-leaf-entry.ts +++ b/src/btree/tree-leaf-entry.ts @@ -1,6 +1,6 @@ -import { NodeEntryKeyType } from './entry-key-type'; -import { BPlusTreeLeaf } from './tree-leaf'; -import { BPlusTreeLeafEntryValue } from './tree-leaf-entry-value'; +import { NodeEntryKeyType } from './entry-key-type.js'; +import { BPlusTreeLeaf } from './tree-leaf.js'; +import { BPlusTreeLeafEntryValue } from './tree-leaf-entry-value.js'; export class BPlusTreeLeafEntry { values: BPlusTreeLeafEntryValue[]; diff --git a/src/btree/tree-leaf.ts b/src/btree/tree-leaf.ts index 9f94bae..0e7fd91 100644 --- a/src/btree/tree-leaf.ts +++ b/src/btree/tree-leaf.ts @@ -1,19 +1,19 @@ -import { assert } from '../assert'; -import { writeByteLength } from '../binary'; -import { DetailedError } from '../detailed-error'; -import { BinaryReference } from './binary-reference'; -import { FLAGS } from './binary-tree-builder'; -import { BinaryWriter } from './binary-writer'; -import { MAX_LEAF_ENTRY_VALUES, MAX_SMALL_LEAF_VALUE_LENGTH, WRITE_SMALL_LEAFS } from './config'; -import { NodeEntryKeyType } from './entry-key-type'; -import { LeafEntryMetaData } from './leaf-entry-metadata'; -import { BPlusTree } from './tree'; -import { BPlusTreeLeafEntry } from './tree-leaf-entry'; -import { BPlusTreeLeafEntryValue } from './tree-leaf-entry-value'; -import { BPlusTreeNode } from './tree-node'; -import { BPlusTreeNodeEntry } from './tree-node-entry'; -import { _isEqual, _isMore } from './typesafe-compare'; -import { _appendToArray, _checkNewEntryArgs } from './utils'; +import { assert } from '../assert.js'; +import { writeByteLength } from '../binary.js'; +import { DetailedError } from '../detailed-error.js'; +import { BinaryReference } from './binary-reference.js'; +import { FLAGS } from './binary-tree-builder.js'; +import { BinaryWriter } from './binary-writer.js'; +import { MAX_LEAF_ENTRY_VALUES, MAX_SMALL_LEAF_VALUE_LENGTH, WRITE_SMALL_LEAFS } from './config.js'; +import { NodeEntryKeyType } from './entry-key-type.js'; +import { LeafEntryMetaData } from './leaf-entry-metadata.js'; +import { BPlusTree } from './tree.js'; +import { BPlusTreeLeafEntry } from './tree-leaf-entry.js'; +import { BPlusTreeLeafEntryValue } from './tree-leaf-entry-value.js'; +import { BPlusTreeNode } from './tree-node.js'; +import { BPlusTreeNodeEntry } from './tree-node-entry.js'; +import { _isEqual, _isMore } from './typesafe-compare.js'; +import { _appendToArray, _checkNewEntryArgs } from './utils.js'; export class BPlusTreeLeaf { diff --git a/src/btree/tree-node-entry.ts b/src/btree/tree-node-entry.ts index 457117a..4a0db37 100644 --- a/src/btree/tree-node-entry.ts +++ b/src/btree/tree-node-entry.ts @@ -1,6 +1,6 @@ -import { BPlusTreeNode } from './tree-node'; -import { BPlusTreeLeaf } from './tree-leaf'; -import { NodeEntryKeyType } from './entry-key-type'; +import { BPlusTreeNode } from './tree-node.js'; +import { BPlusTreeLeaf } from './tree-leaf.js'; +import { NodeEntryKeyType } from './entry-key-type.js'; export class BPlusTreeNodeEntry { ltChild: BPlusTreeNode | BPlusTreeLeaf = null; diff --git a/src/btree/tree-node.ts b/src/btree/tree-node.ts index a722a13..daa2ebf 100644 --- a/src/btree/tree-node.ts +++ b/src/btree/tree-node.ts @@ -1,13 +1,13 @@ -import { BPlusTreeNodeEntry } from './tree-node-entry'; -import { BPlusTreeLeaf } from './tree-leaf'; -import { BPlusTree } from './tree'; -import { NodeEntryKeyType } from './entry-key-type'; -import { _isEqual, _isMore } from './typesafe-compare'; -import { DetailedError } from '../detailed-error'; -import { BinaryWriter } from './binary-writer'; -import { writeByteLength, writeSignedOffset } from '../binary'; -import { BinaryPointer } from './binary-pointer'; -import { BinaryReference } from './binary-reference'; +import { BPlusTreeNodeEntry } from './tree-node-entry.js'; +import { BPlusTreeLeaf } from './tree-leaf.js'; +import { BPlusTree } from './tree.js'; +import { NodeEntryKeyType } from './entry-key-type.js'; +import { _isEqual, _isMore } from './typesafe-compare.js'; +import { DetailedError } from '../detailed-error.js'; +import { BinaryWriter } from './binary-writer.js'; +import { writeByteLength, writeSignedOffset } from '../binary.js'; +import { BinaryPointer } from './binary-pointer.js'; +import { BinaryReference } from './binary-reference.js'; export class BPlusTreeNode { entries: BPlusTreeNodeEntry[] = []; diff --git a/src/btree/tree.spec.ts b/src/btree/tree.spec.ts index 1fdf1bd..9fb6b35 100644 --- a/src/btree/tree.spec.ts +++ b/src/btree/tree.spec.ts @@ -1,7 +1,7 @@ -import { BPlusTree } from '.'; +import { BPlusTree } from './index.js'; import { ID } from 'acebase-core'; -import { BPlusTreeLeafEntryValue } from './tree-leaf-entry-value'; -import { BPlusTreeLeafEntry } from './tree-leaf-entry'; +import { BPlusTreeLeafEntryValue } from './tree-leaf-entry-value.js'; +import { BPlusTreeLeafEntry } from './tree-leaf-entry.js'; describe('Unique B+Tree', () => { // Tests basic operations of the (append only) BPlusTree implementation diff --git a/src/btree/tree.ts b/src/btree/tree.ts index 4c0d3f8..ae63992 100644 --- a/src/btree/tree.ts +++ b/src/btree/tree.ts @@ -1,17 +1,17 @@ import { Utils } from 'acebase-core'; -import { assert } from '../assert'; -import { writeByteLength } from '../binary'; -import { DetailedError } from '../detailed-error'; -import { FLAGS, KEY_TYPE } from './binary-tree-builder'; -import { BinaryWriter } from './binary-writer'; -import { WRITE_SMALL_LEAFS } from './config'; -import { NodeEntryKeyType, NodeEntryValueType } from './entry-key-type'; -import { LeafEntryMetaData } from './leaf-entry-metadata'; -import { BPlusTreeLeaf } from './tree-leaf'; -import { BPlusTreeLeafEntry } from './tree-leaf-entry'; -import { BPlusTreeLeafEntryValue } from './tree-leaf-entry-value'; -import { BPlusTreeNode } from './tree-node'; -import { _isEqual, _isLess, _isLessOrEqual, _isMore, _isMoreOrEqual, _isNotEqual } from './typesafe-compare'; +import { assert } from '../assert.js'; +import { writeByteLength } from '../binary.js'; +import { DetailedError } from '../detailed-error.js'; +import { FLAGS, KEY_TYPE } from './binary-tree-builder.js'; +import { BinaryWriter } from './binary-writer.js'; +import { WRITE_SMALL_LEAFS } from './config.js'; +import { NodeEntryKeyType, NodeEntryValueType } from './entry-key-type.js'; +import { LeafEntryMetaData } from './leaf-entry-metadata.js'; +import { BPlusTreeLeaf } from './tree-leaf.js'; +import { BPlusTreeLeafEntry } from './tree-leaf-entry.js'; +import { BPlusTreeLeafEntryValue } from './tree-leaf-entry-value.js'; +import { BPlusTreeNode } from './tree-node.js'; +import { _isEqual, _isLess, _isLessOrEqual, _isMore, _isMoreOrEqual, _isNotEqual } from './typesafe-compare.js'; const { bigintToBytes, bytesToBigint, bytesToNumber, decodeString, encodeString, numberToBytes } = Utils; diff --git a/src/btree/tx.ts b/src/btree/tx.ts index 9123185..093d2ce 100644 --- a/src/btree/tx.ts +++ b/src/btree/tx.ts @@ -1,5 +1,5 @@ -import { assert } from '../assert'; -import { DetailedError } from '../detailed-error'; +import { assert } from '../assert.js'; +import { DetailedError } from '../detailed-error.js'; export class TxDetailedError extends DetailedError { transactionErrors: Array = null; diff --git a/src/btree/utils.ts b/src/btree/utils.ts index 5f0aa43..80b23d8 100644 --- a/src/btree/utils.ts +++ b/src/btree/utils.ts @@ -1,6 +1,6 @@ -import { NodeEntryKeyType } from './entry-key-type'; -import { LeafEntryMetaData } from './leaf-entry-metadata'; -import { LeafEntryRecordPointer } from './leaf-entry-recordpointer'; +import { NodeEntryKeyType } from './entry-key-type.js'; +import { LeafEntryMetaData } from './leaf-entry-metadata.js'; +import { LeafEntryRecordPointer } from './leaf-entry-recordpointer.js'; export function _checkNewEntryArgs(key: NodeEntryKeyType, recordPointer: LeafEntryRecordPointer, metadataKeys: string[], metadata: LeafEntryMetaData) { const storageTypesText = 'supported types are string, number, boolean, Date and undefined'; diff --git a/src/data-index/array-index-query-hint.ts b/src/data-index/array-index-query-hint.ts index f51c8af..e448d0f 100644 --- a/src/data-index/array-index-query-hint.ts +++ b/src/data-index/array-index-query-hint.ts @@ -1,4 +1,4 @@ -import { IndexQueryHint } from './query-hint'; +import { IndexQueryHint } from './query-hint.js'; export class ArrayIndexQueryHint extends IndexQueryHint { static get types() { diff --git a/src/data-index/array-index.ts b/src/data-index/array-index.ts index d660836..b5931be 100644 --- a/src/data-index/array-index.ts +++ b/src/data-index/array-index.ts @@ -1,274 +1,274 @@ -import { BlacklistingSearchOperator } from '../btree'; -import { DataIndex } from './data-index'; -import { DataIndexOptions } from './options'; -import type { Storage } from '../storage'; -import { IndexableValue, IndexableValueOrArray } from './shared'; -import { VALUE_TYPES } from '../node-value-types'; -import { IndexQueryResults } from './query-results'; -import { IndexQueryStats } from './query-stats'; -import { ArrayIndexQueryHint } from './array-index-query-hint'; - -/** - * An array index allows all values in an array node to be indexed and searched - */ -export class ArrayIndex extends DataIndex { - constructor(storage: Storage, path: string, key: string, options: DataIndexOptions) { - if (key === '{key}') { throw new Error('Cannot create array index on node keys'); } - super(storage, path, key, options); - } - - // get fileName() { - // return super.fileName.slice(0, -4) + '.array.idx'; - // } - - get type() { - return 'array'; - } - - async handleRecordUpdate(path: string, oldValue: unknown, newValue: unknown) { - const tmpOld = oldValue !== null && typeof oldValue === 'object' && this.key in oldValue ? (oldValue as any)[this.key] : null; - const tmpNew = newValue !== null && typeof newValue === 'object' && this.key in newValue ? (newValue as any)[this.key] : null; - - let oldEntries: IndexableValue[]; - if (tmpOld instanceof Array) { - // Only use unique values - oldEntries = tmpOld.reduce((unique, entry) => { - !unique.includes(entry) && unique.push(entry); - return unique; - }, []); - } - else { oldEntries = []; } - if (oldEntries.length === 0) { - // Add undefined entry to indicate empty array - oldEntries.push(undefined); - } - - let newEntries: IndexableValue[]; - if (tmpNew instanceof Array) { - // Only use unique values - newEntries = tmpNew.reduce((unique, entry) => { - !unique.includes(entry) && unique.push(entry); - return unique; - }, []); - } - else { newEntries = []; } - if (newEntries.length === 0) { - // Add undefined entry to indicate empty array - newEntries.push(undefined); - } - const removed = oldEntries.filter(entry => !newEntries.includes(entry)); - const added = newEntries.filter(entry => !oldEntries.includes(entry)); - - const mutated = { old: {} as any, new: {} as any }; - Object.assign(mutated.old, oldValue); - Object.assign(mutated.new, newValue); - - const promises = [] as Promise[]; - removed.forEach(entry => { - mutated.old[this.key] = entry; - mutated.new[this.key] = null; - const p = super.handleRecordUpdate(path, mutated.old, mutated.new); - promises.push(p); - }); - added.forEach(entry => { - mutated.old[this.key] = null; - mutated.new[this.key] = entry; - const p = super.handleRecordUpdate(path, mutated.old, mutated.new); - promises.push(p); - }); - await Promise.all(promises); - } - - build() { - return super.build({ - addCallback: (add, array: IndexableValue[], recordPointer, metadata) => { - if (!(array instanceof Array) || array.length === 0) { - // Add undefined entry to indicate empty array - add(undefined, recordPointer, metadata); - return []; - } - - // index unique items only - array.reduce((unique, value) => { - !unique.includes(value) && unique.push(value); - return unique; - }, []).forEach(value => { - add(value, recordPointer, metadata); - }); - return array; - }, - valueTypes: [VALUE_TYPES.ARRAY], - }); - } - - static get validOperators() { - // This is the only special index that does not use prefixed operators - // because these can also be used to query non-indexed arrays (but slower, of course..) - return ['contains', '!contains']; - } - get validOperators() { - return ArrayIndex.validOperators; - } - - async query(op: BlacklistingSearchOperator): Promise; - async query(op: string, val: IndexableValueOrArray, options?: { filter?: IndexQueryResults; }): Promise; - /** - * @param op "contains" or "!contains" - * @param val value to search for - */ - async query(op: string | BlacklistingSearchOperator, val?: IndexableValueOrArray, options?: { filter?: IndexQueryResults; }) { - if (op instanceof BlacklistingSearchOperator) { - throw new Error(`Not implemented: Can't query array index with blacklisting operator yet`); - } - if (!ArrayIndex.validOperators.includes(op)) { - throw new Error(`Array indexes can only be queried with operators ${ArrayIndex.validOperators.map(op => `"${op}"`).join(', ')}`); - } - if (options) { - this.logger.warn('Not implemented: query options for array indexes are ignored'); - } - - // Check cache - const cache = this.cache(op, val); - if (cache) { - // Use cached results - return cache; - } - - const stats = new IndexQueryStats('array_index_query', val, true); - - if ((op === 'contains' || op === '!contains') && val instanceof Array && val.length === 0) { - // Added for #135: empty compare array for contains/!contains must match all values - stats.type = 'array_index_scan'; - const results = await super.query(new BlacklistingSearchOperator((_) => [])); - stats.stop(results.length); - results.filterKey = this.key; - results.stats = stats; - // Don't cache results - return results; - } - else if (op === 'contains') { - if (val instanceof Array) { - // recipesIndex.query('contains', ['egg','bacon']) - - // Get result count for each value in array - const countPromises = val.map(value => { - const wildcardIndex = typeof value !== 'string' ? -1 : ~(~value.indexOf('*') || ~value.indexOf('?')); - const valueOp = ~wildcardIndex ? 'like' : '=='; - - const step = new IndexQueryStats('count', value, true); - stats.steps.push(step); - - return this.count(valueOp, value) - .then(count => { - step.stop(count); - return { value, count }; - }); - }); - const counts = await Promise.all(countPromises); - // Start with the smallest result set - counts.sort((a, b) => { - if (a.count < b.count) { return -1; } - else if (a.count > b.count) { return 1; } - return 0; - }); - - let results: IndexQueryResults; - - if (counts[0].count === 0) { - stats.stop(0); - - this.logger.info(`Value "${counts[0].value}" not found in index, 0 results for query ${op} ${val}`); - results = new IndexQueryResults(0); - results.filterKey = this.key; - results.stats = stats; - - // Add query hints for each unknown item - counts.forEach(c => { - if (c.count === 0) { - const hint = new ArrayIndexQueryHint(ArrayIndexQueryHint.types.missingValue, c.value); - results.hints.push(hint); - } - }); - - // Cache the empty result set - this.cache(op, val, results); - return results; - } - const allValues = counts.map(c => c.value); - - // Query 1 value, then filter results further and further - // Start with the smallest result set - const queryValue = (value: IndexableValue, filter?: IndexQueryResults) => { - const wildcardIndex = typeof value !== 'string' ? -1 : ~(~value.indexOf('*') || ~value.indexOf('?')); - const valueOp = ~wildcardIndex ? 'like' : '=='; - - return super.query(valueOp, value, { filter }) - .then(results => { - stats.steps.push(results.stats); - return results; - }); - }; - let valueIndex = 0; - // let resultsPerValue = new Array(values.length); - const nextValue = async () => { - const value = allValues[valueIndex]; - const fr = await queryValue(value, results); - results = fr; - valueIndex++; - if (results.length === 0 || valueIndex === allValues.length) { return; } - await nextValue(); - }; - await nextValue(); - results.filterKey = this.key; - - stats.stop(results.length); - results.stats = stats; - - // Cache results - delete results.entryValues; // No need to cache these. Free the memory - this.cache(op, val, results); - return results; - } - else { - // Single value query - const valueOp = - typeof val === 'string' && (val.includes('*') || val.includes('?')) - ? 'like' - : '=='; - const results = await super.query(valueOp, val); - stats.steps.push(results.stats); - results.stats = stats; - delete results.entryValues; - return results; - } - } - else if (op === '!contains') { - // DISABLED executing super.query('!=', val) because it returns false positives - // for arrays that "!contains" val, but does contain other values... - // Eg: an indexed array value of: ['bacon', 'egg', 'toast', 'sausage'], - // when executing index.query('!contains', 'bacon'), - // it will falsely match that record because the 2nd value 'egg' - // matches the filter ('egg' is not 'bacon') - - // NEW: BlacklistingSearchOperator will take all values in the index unless - // they are blacklisted along the way. Our callback determines whether to blacklist - // an entry's values, which we'll do if its key matches val - const customOp = new BlacklistingSearchOperator(entry => { - const blacklist = val === entry.key - || (val instanceof Array && val.includes(entry.key)); - if (blacklist) { return entry.values; } - }); - - stats.type = 'array_index_blacklist_scan'; - const results = await super.query(customOp); - stats.stop(results.length); - results.filterKey = this.key; - results.stats = stats; - - // Cache results - this.cache(op, val, results); - return results; - } - } -} - +import { BlacklistingSearchOperator } from '../btree/index.js'; +import { DataIndex } from './data-index.js'; +import { DataIndexOptions } from './options.js'; +import type { Storage } from '../storage/index.js'; +import { IndexableValue, IndexableValueOrArray } from './shared.js'; +import { VALUE_TYPES } from '../node-value-types.js'; +import { IndexQueryResults } from './query-results.js'; +import { IndexQueryStats } from './query-stats.js'; +import { ArrayIndexQueryHint } from './array-index-query-hint.js'; + +/** + * An array index allows all values in an array node to be indexed and searched + */ +export class ArrayIndex extends DataIndex { + constructor(storage: Storage, path: string, key: string, options: DataIndexOptions) { + if (key === '{key}') { throw new Error('Cannot create array index on node keys'); } + super(storage, path, key, options); + } + + // get fileName() { + // return super.fileName.slice(0, -4) + '.array.idx'; + // } + + get type() { + return 'array'; + } + + async handleRecordUpdate(path: string, oldValue: unknown, newValue: unknown) { + const tmpOld = oldValue !== null && typeof oldValue === 'object' && this.key in oldValue ? (oldValue as any)[this.key] : null; + const tmpNew = newValue !== null && typeof newValue === 'object' && this.key in newValue ? (newValue as any)[this.key] : null; + + let oldEntries: IndexableValue[]; + if (tmpOld instanceof Array) { + // Only use unique values + oldEntries = tmpOld.reduce((unique, entry) => { + !unique.includes(entry) && unique.push(entry); + return unique; + }, []); + } + else { oldEntries = []; } + if (oldEntries.length === 0) { + // Add undefined entry to indicate empty array + oldEntries.push(undefined); + } + + let newEntries: IndexableValue[]; + if (tmpNew instanceof Array) { + // Only use unique values + newEntries = tmpNew.reduce((unique, entry) => { + !unique.includes(entry) && unique.push(entry); + return unique; + }, []); + } + else { newEntries = []; } + if (newEntries.length === 0) { + // Add undefined entry to indicate empty array + newEntries.push(undefined); + } + const removed = oldEntries.filter(entry => !newEntries.includes(entry)); + const added = newEntries.filter(entry => !oldEntries.includes(entry)); + + const mutated = { old: {} as any, new: {} as any }; + Object.assign(mutated.old, oldValue); + Object.assign(mutated.new, newValue); + + const promises = [] as Promise[]; + removed.forEach(entry => { + mutated.old[this.key] = entry; + mutated.new[this.key] = null; + const p = super.handleRecordUpdate(path, mutated.old, mutated.new); + promises.push(p); + }); + added.forEach(entry => { + mutated.old[this.key] = null; + mutated.new[this.key] = entry; + const p = super.handleRecordUpdate(path, mutated.old, mutated.new); + promises.push(p); + }); + await Promise.all(promises); + } + + build() { + return super.build({ + addCallback: (add, array: IndexableValue[], recordPointer, metadata) => { + if (!(array instanceof Array) || array.length === 0) { + // Add undefined entry to indicate empty array + add(undefined, recordPointer, metadata); + return []; + } + + // index unique items only + array.reduce((unique, value) => { + !unique.includes(value) && unique.push(value); + return unique; + }, []).forEach(value => { + add(value, recordPointer, metadata); + }); + return array; + }, + valueTypes: [VALUE_TYPES.ARRAY], + }); + } + + static get validOperators() { + // This is the only special index that does not use prefixed operators + // because these can also be used to query non-indexed arrays (but slower, of course..) + return ['contains', '!contains']; + } + get validOperators() { + return ArrayIndex.validOperators; + } + + async query(op: BlacklistingSearchOperator): Promise; + async query(op: string, val: IndexableValueOrArray, options?: { filter?: IndexQueryResults; }): Promise; + /** + * @param op "contains" or "!contains" + * @param val value to search for + */ + async query(op: string | BlacklistingSearchOperator, val?: IndexableValueOrArray, options?: { filter?: IndexQueryResults; }) { + if (op instanceof BlacklistingSearchOperator) { + throw new Error(`Not implemented: Can't query array index with blacklisting operator yet`); + } + if (!ArrayIndex.validOperators.includes(op)) { + throw new Error(`Array indexes can only be queried with operators ${ArrayIndex.validOperators.map(op => `"${op}"`).join(', ')}`); + } + if (options) { + this.logger.warn('Not implemented: query options for array indexes are ignored'); + } + + // Check cache + const cache = this.cache(op, val); + if (cache) { + // Use cached results + return cache; + } + + const stats = new IndexQueryStats('array_index_query', val, true); + + if ((op === 'contains' || op === '!contains') && val instanceof Array && val.length === 0) { + // Added for #135: empty compare array for contains/!contains must match all values + stats.type = 'array_index_scan'; + const results = await super.query(new BlacklistingSearchOperator((_) => [])); + stats.stop(results.length); + results.filterKey = this.key; + results.stats = stats; + // Don't cache results + return results; + } + else if (op === 'contains') { + if (val instanceof Array) { + // recipesIndex.query('contains', ['egg','bacon']) + + // Get result count for each value in array + const countPromises = val.map(value => { + const wildcardIndex = typeof value !== 'string' ? -1 : ~(~value.indexOf('*') || ~value.indexOf('?')); + const valueOp = ~wildcardIndex ? 'like' : '=='; + + const step = new IndexQueryStats('count', value, true); + stats.steps.push(step); + + return this.count(valueOp, value) + .then(count => { + step.stop(count); + return { value, count }; + }); + }); + const counts = await Promise.all(countPromises); + // Start with the smallest result set + counts.sort((a, b) => { + if (a.count < b.count) { return -1; } + else if (a.count > b.count) { return 1; } + return 0; + }); + + let results: IndexQueryResults; + + if (counts[0].count === 0) { + stats.stop(0); + + this.logger.info(`Value "${counts[0].value}" not found in index, 0 results for query ${op} ${val}`); + results = new IndexQueryResults(0); + results.filterKey = this.key; + results.stats = stats; + + // Add query hints for each unknown item + counts.forEach(c => { + if (c.count === 0) { + const hint = new ArrayIndexQueryHint(ArrayIndexQueryHint.types.missingValue, c.value); + results.hints.push(hint); + } + }); + + // Cache the empty result set + this.cache(op, val, results); + return results; + } + const allValues = counts.map(c => c.value); + + // Query 1 value, then filter results further and further + // Start with the smallest result set + const queryValue = (value: IndexableValue, filter?: IndexQueryResults) => { + const wildcardIndex = typeof value !== 'string' ? -1 : ~(~value.indexOf('*') || ~value.indexOf('?')); + const valueOp = ~wildcardIndex ? 'like' : '=='; + + return super.query(valueOp, value, { filter }) + .then(results => { + stats.steps.push(results.stats); + return results; + }); + }; + let valueIndex = 0; + // let resultsPerValue = new Array(values.length); + const nextValue = async () => { + const value = allValues[valueIndex]; + const fr = await queryValue(value, results); + results = fr; + valueIndex++; + if (results.length === 0 || valueIndex === allValues.length) { return; } + await nextValue(); + }; + await nextValue(); + results.filterKey = this.key; + + stats.stop(results.length); + results.stats = stats; + + // Cache results + delete results.entryValues; // No need to cache these. Free the memory + this.cache(op, val, results); + return results; + } + else { + // Single value query + const valueOp = + typeof val === 'string' && (val.includes('*') || val.includes('?')) + ? 'like' + : '=='; + const results = await super.query(valueOp, val); + stats.steps.push(results.stats); + results.stats = stats; + delete results.entryValues; + return results; + } + } + else if (op === '!contains') { + // DISABLED executing super.query('!=', val) because it returns false positives + // for arrays that "!contains" val, but does contain other values... + // Eg: an indexed array value of: ['bacon', 'egg', 'toast', 'sausage'], + // when executing index.query('!contains', 'bacon'), + // it will falsely match that record because the 2nd value 'egg' + // matches the filter ('egg' is not 'bacon') + + // NEW: BlacklistingSearchOperator will take all values in the index unless + // they are blacklisted along the way. Our callback determines whether to blacklist + // an entry's values, which we'll do if its key matches val + const customOp = new BlacklistingSearchOperator(entry => { + const blacklist = val === entry.key + || (val instanceof Array && val.includes(entry.key)); + if (blacklist) { return entry.values; } + }); + + stats.type = 'array_index_blacklist_scan'; + const results = await super.query(customOp); + stats.stop(results.length); + results.filterKey = this.key; + results.stats = stats; + + // Cache results + this.cache(op, val, results); + return results; + } + } +} + diff --git a/src/data-index/browser.ts b/src/data-index/browser.ts index 2e0bea8..5685fce 100644 --- a/src/data-index/browser.ts +++ b/src/data-index/browser.ts @@ -1,4 +1,4 @@ -import { NotSupported } from '../not-supported'; +import { NotSupported } from '../not-supported.js'; /** * Not supported in browser context diff --git a/src/data-index/data-index.ts b/src/data-index/data-index.ts index 0426e9d..1eac66b 100644 --- a/src/data-index/data-index.ts +++ b/src/data-index/data-index.ts @@ -1,17 +1,17 @@ import { PathInfo, Utils, ID, ColorStyle, Transport, type LoggerPlugin } from 'acebase-core'; -import { ThreadSafe } from '../thread-safe'; -import type { Storage } from '../storage'; -import { pfs } from '../promise-fs'; -import { BPlusTreeBuilder, BPlusTree, BinaryBPlusTree, BinaryBPlusTreeLeafEntry, BinaryWriter, BinaryReader, BlacklistingSearchOperator } from '../btree'; -import { DataIndexOptions } from './options'; -import { FileSystemError, IndexableValue, IndexableValueOrArray, IndexMetaData, IndexRecordPointer } from './shared'; -import { getValueType, VALUE_TYPES } from '../node-value-types'; -import quickSort from '../quicksort'; -import { BinaryBPlusTreeTransactionOperation } from '../btree/binary-tree-transaction-operation'; -import { IndexQueryStats } from './query-stats'; -import { IndexQueryResult, IndexQueryResults } from './query-results'; -import { BPlusTreeLeafEntryValue } from '../btree/tree-leaf-entry-value'; -import { assert } from '../assert'; +import { ThreadSafe } from '../thread-safe.js'; +import type { Storage } from '../storage/index.js'; +import { pfs } from '../promise-fs/index.js'; +import { BPlusTreeBuilder, BPlusTree, BinaryBPlusTree, BinaryBPlusTreeLeafEntry, BinaryWriter, BinaryReader, BlacklistingSearchOperator } from '../btree/index.js'; +import { DataIndexOptions } from './options.js'; +import { FileSystemError, IndexableValue, IndexableValueOrArray, IndexMetaData, IndexRecordPointer } from './shared.js'; +import { getValueType, VALUE_TYPES } from '../node-value-types.js'; +import quickSort from '../quicksort.js'; +import { BinaryBPlusTreeTransactionOperation } from '../btree/binary-tree-transaction-operation.js'; +import { IndexQueryStats } from './query-stats.js'; +import { IndexQueryResult, IndexQueryResults } from './query-results.js'; +import { BPlusTreeLeafEntryValue } from '../btree/tree-leaf-entry-value.js'; +import { assert } from '../assert.js'; const { compareValues, getChildValues, numberToBytes, bytesToNumber, encodeString, decodeString } = Utils; const DISK_BLOCK_SIZE = 4096; // use 512 for older disks diff --git a/src/data-index/fulltext-index-query-hint.ts b/src/data-index/fulltext-index-query-hint.ts index 0da9fee..c66300a 100644 --- a/src/data-index/fulltext-index-query-hint.ts +++ b/src/data-index/fulltext-index-query-hint.ts @@ -1,4 +1,4 @@ -import { IndexQueryHint } from './query-hint'; +import { IndexQueryHint } from './query-hint.js'; export class FullTextIndexQueryHint extends IndexQueryHint { static get types() { diff --git a/src/data-index/fulltext-index.ts b/src/data-index/fulltext-index.ts index 002e1b4..4957f19 100644 --- a/src/data-index/fulltext-index.ts +++ b/src/data-index/fulltext-index.ts @@ -1,1041 +1,1041 @@ -import { DataIndex } from './data-index'; -import { DataIndexOptions } from './options'; -import { IndexQueryResults } from './query-results'; -import { Storage } from '../storage'; -import { IndexMetaData } from './shared'; -import { VALUE_TYPES } from '../node-value-types'; -import { BlacklistingSearchOperator } from '../btree'; -import { IndexQueryStats } from './query-stats'; -import { FullTextIndexQueryHint } from './fulltext-index-query-hint'; -import unidecode from '../unidecode'; -import { assert } from '../assert'; - -class WordInfo { - constructor(public word: string, public indexes: number[], public sourceIndexes: number[]) { } - get occurs() { - return this.indexes.length; - } -} - -// const _wordsRegex = /[\w']+/gmi; // TODO: should use a better pattern that supports non-latin characters -class TextInfo { - static get locales() { - return { - 'default': { - pattern: '[A-Za-z0-9\']+', - flags: 'gmi', - }, - 'en': { - // English stoplist from https://gist.github.com/sebleier/554280 - stoplist: ['i', 'me', 'my', 'myself', 'we', 'our', 'ours', 'ourselves', 'you', 'your', 'yours', 'yourself', 'yourselves', 'he', 'him', 'his', 'himself', 'she', 'her', 'hers', 'herself', 'it', 'its', 'itself', 'they', 'them', 'their', 'theirs', 'themselves', 'what', 'which', 'who', 'whom', 'this', 'that', 'these', 'those', 'am', 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had', 'having', 'do', 'does', 'did', 'doing', 'a', 'an', 'the', 'and', 'but', 'if', 'or', 'because', 'as', 'until', 'while', 'of', 'at', 'by', 'for', 'with', 'about', 'against', 'between', 'into', 'through', 'during', 'before', 'after', 'above', 'below', 'to', 'from', 'up', 'down', 'in', 'out', 'on', 'off', 'over', 'under', 'again', 'further', 'then', 'once', 'here', 'there', 'when', 'where', 'why', 'how', 'all', 'any', 'both', 'each', 'few', 'more', 'most', 'other', 'some', 'such', 'no', 'nor', 'not', 'only', 'own', 'same', 'so', 'than', 'too', 'very', 's', 't', 'can', 'will', 'just', 'don', 'should', 'now'], - }, - get(locale: string) { - const settings = {} as { pattern?: string, flags?: string, stoplist?: string[] }; - Object.assign(settings, this.default); - if (typeof this[locale] === 'undefined' && locale.indexOf('-') > 0) { - locale = locale.split('-')[1]; - } - if (typeof this[locale] === 'undefined') { - return settings; - } - Object.keys(this[locale]).forEach(key => { - (settings as any)[key] = this[locale][key]; - }); - return settings; - }, - }; - } - - public locale: string; - public words: Map; // WordInfo[]; - public ignored: string[]; - - getWordInfo(word: string): WordInfo { - return this.words.get(word); - } - - /** - * Reconstructs an array of words in the order they were encountered - */ - toSequence() { - const arr = [] as string[]; - for (const { word, indexes } of this.words.values()) { - for (const index of indexes) { - arr[index] = word; - } - } - return arr; - } - - /** - * Returns all unique words in an array - */ - toArray() { - const arr = [] as string[]; - for (const word of this.words.keys()) { - arr.push(word); - } - return arr; - } - - get uniqueWordCount() { - return this.words.size; //.length; - } - - get wordCount() { - let total = 0; - for (const wordInfo of this.words.values()) { - total += wordInfo.occurs; - } - return total; - // return this.words.reduce((total, word) => total + word.occurs, 0); - } - - constructor(text: string, options?: { - /** - * Set the text locale to accurately convert words to lowercase - * @default "en" - */ - locale?: string; - - /** - * Overrides the default RegExp pattern used - * @default "[\w']+" - */ - pattern?: RegExp | string; - - /** - * Add characters to the word detection regular expression. Useful to keep wildcards such as * and ? in query texts - */ - includeChars?: string; - - /** - * Overrides the default RegExp flags (`gmi`) used - * @default "gmi" - */ - flags?: string; - - /** - * Optional callback functions that pre-processes the value before performing word splitting. - */ - prepare?: (value: any, locale: string, keepChars: string) => string; - - /** - * Optional callback function that is able to perform word stemming. Will be executed before performing criteria checks - */ - stemming?: (word:string, locale:string) => string; - - /** - * Minimum length of words to include - * @default 1 - */ - minLength?: number; - - /** - * Maximum length of words to include, should be increased if you expect words in your texts - * like "antidisestablishmentarianism" (28), "floccinaucinihilipilification" (29) or "pneumonoultramicroscopicsilicovolcanoconiosis" (45) - * @default 25 - */ - maxLength?: number; - - /** - * Words to ignore. You can use a default stoplist from TextInfo.locales - */ - blacklist?: string[]; - - /** - * Words to include even if they do not meet the min & maxLength criteria - */ - whitelist?: string[]; - - /** - * Whether to use a default stoplist to blacklist words (if available for locale) - * @default false - */ - useStoplist?: boolean; - }) { - // this.text = text; // Be gone later... - this.locale = options.locale || 'en'; - const localeSettings = TextInfo.locales.get(this.locale); - let pattern = localeSettings.pattern; - if (options.pattern && options.pattern instanceof RegExp) { - pattern = options.pattern.source; - } - else if (typeof options.pattern === 'string') { - pattern = options.pattern; - } - if (options.includeChars) { - assert(pattern.indexOf('[') >= 0, 'pattern does not contain []'); - let insert = ''; - for (let i = 0; i < options.includeChars.length; i++) { - insert += '\\' + options.includeChars[i]; - } - let pos = -1; - while(true) { - const index = pattern.indexOf('[', pos + 1) + 1; - if (index === 0) { break; } - pattern = pattern.slice(0, index) + insert + pattern.slice(index); - pos = index; - } - } - let flags = localeSettings.flags; - if (typeof options.flags === 'string') { - flags = options.flags; - } - const re = new RegExp(pattern, flags); - const minLength = typeof options.minLength === 'number' ? options.minLength : 1; - const maxLength = typeof options.maxLength === 'number' ? options.maxLength : 25; - let blacklist = options.blacklist instanceof Array ? options.blacklist : []; - if (localeSettings.stoplist instanceof Array && options.useStoplist === true) { - blacklist = blacklist.concat(localeSettings.stoplist); - } - const whitelist = options.whitelist instanceof Array ? options.whitelist : []; - - const words = this.words = new Map(); - this.ignored = []; - if (text === null || typeof text === 'undefined') { return; } - - if (options.prepare) { - // Pre-process text. Allows decompression, decrypting, custom stemming etc - text = options.prepare(text, this.locale, `"${options.includeChars ?? ''}`); - } - - // Unidecode text to get ASCII characters only - function safe_unidecode (str: string) { - // Fix for occasional multi-pass issue, copied from https://github.com/FGRibreau/node-unidecode/issues/16 - let ret; - while (str !== (ret = unidecode(str))) { - str = ret; - } - return ret; - } - text = safe_unidecode(text); - - // Remove any single quotes, so "don't" will be stored as "dont", "isn't" as "isnt" etc - text = text.replace(/'/g, ''); - - // Process the text - // const wordsRegex = /[\w']+/gu; - let wordIndex = 0; - while(true) { - const match = re.exec(text); - if (match === null) { break; } - let word = match[0]; - - // TODO: use stemming such as snowball (https://www.npmjs.com/package/snowball-stemmers) - // to convert words like "having" to "have", and "cycles", "cycle", "cycling" to "cycl" - if (typeof options.stemming === 'function') { - // Let callback function perform word stemming - const stemmed = options.stemming(word, this.locale); - if (typeof stemmed !== 'string') { - // Ignore this word - if (this.ignored.indexOf(word) < 0) { - this.ignored.push(word); - } - // Do not increase wordIndex - continue; - } - word = stemmed; - } - - word = word.toLocaleLowerCase(this.locale); - - if (word.length < minLength || ~blacklist.indexOf(word)) { - // Word does not meet set criteria - if (!~whitelist.indexOf(word)) { - // Not whitelisted either - if (this.ignored.indexOf(word) < 0) { - this.ignored.push(word); - } - // Do not increase wordIndex - continue; - } - } - else if (word.length > maxLength) { - // Use the word, but cut it to the max length - word = word.slice(0, maxLength); - } - - let wordInfo = words.get(word); - if (wordInfo) { - wordInfo.indexes.push(wordIndex); - wordInfo.sourceIndexes.push(match.index); - } - else { - wordInfo = new WordInfo(word, [wordIndex], [match.index]); - words.set(word, wordInfo); - } - wordIndex++; - } - } - -} - -export interface FullTextIndexOptions extends DataIndexOptions { - /** - * FullText configuration settings. - * NOTE: these settings are not stored in the index file because they contain callback functions - * that might not work after a (de)serializion cycle. Besides this, it is also better for security - * reasons not to store executable code in index files! - * - * That means that in order to keep fulltext indexes working as intended, you will have to: - * - call `db.indexes.create` for fulltext indexes each time your app starts, even if the index exists already - * - rebuild the index if you change this config. (pass `rebuild: true` in the index options) - */ - config?: { - /** - * callback function that prepares a text value for indexing. - * Useful to perform any actions on the text before it is split into words, such as: - * - transforming compressed / encrypted data to strings - * - perform custom word stemming: allows you to replace strings like `I've` to `I have` - * Important: do not remove any of the characters passed in `keepChars` (`"*?`)! - */ - prepare?: (value: any, locale: string, keepChars?: string) => string; - - /** - * callback function that transforms (or filters) words being indexed - */ - transform?: (word: string, locale:string) => string; - - /** - * words to be ignored - */ - blacklist?: string[]; - - /** - * Uses a locale specific stoplist to automatically blacklist words - * @default true - */ - useStoplist?: boolean; - - /** - * Words to be included if they did not match other criteria - */ - whitelist?: string[]; - - /** - * Uses the value of a specific key as locale. Allows different languages to be indexed correctly, - * overrides options.textLocale - * @deprecated move to options.textLocaleKey - */ - localeKey?: string; - - /** - * Minimum length for words to be indexed (after transform) - */ - minLength?: number; - - /** - * Maximum length for words to be indexed (after transform) - */ - maxLength?: number; - } -} - -export interface FullTextContainsQueryOptions { - /** - * Locale to use for the words in the query. When omitted, the default index locale is used - */ - locale?: string; - - /** - * Used internally: treats the words in val as a phrase, eg: "word1 word2 word3": words need to occur in this exact order - */ - phrase?: boolean; - - /** - * Sets minimum amount of characters that have to be used for wildcard (sub)queries such as "a%" to guard the - * system against extremely large result sets. Length does not include the wildcard characters itself. Default - * value is 2 (allows "an*" but blocks "a*") - * @default 2 - */ - minimumWildcardWordLength?: number; -} - -/** - * A full text index allows all words in text nodes to be indexed and searched. - * Eg: "Every word in this text must be indexed." will be indexed with every word - * and can be queried with filters 'contains' and '!contains' a word, words or pattern. - * Eg: 'contains "text"', 'contains "text indexed"', 'contains "text in*"' will all match the text above. - * This does not use a thesauris or word lists (yet), so 'contains "query"' will not match. - * Each word will be stored and searched in lowercase - */ -export class FullTextIndex extends DataIndex { - - public config: FullTextIndexOptions['config']; - - constructor(storage: Storage, path: string, key: string, options: FullTextIndexOptions) { - if (key === '{key}') { throw new Error('Cannot create fulltext index on node keys'); } - super(storage, path, key, options); - // this.enableReverseLookup = true; - this.indexMetadataKeys = ['_occurs_']; //,'_indexes_' - this.config = options.config || {}; - if (this.config.localeKey) { - // localeKey is supported by all indexes now - this.logger.warn(`fulltext index config option "localeKey" has been deprecated, as it is now supported for all indexes. Move the setting to the global index settings`); - this.textLocaleKey = this.config.localeKey; // Do use it now - } - } - - // get fileName() { - // return super.fileName.slice(0, -4) + '.fulltext.idx'; - // } - - get type() { - return 'fulltext'; - } - - getTextInfo(val: string, locale?: string) { - return new TextInfo(val, { - locale: locale ?? this.textLocale, - prepare: this.config.prepare, - stemming: this.config.transform, - blacklist: this.config.blacklist, - whitelist: this.config.whitelist, - useStoplist: this.config.useStoplist, - minLength: this.config.minLength, - maxLength: this.config.maxLength, - }); - } - - test(obj: any, op: 'fulltext:contains' | 'fulltext:!contains', val: string): boolean { - if (obj === null) { return op === 'fulltext:!contains'; } - const text = obj[this.key]; - if (typeof text === 'undefined') { return op === 'fulltext:!contains'; } - - const locale = obj?.[this.textLocaleKey] ?? this.textLocale; - const textInfo = this.getTextInfo(text, locale); - if (op === 'fulltext:contains') { - if (~val.indexOf(' OR ')) { - // split - const tests = val.split(' OR '); - return tests.some(val => this.test(text, op, val)); - } - else if (~val.indexOf('"')) { - // Phrase(s) used. We have to make sure the words used are not only in the text, - // but also in that exact order. - const phraseRegex = /"(.+?)"/g; - const phrases = []; - while (true) { - const match = phraseRegex.exec(val); - if (match === null) { break; } - const phrase = match[1]; - phrases.push(phrase); - val = val.slice(0, match.index) + val.slice(match.index + match[0].length); - phraseRegex.lastIndex = 0; - } - if (val.length > 0) { - phrases.push(val); - } - return phrases.every(phrase => { - const phraseInfo = this.getTextInfo(phrase, locale); - - // This was broken before TS port because WordInfo had an array of words that was not - // in the same order as the source words were. - // TODO: Thoroughly test this new code - const phraseWords = phraseInfo.toSequence(); - const occurrencesPerWord = phraseWords.map((word, i) => { - // Find word in text - const { indexes } = textInfo.words.get(word); - return indexes; - }); - const hasSequenceAtIndex = (wordIndex: number, occurrenceIndex: number): boolean => { - const startIndex = occurrencesPerWord[wordIndex]?.[occurrenceIndex]; - return occurrencesPerWord.slice(wordIndex + 1).every((occurences, i) => { - return occurences.some((index, j) => { - if (index !== startIndex + 1) { return false; } - return hasSequenceAtIndex(wordIndex + i, j); - }); - }); - }; - - // Find the existence of a sequence of words - // Loop: for each occurrence of the first word in text, remember its index - // Try to find second word in text with index+1 - // - found: try to find third word in text with index+2, etc (recursive) - // - not found: stop, proceed with next occurrence in main loop - return occurrencesPerWord[0].some((occurrence, i) => { - return hasSequenceAtIndex(0, i); - }); - - // const indexes = phraseInfo.words.map(word => textInfo.words.indexOf(word)); - // if (indexes[0] < 0) { return false; } - // for (let i = 1; i < indexes.length; i++) { - // if (indexes[i] - indexes[i-1] !== 1) { - // return false; - // } - // } - // return true; - }); - } - else { - // test 1 or more words - const wordsInfo = this.getTextInfo(val, locale); - return wordsInfo.toSequence().every(word => { - return textInfo.words.has(word); - }); - } - } - } - - async handleRecordUpdate(path: string, oldValue: any, newValue: any): Promise { - let oldText = oldValue !== null && typeof oldValue === 'object' && this.key in oldValue ? (oldValue as any)[this.key] : null; - let newText = newValue !== null && typeof newValue === 'object' && this.key in newValue ? (newValue as any)[this.key] : null; - - const oldLocale = oldValue?.[this.textLocaleKey] ?? this.textLocale, - newLocale = newValue?.[this.textLocaleKey] ?? this.textLocale; - - if (typeof oldText === 'object' && oldText instanceof Array) { - oldText = oldText.join(' '); - } - if (typeof newText === 'object' && newText instanceof Array) { - newText = newText.join(' '); - } - - const oldTextInfo = this.getTextInfo(oldText, oldLocale); - const newTextInfo = this.getTextInfo(newText, newLocale); - - // super._updateReverseLookupKey( - // path, - // oldText ? textEncoder.encode(oldText) : null, - // newText ? textEncoder.encode(newText) : null, - // metadata - // ); - - const oldWords = oldTextInfo.toArray(); //.words.map(w => w.word); - const newWords = newTextInfo.toArray(); //.words.map(w => w.word); - - const removed = oldWords.filter(word => newWords.indexOf(word) < 0); - const added = newWords.filter(word => oldWords.indexOf(word) < 0); - const changed = oldWords.filter(word => newWords.indexOf(word) >= 0).filter(word => { - const oldInfo = oldTextInfo.getWordInfo(word); - const newInfo = newTextInfo.getWordInfo(word); - return oldInfo.occurs !== newInfo.occurs || oldInfo.indexes.some((index, i) => newInfo.indexes[i] !== index); - }); - changed.forEach(word => { - // Word metadata changed. Simplest solution: remove and add again - removed.push(word); - added.push(word); - }); - const promises = [] as Promise[]; - // TODO: Prepare operations batch, then execute 1 tree update. - // Now every word is a seperate update which is not necessary! - removed.forEach(word => { - const p = super.handleRecordUpdate(path, { [this.key]: word }, { [this.key]: null }); - promises.push(p); - }); - added.forEach(word => { - const mutated: Record = { }; - Object.assign(mutated, newValue); - mutated[this.key] = word; - - const wordInfo = newTextInfo.getWordInfo(word); - // const indexMetadata = { - // '_occurs_': wordInfo.occurs, - // '_indexes_': wordInfo.indexes.join(',') - // }; - - let occurs = wordInfo.indexes.join(','); - if (occurs.length > 255) { - console.warn(`FullTextIndex ${this.description}: word "${word}" occurs too many times in "${path}/${this.key}" to store in index metadata. Truncating occurrences`); - const cutIndex = occurs.lastIndexOf(',', 255); - occurs = occurs.slice(0, cutIndex); - } - const indexMetadata = { - '_occurs_': occurs, - }; - const p = super.handleRecordUpdate(path, { [this.key]: null }, mutated, indexMetadata); - promises.push(p); - }); - await Promise.all(promises); - } - - build() { - return super.build({ - addCallback: (add, text: string | string[], recordPointer, metadata, env) => { - if (typeof text === 'object' && text instanceof Array) { - text = text.join(' '); - } - if (typeof text === 'undefined') { - text = ''; - } - const locale = env.locale || this.textLocale; - const textInfo = this.getTextInfo(text, locale); - if (textInfo.words.size === 0) { - this.logger.warn(`No words found in "${typeof text === 'string' && text.length > 50 ? text.slice(0, 50) + '...' : text}" to fulltext index "${env.path}"`); - } - - // const revLookupKey = super._getRevLookupKey(env.path); - // tree.add(revLookupKey, textEncoder.encode(text), metadata); - - textInfo.words.forEach(wordInfo => { - - // IDEA: To enable fast '*word' queries (starting with wildcard), we can also store - // reversed words and run reversed query 'drow*' on it. we'd have to enable storing - // multiple B+Trees in a single index file: a 'forward' tree & a 'reversed' tree - - // IDEA: Following up on previous idea: being able to backtrack nodes within an index would - // help to speed up sorting queries on an indexed key, - // eg: query .take(10).filter('rating','>=', 8).sort('title') - // does not filter on key 'title', but can then use an index on 'title' for the sorting: - // it can take the results from the 'rating' index and backtrack the nodes' titles to quickly - // get a sorted top 10. We'd have to store a seperate 'backtrack' tree that uses recordPointers - // as the key, and 'title' values as recordPointers. Caveat: max string length for sorting would - // then be 255 ASCII chars, because that's the recordPointer size limit. - // The same boost can currently only be achieved by creating an index that includes 'title' in - // the index on 'rating' ==> db.indexes.create('movies', 'rating', { include: ['title'] }) - - // Extend metadata with more details about the word (occurrences, positions) - // const wordMetadata = { - // '_occurs_': wordInfo.occurs, - // '_indexes_': wordInfo.indexes.join(',') - // }; - - let occurs = wordInfo.indexes.join(','); - if (occurs.length > 255) { - console.warn(`FullTextIndex ${this.description}: word "${wordInfo.word}" occurs too many times to store in index metadata. Truncating occurrences`); - const cutIndex = occurs.lastIndexOf(',', 255); - occurs = occurs.slice(0, cutIndex); - } - const wordMetadata: IndexMetaData = { - '_occurs_': occurs, - }; - Object.assign(wordMetadata, metadata); - add(wordInfo.word, recordPointer, wordMetadata); - }); - return textInfo.toArray(); //words.map(info => info.word); - }, - valueTypes: [VALUE_TYPES.STRING], - }); - } - - static get validOperators() { - return ['fulltext:contains', 'fulltext:!contains']; - } - get validOperators() { - return FullTextIndex.validOperators; - } - - async query(op: string | BlacklistingSearchOperator, val?: string, options?: any) { - if (op instanceof BlacklistingSearchOperator) { - throw new Error(`Not implemented: Can't query fulltext index with blacklisting operator yet`); - } - if (op === 'fulltext:contains' || op === 'fulltext:!contains') { - return this.contains(op, val, options); - } - else { - throw new Error(`Fulltext indexes can only be queried with operators ${FullTextIndex.validOperators.map(op => `"${op}"`).join(', ')}`); - } - } - - /** - * - * @param op Operator to use, can be either "fulltext:contains" or "fulltext:!contains" - * @param val Text to search for. Can include * and ? wildcards, OR's for combined searches, and "quotes" for phrase searches - */ - async contains(op: 'fulltext:contains' | 'fulltext:!contains', val: string, options: FullTextContainsQueryOptions = { - phrase: false, - locale: undefined, - minimumWildcardWordLength: 2, - }): Promise { - if (!FullTextIndex.validOperators.includes(op)) { //if (op !== 'fulltext:contains' && op !== 'fulltext:not_contains') { - throw new Error(`Fulltext indexes can only be queried with operators ${FullTextIndex.validOperators.map(op => `"${op}"`).join(', ')}`); - } - - // Check cache - const cache = this.cache(op, val); - if (cache) { - // Use cached results - return Promise.resolve(cache); - } - - const stats = new IndexQueryStats(options.phrase ? 'fulltext_phrase_query' : 'fulltext_query', val, true); - - // const searchWordRegex = /[\w'?*]+/g; // Use TextInfo to find and transform words using index settings - const getTextInfo = (text: string) => { - const info = new TextInfo(text, { - locale: options.locale || this.textLocale, - prepare: this.config.prepare, - stemming: this.config.transform, - minLength: this.config.minLength, - maxLength: this.config.maxLength, - blacklist: this.config.blacklist, - whitelist: this.config.whitelist, - useStoplist: this.config.useStoplist, - includeChars: '*?', - }); - - // Ignore any wildcard words that do not meet the set minimum length - // This is to safeguard the system against (possibly unwanted) very large - // result sets - const words = info.toArray(); - let i; - while (i = words.findIndex(w => /^[*?]+$/.test(w)), i >= 0) { - // Word is wildcards only. Ignore - const word = words[i]; - info.ignored.push(word); - info.words.delete(word); - } - - if (options.minimumWildcardWordLength > 0) { - for (const word of words) { - const starIndex = word.indexOf('*'); - // min = 2, word = 'an*', starIndex = 2, ok! - // min = 3: starIndex < min: not ok! - if (starIndex > 0 && starIndex < options.minimumWildcardWordLength) { - info.ignored.push(word); - info.words.delete(word); - i--; - } - } - } - return info; - }; - - if (val.includes(' OR ')) { - // Multiple searches in one query: 'secret OR confidential OR "don't tell"' - // TODO: chain queries instead of running simultanious? - const queries = val.split(' OR '); - const promises = queries.map(q => this.query(op, q, options)); - const resultSets = await Promise.all(promises); - stats.steps.push(...resultSets.map(results => results.stats)); - - const mergeStep = new IndexQueryStats('merge_expand', { sets: resultSets.length, results: resultSets.reduce((total, set) => total + set.length, 0) }, true); - stats.steps.push(mergeStep); - - const merged = resultSets[0]; - resultSets.slice(1).forEach(results => { - results.forEach(result => { - const exists = ~merged.findIndex(r => r.path === result.path); - if (!exists) { merged.push(result); } - }); - }); - const results = IndexQueryResults.fromResults(merged, this.key); - mergeStep.stop(results.length); - - stats.stop(results.length); - results.stats = stats; - results.hints.push(...resultSets.reduce((hints, set) => { hints.push(...set.hints); return hints; }, [])); - return results; - } - if (val.includes('"')) { - // Phrase(s) used. We have to make sure the words used are not only in the text, - // but also in that exact order. - const phraseRegex = /"(.+?)"/g; - const phrases = []; - while (true) { - const match = phraseRegex.exec(val); - if (match === null) { break; } - const phrase = match[1]; - phrases.push(phrase); - val = val.slice(0, match.index) + val.slice(match.index + match[0].length); - phraseRegex.lastIndex = 0; - } - - const phraseOptions: typeof options = {}; - Object.assign(phraseOptions, options); - phraseOptions.phrase = true; - const promises = phrases.map(phrase => this.query(op, phrase, phraseOptions)); - - // Check if what is left over still contains words - if (val.length > 0 && getTextInfo(val).wordCount > 0) { //(val.match(searchWordRegex) !== null) { - // Add it - const promise = this.query(op, val, options); - promises.push(promise); - } - - const resultSets = await Promise.all(promises); - stats.steps.push(...resultSets.map(results => results.stats)); - - // Take shortest set, only keep results that are matched in all other sets - const mergeStep = new IndexQueryStats('merge_reduce', { sets: resultSets.length, results: resultSets.reduce((total, set) => total + set.length, 0) }, true); - resultSets.length > 1 && stats.steps.push(mergeStep); - - const shortestSet = resultSets.sort((a,b) => a.length < b.length ? -1 : 1)[0]; - const otherSets = resultSets.slice(1); - const matches = shortestSet.reduce((matches, match) => { - // Check if the key is present in the other result sets - const path = match.path; - const matchedInAllSets = otherSets.every(set => set.findIndex(match => match.path === path) >= 0); - if (matchedInAllSets) { matches.push(match); } - return matches; - }, new IndexQueryResults()); - matches.filterKey = this.key; - mergeStep.stop(matches.length); - - stats.stop(matches.length); - matches.stats = stats; - matches.hints.push(...resultSets.reduce((hints, set) => { hints.push(...set.hints); return hints; }, [])); - return matches; - } - - const info = getTextInfo(val); - - /** - * Add ignored words to the result hints - */ - function addIgnoredWordHints(results: IndexQueryResults) { - // Add hints for ignored words - info.ignored.forEach(word => { - const hint = new FullTextIndexQueryHint(FullTextIndexQueryHint.types.ignoredWord, word); - results.hints.push(hint); - }); - } - - const words = info.toArray(); - if (words.length === 0) { - // Resolve with empty array - stats.stop(0); - const results = IndexQueryResults.fromResults([], this.key); - results.stats = stats; - addIgnoredWordHints(results); - return results; - } - - if (op === 'fulltext:!contains') { - // NEW: Use BlacklistingSearchOperator that uses all (unique) values in the index, - // besides the ones that get blacklisted along the way by our callback function - const wordChecks = words.map(word => { - if (word.includes('*') || word.includes('?')) { - const pattern = '^' + word.replace(/\*/g, '.*').replace(/\?/g, '.') + '$'; - const re = new RegExp(pattern, 'i'); - return re; - } - return word; - }); - const customOp = new BlacklistingSearchOperator(entry => { - const blacklist = wordChecks.some(word => { - if (word instanceof RegExp) { - return word.test(entry.key as string); - } - return entry.key === word; - }); - if (blacklist) { return entry.values; } - }); - - stats.type = 'fulltext_blacklist_scan'; - const results = await super.query(customOp); - stats.stop(results.length); - results.filterKey = this.key; - results.stats = stats; - addIgnoredWordHints(results); - - // Cache results - this.cache(op, val, results); - return results; - } - - // op === 'fulltext:contains' - // Get result count for each word - const countPromises = words.map(word => { - const wildcardIndex = ~(~word.indexOf('*') || ~word.indexOf('?')); // TODO: improve readability - const wordOp = wildcardIndex >= 0 ? 'like' : '=='; - const step = new IndexQueryStats('count', { op: wordOp, word }, true); - stats.steps.push(step); - return super.count(wordOp, word) - .then(count => { - step.stop(count); - return { word, count }; - }); - }); - const counts = await Promise.all(countPromises); - // Start with the smallest result set - counts.sort((a, b) => { - if (a.count < b.count) { return -1; } - else if (a.count > b.count) { return 1; } - return 0; - }); - - let results: IndexQueryResults; - - if (counts[0].count === 0) { - stats.stop(0); - - this.logger.info(`Word "${counts[0].word}" not found in index, 0 results for query ${op} "${val}"`); - results = new IndexQueryResults(0); - results.filterKey = this.key; - results.stats = stats; - addIgnoredWordHints(results); - - // Add query hints for each unknown word - counts.forEach(c => { - if (c.count === 0) { - const hint = new FullTextIndexQueryHint(FullTextIndexQueryHint.types.missingWord, c.word); - results.hints.push(hint); - } - }); - - // Cache the empty result set - this.cache(op, val, results); - return results; - } - const allWords = counts.map(c => c.word); - - // Sequentual method: query 1 word, then filter results further and further - // More or less performs the same as parallel, but uses less memory - // NEW: Start with the smallest result set - - // OLD: Use the longest word to search with, then filter those results - // const allWords = words.slice().sort((a,b) => { - // if (a.length < b.length) { return 1; } - // else if (a.length > b.length) { return -1; } - // return 0; - // }); - - const queryWord = async (word: string, filter: IndexQueryResults) => { - const wildcardIndex = ~(~word.indexOf('*') || ~word.indexOf('?')); // TODO: improve readability - const wordOp = wildcardIndex >= 0 ? 'like' : '=='; - // const step = new IndexQueryStats('query', { op: wordOp, word }, true); - // stats.steps.push(step); - const results = await super.query(wordOp, word, { filter }); - stats.steps.push(results.stats); - // step.stop(results.length); - return results; - }; - let wordIndex = 0; - const resultsPerWord: IndexQueryResults[] = new Array(words.length); - const nextWord = async () => { - const word = allWords[wordIndex]; - const t1 = Date.now(); - const fr = await queryWord(word, results); - const t2 = Date.now(); - this.logger.info(`fulltext search for "${word}" took ${t2-t1}ms`); - resultsPerWord[words.indexOf(word)] = fr; - results = fr; - wordIndex++; - if (results.length === 0 || wordIndex === allWords.length) { return; } - await nextWord(); - }; - await nextWord(); - - type MetaDataWithOccursArray = IndexMetaData & { _occurs_: number[] }; - - if (options.phrase === true && allWords.length > 1) { - // Check which results have the words in the right order - const step = new IndexQueryStats('phrase_check', val, true); - stats.steps.push(step); - results = results.reduce((matches, match) => { - // the order of the resultsPerWord is in the same order as the given words, - // check if their metadata._occurs_ say the same about the indexed content - const path = match.path; - const wordMatches = resultsPerWord.map(results => { - return results.find(result => result.path === path); - }); - // Convert the _occurs_ strings to arrays we can use - wordMatches.forEach(match => { - (match.metadata as MetaDataWithOccursArray)._occurs_ = (match.metadata._occurs_ as string).split(',').map(parseInt); - }); - const check = (wordMatchIndex: number, prevWordIndex?: number): boolean => { - const sourceIndexes = (wordMatches[wordMatchIndex].metadata as MetaDataWithOccursArray)._occurs_; - if (typeof prevWordIndex !== 'number') { - // try with each sourceIndex of the first word - for (let i = 0; i < sourceIndexes.length; i++) { - const found = check(1, sourceIndexes[i]); - if (found) { return true; } - } - return false; - } - // We're in a recursive call on the 2nd+ word - if (sourceIndexes.includes(prevWordIndex + 1)) { - // This word came after the previous word, hooray! - // Proceed with next word, or report success if this was the last word to check - if (wordMatchIndex === wordMatches.length-1) { return true; } - return check(wordMatchIndex+1, prevWordIndex+1); - } - else { - return false; - } - }; - if (check(0)) { - matches.push(match); // Keep! - } - return matches; - }, new IndexQueryResults()); - step.stop(results.length); - } - results.filterKey = this.key; - - stats.stop(results.length); - results.stats = stats; - addIgnoredWordHints(results); - - // Cache results - delete results.entryValues; // No need to cache these. Free the memory - this.cache(op, val, results); - return results; - - // Parallel method: query all words at the same time, then combine results - // Uses more memory - // const promises = words.map(word => { - // const wildcardIndex = ~(~word.indexOf('*') || ~word.indexOf('?')); - // let wordOp; - // if (op === 'fulltext:contains') { - // wordOp = wildcardIndex >= 0 ? 'like' : '=='; - // } - // else if (op === 'fulltext:!contains') { - // wordOp = wildcardIndex >= 0 ? '!like' : '!='; - // } - // // return super.query(wordOp, word) - // return super.query(wordOp, word) - // }); - // return Promise.all(promises) - // .then(resultSets => { - // // Now only use matches that exist in all result sets - // const sortedSets = resultSets.slice().sort((a,b) => a.length < b.length ? -1 : 1) - // const shortestSet = sortedSets[0]; - // const otherSets = sortedSets.slice(1); - // let matches = shortestSet.reduce((matches, match) => { - // // Check if the key is present in the other result sets - // const path = match.path; - // const matchedInAllSets = otherSets.every(set => set.findIndex(match => match.path === path) >= 0); - // if (matchedInAllSets) { matches.push(match); } - // return matches; - // }, new IndexQueryResults()); - - // if (options.phrase === true && resultSets.length > 1) { - // // Check if the words are in the right order - // console.log(`Breakpoint time`); - // matches = matches.reduce((matches, match) => { - // // the order of the resultSets is in the same order as the given words, - // // check if their metadata._indexes_ say the same about the indexed content - // const path = match.path; - // const wordMatches = resultSets.map(set => { - // return set.find(match => match.path === path); - // }); - // // Convert the _indexes_ strings to arrays we can use - // wordMatches.forEach(match => { - // // match.metadata._indexes_ = match.metadata._indexes_.split(',').map(parseInt); - // match.metadata._occurs_ = match.metadata._occurs_.split(',').map(parseInt); - // }); - // const check = (wordMatchIndex, prevWordIndex) => { - // const sourceIndexes = wordMatches[wordMatchIndex].metadata._occurs_; //wordMatches[wordMatchIndex].metadata._indexes_; - // if (typeof prevWordIndex !== 'number') { - // // try with each sourceIndex of the first word - // for (let i = 0; i < sourceIndexes.length; i++) { - // const found = check(1, sourceIndexes[i]); - // if (found) { return true; } - // } - // return false; - // } - // // We're in a recursive call on the 2nd+ word - // if (~sourceIndexes.indexOf(prevWordIndex + 1)) { - // // This word came after the previous word, hooray! - // // Proceed with next word, or report success if this was the last word to check - // if (wordMatchIndex === wordMatches.length-1) { return true; } - // return check(wordMatchIndex+1, prevWordIndex+1); - // } - // else { - // return false; - // } - // } - // if (check(0)) { - // matches.push(match); // Keep! - // } - // return matches; - // }, new IndexQueryResults()); - // } - // matches.filterKey = this.key; - // return matches; - // }); - } -} +import { DataIndex } from './data-index.js'; +import { DataIndexOptions } from './options.js'; +import { IndexQueryResults } from './query-results.js'; +import { Storage } from '../storage/index.js'; +import { IndexMetaData } from './shared.js'; +import { VALUE_TYPES } from '../node-value-types.js'; +import { BlacklistingSearchOperator } from '../btree/index.js'; +import { IndexQueryStats } from './query-stats.js'; +import { FullTextIndexQueryHint } from './fulltext-index-query-hint.js'; +import unidecode from '../unidecode.js'; +import { assert } from '../assert.js'; + +class WordInfo { + constructor(public word: string, public indexes: number[], public sourceIndexes: number[]) { } + get occurs() { + return this.indexes.length; + } +} + +// const _wordsRegex = /[\w']+/gmi; // TODO: should use a better pattern that supports non-latin characters +class TextInfo { + static get locales() { + return { + 'default': { + pattern: '[A-Za-z0-9\']+', + flags: 'gmi', + }, + 'en': { + // English stoplist from https://gist.github.com/sebleier/554280 + stoplist: ['i', 'me', 'my', 'myself', 'we', 'our', 'ours', 'ourselves', 'you', 'your', 'yours', 'yourself', 'yourselves', 'he', 'him', 'his', 'himself', 'she', 'her', 'hers', 'herself', 'it', 'its', 'itself', 'they', 'them', 'their', 'theirs', 'themselves', 'what', 'which', 'who', 'whom', 'this', 'that', 'these', 'those', 'am', 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had', 'having', 'do', 'does', 'did', 'doing', 'a', 'an', 'the', 'and', 'but', 'if', 'or', 'because', 'as', 'until', 'while', 'of', 'at', 'by', 'for', 'with', 'about', 'against', 'between', 'into', 'through', 'during', 'before', 'after', 'above', 'below', 'to', 'from', 'up', 'down', 'in', 'out', 'on', 'off', 'over', 'under', 'again', 'further', 'then', 'once', 'here', 'there', 'when', 'where', 'why', 'how', 'all', 'any', 'both', 'each', 'few', 'more', 'most', 'other', 'some', 'such', 'no', 'nor', 'not', 'only', 'own', 'same', 'so', 'than', 'too', 'very', 's', 't', 'can', 'will', 'just', 'don', 'should', 'now'], + }, + get(locale: string) { + const settings = {} as { pattern?: string, flags?: string, stoplist?: string[] }; + Object.assign(settings, this.default); + if (typeof this[locale] === 'undefined' && locale.indexOf('-') > 0) { + locale = locale.split('-')[1]; + } + if (typeof this[locale] === 'undefined') { + return settings; + } + Object.keys(this[locale]).forEach(key => { + (settings as any)[key] = this[locale][key]; + }); + return settings; + }, + }; + } + + public locale: string; + public words: Map; // WordInfo[]; + public ignored: string[]; + + getWordInfo(word: string): WordInfo { + return this.words.get(word); + } + + /** + * Reconstructs an array of words in the order they were encountered + */ + toSequence() { + const arr = [] as string[]; + for (const { word, indexes } of this.words.values()) { + for (const index of indexes) { + arr[index] = word; + } + } + return arr; + } + + /** + * Returns all unique words in an array + */ + toArray() { + const arr = [] as string[]; + for (const word of this.words.keys()) { + arr.push(word); + } + return arr; + } + + get uniqueWordCount() { + return this.words.size; //.length; + } + + get wordCount() { + let total = 0; + for (const wordInfo of this.words.values()) { + total += wordInfo.occurs; + } + return total; + // return this.words.reduce((total, word) => total + word.occurs, 0); + } + + constructor(text: string, options?: { + /** + * Set the text locale to accurately convert words to lowercase + * @default "en" + */ + locale?: string; + + /** + * Overrides the default RegExp pattern used + * @default "[\w']+" + */ + pattern?: RegExp | string; + + /** + * Add characters to the word detection regular expression. Useful to keep wildcards such as * and ? in query texts + */ + includeChars?: string; + + /** + * Overrides the default RegExp flags (`gmi`) used + * @default "gmi" + */ + flags?: string; + + /** + * Optional callback functions that pre-processes the value before performing word splitting. + */ + prepare?: (value: any, locale: string, keepChars: string) => string; + + /** + * Optional callback function that is able to perform word stemming. Will be executed before performing criteria checks + */ + stemming?: (word:string, locale:string) => string; + + /** + * Minimum length of words to include + * @default 1 + */ + minLength?: number; + + /** + * Maximum length of words to include, should be increased if you expect words in your texts + * like "antidisestablishmentarianism" (28), "floccinaucinihilipilification" (29) or "pneumonoultramicroscopicsilicovolcanoconiosis" (45) + * @default 25 + */ + maxLength?: number; + + /** + * Words to ignore. You can use a default stoplist from TextInfo.locales + */ + blacklist?: string[]; + + /** + * Words to include even if they do not meet the min & maxLength criteria + */ + whitelist?: string[]; + + /** + * Whether to use a default stoplist to blacklist words (if available for locale) + * @default false + */ + useStoplist?: boolean; + }) { + // this.text = text; // Be gone later... + this.locale = options.locale || 'en'; + const localeSettings = TextInfo.locales.get(this.locale); + let pattern = localeSettings.pattern; + if (options.pattern && options.pattern instanceof RegExp) { + pattern = options.pattern.source; + } + else if (typeof options.pattern === 'string') { + pattern = options.pattern; + } + if (options.includeChars) { + assert(pattern.indexOf('[') >= 0, 'pattern does not contain []'); + let insert = ''; + for (let i = 0; i < options.includeChars.length; i++) { + insert += '\\' + options.includeChars[i]; + } + let pos = -1; + while(true) { + const index = pattern.indexOf('[', pos + 1) + 1; + if (index === 0) { break; } + pattern = pattern.slice(0, index) + insert + pattern.slice(index); + pos = index; + } + } + let flags = localeSettings.flags; + if (typeof options.flags === 'string') { + flags = options.flags; + } + const re = new RegExp(pattern, flags); + const minLength = typeof options.minLength === 'number' ? options.minLength : 1; + const maxLength = typeof options.maxLength === 'number' ? options.maxLength : 25; + let blacklist = options.blacklist instanceof Array ? options.blacklist : []; + if (localeSettings.stoplist instanceof Array && options.useStoplist === true) { + blacklist = blacklist.concat(localeSettings.stoplist); + } + const whitelist = options.whitelist instanceof Array ? options.whitelist : []; + + const words = this.words = new Map(); + this.ignored = []; + if (text === null || typeof text === 'undefined') { return; } + + if (options.prepare) { + // Pre-process text. Allows decompression, decrypting, custom stemming etc + text = options.prepare(text, this.locale, `"${options.includeChars ?? ''}`); + } + + // Unidecode text to get ASCII characters only + function safe_unidecode (str: string) { + // Fix for occasional multi-pass issue, copied from https://github.com/FGRibreau/node-unidecode/issues/16 + let ret; + while (str !== (ret = unidecode(str))) { + str = ret; + } + return ret; + } + text = safe_unidecode(text); + + // Remove any single quotes, so "don't" will be stored as "dont", "isn't" as "isnt" etc + text = text.replace(/'/g, ''); + + // Process the text + // const wordsRegex = /[\w']+/gu; + let wordIndex = 0; + while(true) { + const match = re.exec(text); + if (match === null) { break; } + let word = match[0]; + + // TODO: use stemming such as snowball (https://www.npmjs.com/package/snowball-stemmers) + // to convert words like "having" to "have", and "cycles", "cycle", "cycling" to "cycl" + if (typeof options.stemming === 'function') { + // Let callback function perform word stemming + const stemmed = options.stemming(word, this.locale); + if (typeof stemmed !== 'string') { + // Ignore this word + if (this.ignored.indexOf(word) < 0) { + this.ignored.push(word); + } + // Do not increase wordIndex + continue; + } + word = stemmed; + } + + word = word.toLocaleLowerCase(this.locale); + + if (word.length < minLength || ~blacklist.indexOf(word)) { + // Word does not meet set criteria + if (!~whitelist.indexOf(word)) { + // Not whitelisted either + if (this.ignored.indexOf(word) < 0) { + this.ignored.push(word); + } + // Do not increase wordIndex + continue; + } + } + else if (word.length > maxLength) { + // Use the word, but cut it to the max length + word = word.slice(0, maxLength); + } + + let wordInfo = words.get(word); + if (wordInfo) { + wordInfo.indexes.push(wordIndex); + wordInfo.sourceIndexes.push(match.index); + } + else { + wordInfo = new WordInfo(word, [wordIndex], [match.index]); + words.set(word, wordInfo); + } + wordIndex++; + } + } + +} + +export interface FullTextIndexOptions extends DataIndexOptions { + /** + * FullText configuration settings. + * NOTE: these settings are not stored in the index file because they contain callback functions + * that might not work after a (de)serializion cycle. Besides this, it is also better for security + * reasons not to store executable code in index files! + * + * That means that in order to keep fulltext indexes working as intended, you will have to: + * - call `db.indexes.create` for fulltext indexes each time your app starts, even if the index exists already + * - rebuild the index if you change this config. (pass `rebuild: true` in the index options) + */ + config?: { + /** + * callback function that prepares a text value for indexing. + * Useful to perform any actions on the text before it is split into words, such as: + * - transforming compressed / encrypted data to strings + * - perform custom word stemming: allows you to replace strings like `I've` to `I have` + * Important: do not remove any of the characters passed in `keepChars` (`"*?`)! + */ + prepare?: (value: any, locale: string, keepChars?: string) => string; + + /** + * callback function that transforms (or filters) words being indexed + */ + transform?: (word: string, locale:string) => string; + + /** + * words to be ignored + */ + blacklist?: string[]; + + /** + * Uses a locale specific stoplist to automatically blacklist words + * @default true + */ + useStoplist?: boolean; + + /** + * Words to be included if they did not match other criteria + */ + whitelist?: string[]; + + /** + * Uses the value of a specific key as locale. Allows different languages to be indexed correctly, + * overrides options.textLocale + * @deprecated move to options.textLocaleKey + */ + localeKey?: string; + + /** + * Minimum length for words to be indexed (after transform) + */ + minLength?: number; + + /** + * Maximum length for words to be indexed (after transform) + */ + maxLength?: number; + } +} + +export interface FullTextContainsQueryOptions { + /** + * Locale to use for the words in the query. When omitted, the default index locale is used + */ + locale?: string; + + /** + * Used internally: treats the words in val as a phrase, eg: "word1 word2 word3": words need to occur in this exact order + */ + phrase?: boolean; + + /** + * Sets minimum amount of characters that have to be used for wildcard (sub)queries such as "a%" to guard the + * system against extremely large result sets. Length does not include the wildcard characters itself. Default + * value is 2 (allows "an*" but blocks "a*") + * @default 2 + */ + minimumWildcardWordLength?: number; +} + +/** + * A full text index allows all words in text nodes to be indexed and searched. + * Eg: "Every word in this text must be indexed." will be indexed with every word + * and can be queried with filters 'contains' and '!contains' a word, words or pattern. + * Eg: 'contains "text"', 'contains "text indexed"', 'contains "text in*"' will all match the text above. + * This does not use a thesauris or word lists (yet), so 'contains "query"' will not match. + * Each word will be stored and searched in lowercase + */ +export class FullTextIndex extends DataIndex { + + public config: FullTextIndexOptions['config']; + + constructor(storage: Storage, path: string, key: string, options: FullTextIndexOptions) { + if (key === '{key}') { throw new Error('Cannot create fulltext index on node keys'); } + super(storage, path, key, options); + // this.enableReverseLookup = true; + this.indexMetadataKeys = ['_occurs_']; //,'_indexes_' + this.config = options.config || {}; + if (this.config.localeKey) { + // localeKey is supported by all indexes now + this.logger.warn(`fulltext index config option "localeKey" has been deprecated, as it is now supported for all indexes. Move the setting to the global index settings`); + this.textLocaleKey = this.config.localeKey; // Do use it now + } + } + + // get fileName() { + // return super.fileName.slice(0, -4) + '.fulltext.idx'; + // } + + get type() { + return 'fulltext'; + } + + getTextInfo(val: string, locale?: string) { + return new TextInfo(val, { + locale: locale ?? this.textLocale, + prepare: this.config.prepare, + stemming: this.config.transform, + blacklist: this.config.blacklist, + whitelist: this.config.whitelist, + useStoplist: this.config.useStoplist, + minLength: this.config.minLength, + maxLength: this.config.maxLength, + }); + } + + test(obj: any, op: 'fulltext:contains' | 'fulltext:!contains', val: string): boolean { + if (obj === null) { return op === 'fulltext:!contains'; } + const text = obj[this.key]; + if (typeof text === 'undefined') { return op === 'fulltext:!contains'; } + + const locale = obj?.[this.textLocaleKey] ?? this.textLocale; + const textInfo = this.getTextInfo(text, locale); + if (op === 'fulltext:contains') { + if (~val.indexOf(' OR ')) { + // split + const tests = val.split(' OR '); + return tests.some(val => this.test(text, op, val)); + } + else if (~val.indexOf('"')) { + // Phrase(s) used. We have to make sure the words used are not only in the text, + // but also in that exact order. + const phraseRegex = /"(.+?)"/g; + const phrases = []; + while (true) { + const match = phraseRegex.exec(val); + if (match === null) { break; } + const phrase = match[1]; + phrases.push(phrase); + val = val.slice(0, match.index) + val.slice(match.index + match[0].length); + phraseRegex.lastIndex = 0; + } + if (val.length > 0) { + phrases.push(val); + } + return phrases.every(phrase => { + const phraseInfo = this.getTextInfo(phrase, locale); + + // This was broken before TS port because WordInfo had an array of words that was not + // in the same order as the source words were. + // TODO: Thoroughly test this new code + const phraseWords = phraseInfo.toSequence(); + const occurrencesPerWord = phraseWords.map((word, i) => { + // Find word in text + const { indexes } = textInfo.words.get(word); + return indexes; + }); + const hasSequenceAtIndex = (wordIndex: number, occurrenceIndex: number): boolean => { + const startIndex = occurrencesPerWord[wordIndex]?.[occurrenceIndex]; + return occurrencesPerWord.slice(wordIndex + 1).every((occurences, i) => { + return occurences.some((index, j) => { + if (index !== startIndex + 1) { return false; } + return hasSequenceAtIndex(wordIndex + i, j); + }); + }); + }; + + // Find the existence of a sequence of words + // Loop: for each occurrence of the first word in text, remember its index + // Try to find second word in text with index+1 + // - found: try to find third word in text with index+2, etc (recursive) + // - not found: stop, proceed with next occurrence in main loop + return occurrencesPerWord[0].some((occurrence, i) => { + return hasSequenceAtIndex(0, i); + }); + + // const indexes = phraseInfo.words.map(word => textInfo.words.indexOf(word)); + // if (indexes[0] < 0) { return false; } + // for (let i = 1; i < indexes.length; i++) { + // if (indexes[i] - indexes[i-1] !== 1) { + // return false; + // } + // } + // return true; + }); + } + else { + // test 1 or more words + const wordsInfo = this.getTextInfo(val, locale); + return wordsInfo.toSequence().every(word => { + return textInfo.words.has(word); + }); + } + } + } + + async handleRecordUpdate(path: string, oldValue: any, newValue: any): Promise { + let oldText = oldValue !== null && typeof oldValue === 'object' && this.key in oldValue ? (oldValue as any)[this.key] : null; + let newText = newValue !== null && typeof newValue === 'object' && this.key in newValue ? (newValue as any)[this.key] : null; + + const oldLocale = oldValue?.[this.textLocaleKey] ?? this.textLocale, + newLocale = newValue?.[this.textLocaleKey] ?? this.textLocale; + + if (typeof oldText === 'object' && oldText instanceof Array) { + oldText = oldText.join(' '); + } + if (typeof newText === 'object' && newText instanceof Array) { + newText = newText.join(' '); + } + + const oldTextInfo = this.getTextInfo(oldText, oldLocale); + const newTextInfo = this.getTextInfo(newText, newLocale); + + // super._updateReverseLookupKey( + // path, + // oldText ? textEncoder.encode(oldText) : null, + // newText ? textEncoder.encode(newText) : null, + // metadata + // ); + + const oldWords = oldTextInfo.toArray(); //.words.map(w => w.word); + const newWords = newTextInfo.toArray(); //.words.map(w => w.word); + + const removed = oldWords.filter(word => newWords.indexOf(word) < 0); + const added = newWords.filter(word => oldWords.indexOf(word) < 0); + const changed = oldWords.filter(word => newWords.indexOf(word) >= 0).filter(word => { + const oldInfo = oldTextInfo.getWordInfo(word); + const newInfo = newTextInfo.getWordInfo(word); + return oldInfo.occurs !== newInfo.occurs || oldInfo.indexes.some((index, i) => newInfo.indexes[i] !== index); + }); + changed.forEach(word => { + // Word metadata changed. Simplest solution: remove and add again + removed.push(word); + added.push(word); + }); + const promises = [] as Promise[]; + // TODO: Prepare operations batch, then execute 1 tree update. + // Now every word is a seperate update which is not necessary! + removed.forEach(word => { + const p = super.handleRecordUpdate(path, { [this.key]: word }, { [this.key]: null }); + promises.push(p); + }); + added.forEach(word => { + const mutated: Record = { }; + Object.assign(mutated, newValue); + mutated[this.key] = word; + + const wordInfo = newTextInfo.getWordInfo(word); + // const indexMetadata = { + // '_occurs_': wordInfo.occurs, + // '_indexes_': wordInfo.indexes.join(',') + // }; + + let occurs = wordInfo.indexes.join(','); + if (occurs.length > 255) { + console.warn(`FullTextIndex ${this.description}: word "${word}" occurs too many times in "${path}/${this.key}" to store in index metadata. Truncating occurrences`); + const cutIndex = occurs.lastIndexOf(',', 255); + occurs = occurs.slice(0, cutIndex); + } + const indexMetadata = { + '_occurs_': occurs, + }; + const p = super.handleRecordUpdate(path, { [this.key]: null }, mutated, indexMetadata); + promises.push(p); + }); + await Promise.all(promises); + } + + build() { + return super.build({ + addCallback: (add, text: string | string[], recordPointer, metadata, env) => { + if (typeof text === 'object' && text instanceof Array) { + text = text.join(' '); + } + if (typeof text === 'undefined') { + text = ''; + } + const locale = env.locale || this.textLocale; + const textInfo = this.getTextInfo(text, locale); + if (textInfo.words.size === 0) { + this.logger.warn(`No words found in "${typeof text === 'string' && text.length > 50 ? text.slice(0, 50) + '...' : text}" to fulltext index "${env.path}"`); + } + + // const revLookupKey = super._getRevLookupKey(env.path); + // tree.add(revLookupKey, textEncoder.encode(text), metadata); + + textInfo.words.forEach(wordInfo => { + + // IDEA: To enable fast '*word' queries (starting with wildcard), we can also store + // reversed words and run reversed query 'drow*' on it. we'd have to enable storing + // multiple B+Trees in a single index file: a 'forward' tree & a 'reversed' tree + + // IDEA: Following up on previous idea: being able to backtrack nodes within an index would + // help to speed up sorting queries on an indexed key, + // eg: query .take(10).filter('rating','>=', 8).sort('title') + // does not filter on key 'title', but can then use an index on 'title' for the sorting: + // it can take the results from the 'rating' index and backtrack the nodes' titles to quickly + // get a sorted top 10. We'd have to store a seperate 'backtrack' tree that uses recordPointers + // as the key, and 'title' values as recordPointers. Caveat: max string length for sorting would + // then be 255 ASCII chars, because that's the recordPointer size limit. + // The same boost can currently only be achieved by creating an index that includes 'title' in + // the index on 'rating' ==> db.indexes.create('movies', 'rating', { include: ['title'] }) + + // Extend metadata with more details about the word (occurrences, positions) + // const wordMetadata = { + // '_occurs_': wordInfo.occurs, + // '_indexes_': wordInfo.indexes.join(',') + // }; + + let occurs = wordInfo.indexes.join(','); + if (occurs.length > 255) { + console.warn(`FullTextIndex ${this.description}: word "${wordInfo.word}" occurs too many times to store in index metadata. Truncating occurrences`); + const cutIndex = occurs.lastIndexOf(',', 255); + occurs = occurs.slice(0, cutIndex); + } + const wordMetadata: IndexMetaData = { + '_occurs_': occurs, + }; + Object.assign(wordMetadata, metadata); + add(wordInfo.word, recordPointer, wordMetadata); + }); + return textInfo.toArray(); //words.map(info => info.word); + }, + valueTypes: [VALUE_TYPES.STRING], + }); + } + + static get validOperators() { + return ['fulltext:contains', 'fulltext:!contains']; + } + get validOperators() { + return FullTextIndex.validOperators; + } + + async query(op: string | BlacklistingSearchOperator, val?: string, options?: any) { + if (op instanceof BlacklistingSearchOperator) { + throw new Error(`Not implemented: Can't query fulltext index with blacklisting operator yet`); + } + if (op === 'fulltext:contains' || op === 'fulltext:!contains') { + return this.contains(op, val, options); + } + else { + throw new Error(`Fulltext indexes can only be queried with operators ${FullTextIndex.validOperators.map(op => `"${op}"`).join(', ')}`); + } + } + + /** + * + * @param op Operator to use, can be either "fulltext:contains" or "fulltext:!contains" + * @param val Text to search for. Can include * and ? wildcards, OR's for combined searches, and "quotes" for phrase searches + */ + async contains(op: 'fulltext:contains' | 'fulltext:!contains', val: string, options: FullTextContainsQueryOptions = { + phrase: false, + locale: undefined, + minimumWildcardWordLength: 2, + }): Promise { + if (!FullTextIndex.validOperators.includes(op)) { //if (op !== 'fulltext:contains' && op !== 'fulltext:not_contains') { + throw new Error(`Fulltext indexes can only be queried with operators ${FullTextIndex.validOperators.map(op => `"${op}"`).join(', ')}`); + } + + // Check cache + const cache = this.cache(op, val); + if (cache) { + // Use cached results + return Promise.resolve(cache); + } + + const stats = new IndexQueryStats(options.phrase ? 'fulltext_phrase_query' : 'fulltext_query', val, true); + + // const searchWordRegex = /[\w'?*]+/g; // Use TextInfo to find and transform words using index settings + const getTextInfo = (text: string) => { + const info = new TextInfo(text, { + locale: options.locale || this.textLocale, + prepare: this.config.prepare, + stemming: this.config.transform, + minLength: this.config.minLength, + maxLength: this.config.maxLength, + blacklist: this.config.blacklist, + whitelist: this.config.whitelist, + useStoplist: this.config.useStoplist, + includeChars: '*?', + }); + + // Ignore any wildcard words that do not meet the set minimum length + // This is to safeguard the system against (possibly unwanted) very large + // result sets + const words = info.toArray(); + let i; + while (i = words.findIndex(w => /^[*?]+$/.test(w)), i >= 0) { + // Word is wildcards only. Ignore + const word = words[i]; + info.ignored.push(word); + info.words.delete(word); + } + + if (options.minimumWildcardWordLength > 0) { + for (const word of words) { + const starIndex = word.indexOf('*'); + // min = 2, word = 'an*', starIndex = 2, ok! + // min = 3: starIndex < min: not ok! + if (starIndex > 0 && starIndex < options.minimumWildcardWordLength) { + info.ignored.push(word); + info.words.delete(word); + i--; + } + } + } + return info; + }; + + if (val.includes(' OR ')) { + // Multiple searches in one query: 'secret OR confidential OR "don't tell"' + // TODO: chain queries instead of running simultanious? + const queries = val.split(' OR '); + const promises = queries.map(q => this.query(op, q, options)); + const resultSets = await Promise.all(promises); + stats.steps.push(...resultSets.map(results => results.stats)); + + const mergeStep = new IndexQueryStats('merge_expand', { sets: resultSets.length, results: resultSets.reduce((total, set) => total + set.length, 0) }, true); + stats.steps.push(mergeStep); + + const merged = resultSets[0]; + resultSets.slice(1).forEach(results => { + results.forEach(result => { + const exists = ~merged.findIndex(r => r.path === result.path); + if (!exists) { merged.push(result); } + }); + }); + const results = IndexQueryResults.fromResults(merged, this.key); + mergeStep.stop(results.length); + + stats.stop(results.length); + results.stats = stats; + results.hints.push(...resultSets.reduce((hints, set) => { hints.push(...set.hints); return hints; }, [])); + return results; + } + if (val.includes('"')) { + // Phrase(s) used. We have to make sure the words used are not only in the text, + // but also in that exact order. + const phraseRegex = /"(.+?)"/g; + const phrases = []; + while (true) { + const match = phraseRegex.exec(val); + if (match === null) { break; } + const phrase = match[1]; + phrases.push(phrase); + val = val.slice(0, match.index) + val.slice(match.index + match[0].length); + phraseRegex.lastIndex = 0; + } + + const phraseOptions: typeof options = {}; + Object.assign(phraseOptions, options); + phraseOptions.phrase = true; + const promises = phrases.map(phrase => this.query(op, phrase, phraseOptions)); + + // Check if what is left over still contains words + if (val.length > 0 && getTextInfo(val).wordCount > 0) { //(val.match(searchWordRegex) !== null) { + // Add it + const promise = this.query(op, val, options); + promises.push(promise); + } + + const resultSets = await Promise.all(promises); + stats.steps.push(...resultSets.map(results => results.stats)); + + // Take shortest set, only keep results that are matched in all other sets + const mergeStep = new IndexQueryStats('merge_reduce', { sets: resultSets.length, results: resultSets.reduce((total, set) => total + set.length, 0) }, true); + resultSets.length > 1 && stats.steps.push(mergeStep); + + const shortestSet = resultSets.sort((a,b) => a.length < b.length ? -1 : 1)[0]; + const otherSets = resultSets.slice(1); + const matches = shortestSet.reduce((matches, match) => { + // Check if the key is present in the other result sets + const path = match.path; + const matchedInAllSets = otherSets.every(set => set.findIndex(match => match.path === path) >= 0); + if (matchedInAllSets) { matches.push(match); } + return matches; + }, new IndexQueryResults()); + matches.filterKey = this.key; + mergeStep.stop(matches.length); + + stats.stop(matches.length); + matches.stats = stats; + matches.hints.push(...resultSets.reduce((hints, set) => { hints.push(...set.hints); return hints; }, [])); + return matches; + } + + const info = getTextInfo(val); + + /** + * Add ignored words to the result hints + */ + function addIgnoredWordHints(results: IndexQueryResults) { + // Add hints for ignored words + info.ignored.forEach(word => { + const hint = new FullTextIndexQueryHint(FullTextIndexQueryHint.types.ignoredWord, word); + results.hints.push(hint); + }); + } + + const words = info.toArray(); + if (words.length === 0) { + // Resolve with empty array + stats.stop(0); + const results = IndexQueryResults.fromResults([], this.key); + results.stats = stats; + addIgnoredWordHints(results); + return results; + } + + if (op === 'fulltext:!contains') { + // NEW: Use BlacklistingSearchOperator that uses all (unique) values in the index, + // besides the ones that get blacklisted along the way by our callback function + const wordChecks = words.map(word => { + if (word.includes('*') || word.includes('?')) { + const pattern = '^' + word.replace(/\*/g, '.*').replace(/\?/g, '.') + '$'; + const re = new RegExp(pattern, 'i'); + return re; + } + return word; + }); + const customOp = new BlacklistingSearchOperator(entry => { + const blacklist = wordChecks.some(word => { + if (word instanceof RegExp) { + return word.test(entry.key as string); + } + return entry.key === word; + }); + if (blacklist) { return entry.values; } + }); + + stats.type = 'fulltext_blacklist_scan'; + const results = await super.query(customOp); + stats.stop(results.length); + results.filterKey = this.key; + results.stats = stats; + addIgnoredWordHints(results); + + // Cache results + this.cache(op, val, results); + return results; + } + + // op === 'fulltext:contains' + // Get result count for each word + const countPromises = words.map(word => { + const wildcardIndex = ~(~word.indexOf('*') || ~word.indexOf('?')); // TODO: improve readability + const wordOp = wildcardIndex >= 0 ? 'like' : '=='; + const step = new IndexQueryStats('count', { op: wordOp, word }, true); + stats.steps.push(step); + return super.count(wordOp, word) + .then(count => { + step.stop(count); + return { word, count }; + }); + }); + const counts = await Promise.all(countPromises); + // Start with the smallest result set + counts.sort((a, b) => { + if (a.count < b.count) { return -1; } + else if (a.count > b.count) { return 1; } + return 0; + }); + + let results: IndexQueryResults; + + if (counts[0].count === 0) { + stats.stop(0); + + this.logger.info(`Word "${counts[0].word}" not found in index, 0 results for query ${op} "${val}"`); + results = new IndexQueryResults(0); + results.filterKey = this.key; + results.stats = stats; + addIgnoredWordHints(results); + + // Add query hints for each unknown word + counts.forEach(c => { + if (c.count === 0) { + const hint = new FullTextIndexQueryHint(FullTextIndexQueryHint.types.missingWord, c.word); + results.hints.push(hint); + } + }); + + // Cache the empty result set + this.cache(op, val, results); + return results; + } + const allWords = counts.map(c => c.word); + + // Sequentual method: query 1 word, then filter results further and further + // More or less performs the same as parallel, but uses less memory + // NEW: Start with the smallest result set + + // OLD: Use the longest word to search with, then filter those results + // const allWords = words.slice().sort((a,b) => { + // if (a.length < b.length) { return 1; } + // else if (a.length > b.length) { return -1; } + // return 0; + // }); + + const queryWord = async (word: string, filter: IndexQueryResults) => { + const wildcardIndex = ~(~word.indexOf('*') || ~word.indexOf('?')); // TODO: improve readability + const wordOp = wildcardIndex >= 0 ? 'like' : '=='; + // const step = new IndexQueryStats('query', { op: wordOp, word }, true); + // stats.steps.push(step); + const results = await super.query(wordOp, word, { filter }); + stats.steps.push(results.stats); + // step.stop(results.length); + return results; + }; + let wordIndex = 0; + const resultsPerWord: IndexQueryResults[] = new Array(words.length); + const nextWord = async () => { + const word = allWords[wordIndex]; + const t1 = Date.now(); + const fr = await queryWord(word, results); + const t2 = Date.now(); + this.logger.info(`fulltext search for "${word}" took ${t2-t1}ms`); + resultsPerWord[words.indexOf(word)] = fr; + results = fr; + wordIndex++; + if (results.length === 0 || wordIndex === allWords.length) { return; } + await nextWord(); + }; + await nextWord(); + + type MetaDataWithOccursArray = IndexMetaData & { _occurs_: number[] }; + + if (options.phrase === true && allWords.length > 1) { + // Check which results have the words in the right order + const step = new IndexQueryStats('phrase_check', val, true); + stats.steps.push(step); + results = results.reduce((matches, match) => { + // the order of the resultsPerWord is in the same order as the given words, + // check if their metadata._occurs_ say the same about the indexed content + const path = match.path; + const wordMatches = resultsPerWord.map(results => { + return results.find(result => result.path === path); + }); + // Convert the _occurs_ strings to arrays we can use + wordMatches.forEach(match => { + (match.metadata as MetaDataWithOccursArray)._occurs_ = (match.metadata._occurs_ as string).split(',').map(parseInt); + }); + const check = (wordMatchIndex: number, prevWordIndex?: number): boolean => { + const sourceIndexes = (wordMatches[wordMatchIndex].metadata as MetaDataWithOccursArray)._occurs_; + if (typeof prevWordIndex !== 'number') { + // try with each sourceIndex of the first word + for (let i = 0; i < sourceIndexes.length; i++) { + const found = check(1, sourceIndexes[i]); + if (found) { return true; } + } + return false; + } + // We're in a recursive call on the 2nd+ word + if (sourceIndexes.includes(prevWordIndex + 1)) { + // This word came after the previous word, hooray! + // Proceed with next word, or report success if this was the last word to check + if (wordMatchIndex === wordMatches.length-1) { return true; } + return check(wordMatchIndex+1, prevWordIndex+1); + } + else { + return false; + } + }; + if (check(0)) { + matches.push(match); // Keep! + } + return matches; + }, new IndexQueryResults()); + step.stop(results.length); + } + results.filterKey = this.key; + + stats.stop(results.length); + results.stats = stats; + addIgnoredWordHints(results); + + // Cache results + delete results.entryValues; // No need to cache these. Free the memory + this.cache(op, val, results); + return results; + + // Parallel method: query all words at the same time, then combine results + // Uses more memory + // const promises = words.map(word => { + // const wildcardIndex = ~(~word.indexOf('*') || ~word.indexOf('?')); + // let wordOp; + // if (op === 'fulltext:contains') { + // wordOp = wildcardIndex >= 0 ? 'like' : '=='; + // } + // else if (op === 'fulltext:!contains') { + // wordOp = wildcardIndex >= 0 ? '!like' : '!='; + // } + // // return super.query(wordOp, word) + // return super.query(wordOp, word) + // }); + // return Promise.all(promises) + // .then(resultSets => { + // // Now only use matches that exist in all result sets + // const sortedSets = resultSets.slice().sort((a,b) => a.length < b.length ? -1 : 1) + // const shortestSet = sortedSets[0]; + // const otherSets = sortedSets.slice(1); + // let matches = shortestSet.reduce((matches, match) => { + // // Check if the key is present in the other result sets + // const path = match.path; + // const matchedInAllSets = otherSets.every(set => set.findIndex(match => match.path === path) >= 0); + // if (matchedInAllSets) { matches.push(match); } + // return matches; + // }, new IndexQueryResults()); + + // if (options.phrase === true && resultSets.length > 1) { + // // Check if the words are in the right order + // console.log(`Breakpoint time`); + // matches = matches.reduce((matches, match) => { + // // the order of the resultSets is in the same order as the given words, + // // check if their metadata._indexes_ say the same about the indexed content + // const path = match.path; + // const wordMatches = resultSets.map(set => { + // return set.find(match => match.path === path); + // }); + // // Convert the _indexes_ strings to arrays we can use + // wordMatches.forEach(match => { + // // match.metadata._indexes_ = match.metadata._indexes_.split(',').map(parseInt); + // match.metadata._occurs_ = match.metadata._occurs_.split(',').map(parseInt); + // }); + // const check = (wordMatchIndex, prevWordIndex) => { + // const sourceIndexes = wordMatches[wordMatchIndex].metadata._occurs_; //wordMatches[wordMatchIndex].metadata._indexes_; + // if (typeof prevWordIndex !== 'number') { + // // try with each sourceIndex of the first word + // for (let i = 0; i < sourceIndexes.length; i++) { + // const found = check(1, sourceIndexes[i]); + // if (found) { return true; } + // } + // return false; + // } + // // We're in a recursive call on the 2nd+ word + // if (~sourceIndexes.indexOf(prevWordIndex + 1)) { + // // This word came after the previous word, hooray! + // // Proceed with next word, or report success if this was the last word to check + // if (wordMatchIndex === wordMatches.length-1) { return true; } + // return check(wordMatchIndex+1, prevWordIndex+1); + // } + // else { + // return false; + // } + // } + // if (check(0)) { + // matches.push(match); // Keep! + // } + // return matches; + // }, new IndexQueryResults()); + // } + // matches.filterKey = this.key; + // return matches; + // }); + } +} diff --git a/src/data-index/geo-index.ts b/src/data-index/geo-index.ts index 5dcab97..3b96dad 100644 --- a/src/data-index/geo-index.ts +++ b/src/data-index/geo-index.ts @@ -1,284 +1,284 @@ -import type { Storage } from '../storage'; -import { BlacklistingSearchOperator } from '../btree'; -import { VALUE_TYPES } from '../node-value-types'; -import { DataIndex } from './data-index'; -import { DataIndexOptions } from './options'; -import { IndexQueryResults } from './query-results'; -import { IndexQueryStats } from './query-stats'; -import { IndexableValueOrArray } from './shared'; -import * as Geohash from '../geohash'; - -function _getGeoRadiusPrecision(radiusM: number) { - if (typeof radiusM !== 'number') { return; } - if (radiusM < 0.01) { return 12; } - if (radiusM < 0.075) { return 11; } - if (radiusM < 0.6) { return 10; } - if (radiusM < 2.3) { return 9; } - if (radiusM < 19) { return 8; } - if (radiusM < 76) { return 7; } - if (radiusM < 610) { return 6; } - if (radiusM < 2400) { return 5; } - if (radiusM < 19500) { return 4; } - if (radiusM < 78700) { return 3; } - if (radiusM < 626000) { return 2; } - return 1; -} - -function _getGeoHash(obj: { lat: number; long: number }) { - if (typeof obj.lat !== 'number' || typeof obj.long !== 'number') { - return; - } - const precision = 10; //_getGeoRadiusPrecision(obj.radius); - const geohash = Geohash.encode(obj.lat, obj.long, precision); - return geohash; -} - -// Calculates which hashes (of different precisions) are within the radius of a point -function _hashesInRadius(lat: number, lon: number, radiusM: number, precision: number) { - - const isInCircle = (checkLat: number, checkLon: number, lat: number, lon: number, radiusM: number) => { - const deltaLon = checkLon - lon; - const deltaLat = checkLat - lat; - return Math.pow(deltaLon, 2) + Math.pow(deltaLat, 2) <= Math.pow(radiusM, 2); - }; - const getCentroid = (latitude: number, longitude: number, height: number, width: number) => { - const y_cen = latitude + (height / 2); - const x_cen = longitude + (width / 2); - return { x: x_cen, y: y_cen }; - }; - const convertToLatLon = (y: number, x: number, lat: number, lon: number) => { - const pi = 3.14159265359; - const r_earth = 6371000; - - const lat_diff = (y / r_earth) * (180 / pi); - const lon_diff = (x / r_earth) * (180 / pi) / Math.cos(lat * pi/180); - - const final_lat = lat + lat_diff; - const final_lon = lon + lon_diff; - - return { lat: final_lat, lon: final_lon }; - }; - - const x = 0; - const y = 0; - - const points = [] as Array<{ lat: number; lon: number; }>; - const geohashes = [] as string[]; - - const gridWidths = [5009400.0, 1252300.0, 156500.0, 39100.0, 4900.0, 1200.0, 152.9, 38.2, 4.8, 1.2, 0.149, 0.0370]; - const gridHeights = [4992600.0, 624100.0, 156000.0, 19500.0, 4900.0, 609.4, 152.4, 19.0, 4.8, 0.595, 0.149, 0.0199]; - - const height = gridHeights[precision-1] / 2; - const width = gridWidths[precision-1] / 2; - - const latMoves = Math.ceil(radiusM / height); - const lonMoves = Math.ceil(radiusM / width); - - for (let i = 0; i <= latMoves; i++) { - const tmpLat = y + height*i; - - for (let j = 0; j < lonMoves; j++) { - const tmpLon = x + width * j; - - if (isInCircle(tmpLat, tmpLon, y, x, radiusM)) { - const center = getCentroid(tmpLat, tmpLon, height, width); - points.push(convertToLatLon(center.y, center.x, lat, lon)); - points.push(convertToLatLon(-center.y, center.x, lat, lon)); - points.push(convertToLatLon(center.y, -center.x, lat, lon)); - points.push(convertToLatLon(-center.y, -center.x, lat, lon)); - } - } - } - - points.forEach(point => { - const hash = Geohash.encode(point.lat, point.lon, precision); - if (geohashes.indexOf(hash) < 0) { - geohashes.push(hash); - } - }); - - // Original optionally uses Georaptor compression of geohashes - // This is my simple implementation - geohashes.forEach((currentHash, index, arr) => { - const precision = currentHash.length; - const parentHash = currentHash.substr(0, precision-1); - let hashNeighbourMatches = 0; - const removeIndexes = []; - arr.forEach((otherHash, otherIndex) => { - if (otherHash.startsWith(parentHash)) { - removeIndexes.push(otherIndex); - if (otherHash.length == precision) { - hashNeighbourMatches++; - } - } - }); - if (hashNeighbourMatches === 32) { - // All 32 areas of a less precise geohash are included. - // Replace those with the less precise parent - for (let i = removeIndexes.length - 1; i >= 0; i--) { - arr.splice(i, 1); - } - arr.splice(index, 0, parentHash); - } - }); - - return geohashes; -} - -export class GeoIndex extends DataIndex { - constructor(storage: Storage, path: string, key: string, options: DataIndexOptions) { - if (key === '{key}') { throw new Error('Cannot create geo index on node keys'); } - super(storage, path, key, options); - } - - // get fileName() { - // return super.fileName.slice(0, -4) + '.geo.idx'; - // } - - get type() { - return 'geo'; - } - - async handleRecordUpdate(path: string, oldValue: unknown, newValue: unknown) { - const mutated = { old: {} as any, new: {} as any }; - oldValue !== null && typeof oldValue === 'object' && Object.assign(mutated.old, oldValue); - newValue !== null && typeof newValue === 'object' && Object.assign(mutated.new, newValue); - if (mutated.old[this.key] !== null && typeof mutated.old[this.key] === 'object') { - mutated.old[this.key] = _getGeoHash(mutated.old[this.key]); - } - if (mutated.new[this.key] !== null && typeof mutated.new[this.key] === 'object') { - mutated.new[this.key] = _getGeoHash(mutated.new[this.key]); - } - super.handleRecordUpdate(path, mutated.old, mutated.new); - } - - build() { - return super.build({ - addCallback: (add, obj: { lat: number; long: number; }, recordPointer, metadata) => { - if (typeof obj !== 'object') { - this.logger.warn(`GeoIndex cannot index location because value "${obj}" is not an object`); - return; - } - if (typeof obj.lat !== 'number' || typeof obj.long !== 'number') { - this.logger.warn(`GeoIndex cannot index location because lat (${obj.lat}) or long (${obj.long}) are invalid`); - return; - } - const geohash = _getGeoHash(obj); - add(geohash, recordPointer, metadata); - return geohash; - }, - valueTypes: [VALUE_TYPES.OBJECT], - }); - } - - static get validOperators() { - return ['geo:nearby']; - } - - get validOperators() { - return GeoIndex.validOperators; - } - - test(obj: any, op: 'geo:nearby', val: { lat: number; long: number; radius: number }) { - if (!this.validOperators.includes(op)) { - throw new Error(`Unsupported operator "${op}"`); - } - if (obj == null || typeof obj !== 'object') { - // No source object - return false; - } - const src = obj[this.key] as { lat: number; long: number }; - if (typeof src !== 'object' || typeof src.lat !== 'number' || typeof src.long !== 'number') { - // source object is not geo - return false; - } - if (typeof val !== 'object' || typeof val.lat !== 'number' || typeof val.long !== 'number' || typeof val.radius !== 'number') { - // compare object is not geo with radius - return false; - } - - const isInCircle = (checkLat: number, checkLon: number, lat: number, lon: number, radiusM: number) => { - const deltaLon = checkLon - lon; - const deltaLat = checkLat - lat; - return Math.pow(deltaLon, 2) + Math.pow(deltaLat, 2) <= Math.pow(radiusM, 2); - }; - return isInCircle(src.lat, src.long, val.lat, val.long, val.radius); - } - - async query(op: string | BlacklistingSearchOperator, val?: IndexableValueOrArray, options?: { filter?: IndexQueryResults; }) { - if (op instanceof BlacklistingSearchOperator) { - throw new Error(`Not implemented: Can't query geo index with blacklisting operator yet`); - } - if (options) { - this.logger.warn('Not implemented: query options for geo indexes are ignored'); - } - if (op === 'geo:nearby') { - if (val === null || typeof val !== 'object' || !('lat' in val) || !('long' in val) || !('radius' in val) || typeof val.lat !== 'number' || typeof val.long !== 'number' || typeof val.radius !== 'number') { - throw new Error(`geo nearby query expects an object with numeric lat, long and radius properties`); - } - return this.nearby(val as { lat: number; long: number; radius: number }); - } - else { - throw new Error(`Geo indexes can only be queried with operators ${GeoIndex.validOperators.map(op => `"${op}"`).join(', ')}`); - } - } - /** - * @param op Only 'geo:nearby' is supported at the moment - */ - async nearby( - val: { - /** - * nearby query center latitude - */ - lat: number; - - /** - * nearby query center longitude - */ - long: number; - - /** - * nearby query radius in meters - */ - radius: number; - }, - ): Promise { - const op = 'geo:nearby'; - - // Check cache - const cached = this.cache(op, val); - if (cached) { - // Use cached results - return cached; - } - - if (typeof val.lat !== 'number' || typeof val.long !== 'number' || typeof val.radius !== 'number') { - throw new Error('geo:nearby query must supply an object with properties .lat, .long and .radius'); - } - const stats = new IndexQueryStats('geo_nearby_query', val, true); - - const precision = _getGeoRadiusPrecision(val.radius / 10); - const targetHashes = _hashesInRadius(val.lat, val.long, val.radius, precision); - - stats.queries = targetHashes.length; - - const promises = targetHashes.map(hash => { - return super.query('like', `${hash}*`); - }); - const resultSets= await Promise.all(promises); - - // Combine all results - const results = new IndexQueryResults(); - results.filterKey = this.key; - resultSets.forEach(set => { - set.forEach(match => results.push(match)); - }); - - stats.stop(results.length); - results.stats = stats; - - this.cache(op, val, results); - - return results; - } -} +import type { Storage } from '../storage/index.js'; +import { BlacklistingSearchOperator } from '../btree/index.js'; +import { VALUE_TYPES } from '../node-value-types.js'; +import { DataIndex } from './data-index.js'; +import { DataIndexOptions } from './options.js'; +import { IndexQueryResults } from './query-results.js'; +import { IndexQueryStats } from './query-stats.js'; +import { IndexableValueOrArray } from './shared.js'; +import * as Geohash from '../geohash.js'; + +function _getGeoRadiusPrecision(radiusM: number) { + if (typeof radiusM !== 'number') { return; } + if (radiusM < 0.01) { return 12; } + if (radiusM < 0.075) { return 11; } + if (radiusM < 0.6) { return 10; } + if (radiusM < 2.3) { return 9; } + if (radiusM < 19) { return 8; } + if (radiusM < 76) { return 7; } + if (radiusM < 610) { return 6; } + if (radiusM < 2400) { return 5; } + if (radiusM < 19500) { return 4; } + if (radiusM < 78700) { return 3; } + if (radiusM < 626000) { return 2; } + return 1; +} + +function _getGeoHash(obj: { lat: number; long: number }) { + if (typeof obj.lat !== 'number' || typeof obj.long !== 'number') { + return; + } + const precision = 10; //_getGeoRadiusPrecision(obj.radius); + const geohash = Geohash.encode(obj.lat, obj.long, precision); + return geohash; +} + +// Calculates which hashes (of different precisions) are within the radius of a point +function _hashesInRadius(lat: number, lon: number, radiusM: number, precision: number) { + + const isInCircle = (checkLat: number, checkLon: number, lat: number, lon: number, radiusM: number) => { + const deltaLon = checkLon - lon; + const deltaLat = checkLat - lat; + return Math.pow(deltaLon, 2) + Math.pow(deltaLat, 2) <= Math.pow(radiusM, 2); + }; + const getCentroid = (latitude: number, longitude: number, height: number, width: number) => { + const y_cen = latitude + (height / 2); + const x_cen = longitude + (width / 2); + return { x: x_cen, y: y_cen }; + }; + const convertToLatLon = (y: number, x: number, lat: number, lon: number) => { + const pi = 3.14159265359; + const r_earth = 6371000; + + const lat_diff = (y / r_earth) * (180 / pi); + const lon_diff = (x / r_earth) * (180 / pi) / Math.cos(lat * pi/180); + + const final_lat = lat + lat_diff; + const final_lon = lon + lon_diff; + + return { lat: final_lat, lon: final_lon }; + }; + + const x = 0; + const y = 0; + + const points = [] as Array<{ lat: number; lon: number; }>; + const geohashes = [] as string[]; + + const gridWidths = [5009400.0, 1252300.0, 156500.0, 39100.0, 4900.0, 1200.0, 152.9, 38.2, 4.8, 1.2, 0.149, 0.0370]; + const gridHeights = [4992600.0, 624100.0, 156000.0, 19500.0, 4900.0, 609.4, 152.4, 19.0, 4.8, 0.595, 0.149, 0.0199]; + + const height = gridHeights[precision-1] / 2; + const width = gridWidths[precision-1] / 2; + + const latMoves = Math.ceil(radiusM / height); + const lonMoves = Math.ceil(radiusM / width); + + for (let i = 0; i <= latMoves; i++) { + const tmpLat = y + height*i; + + for (let j = 0; j < lonMoves; j++) { + const tmpLon = x + width * j; + + if (isInCircle(tmpLat, tmpLon, y, x, radiusM)) { + const center = getCentroid(tmpLat, tmpLon, height, width); + points.push(convertToLatLon(center.y, center.x, lat, lon)); + points.push(convertToLatLon(-center.y, center.x, lat, lon)); + points.push(convertToLatLon(center.y, -center.x, lat, lon)); + points.push(convertToLatLon(-center.y, -center.x, lat, lon)); + } + } + } + + points.forEach(point => { + const hash = Geohash.encode(point.lat, point.lon, precision); + if (geohashes.indexOf(hash) < 0) { + geohashes.push(hash); + } + }); + + // Original optionally uses Georaptor compression of geohashes + // This is my simple implementation + geohashes.forEach((currentHash, index, arr) => { + const precision = currentHash.length; + const parentHash = currentHash.substr(0, precision-1); + let hashNeighbourMatches = 0; + const removeIndexes = []; + arr.forEach((otherHash, otherIndex) => { + if (otherHash.startsWith(parentHash)) { + removeIndexes.push(otherIndex); + if (otherHash.length == precision) { + hashNeighbourMatches++; + } + } + }); + if (hashNeighbourMatches === 32) { + // All 32 areas of a less precise geohash are included. + // Replace those with the less precise parent + for (let i = removeIndexes.length - 1; i >= 0; i--) { + arr.splice(i, 1); + } + arr.splice(index, 0, parentHash); + } + }); + + return geohashes; +} + +export class GeoIndex extends DataIndex { + constructor(storage: Storage, path: string, key: string, options: DataIndexOptions) { + if (key === '{key}') { throw new Error('Cannot create geo index on node keys'); } + super(storage, path, key, options); + } + + // get fileName() { + // return super.fileName.slice(0, -4) + '.geo.idx'; + // } + + get type() { + return 'geo'; + } + + async handleRecordUpdate(path: string, oldValue: unknown, newValue: unknown) { + const mutated = { old: {} as any, new: {} as any }; + oldValue !== null && typeof oldValue === 'object' && Object.assign(mutated.old, oldValue); + newValue !== null && typeof newValue === 'object' && Object.assign(mutated.new, newValue); + if (mutated.old[this.key] !== null && typeof mutated.old[this.key] === 'object') { + mutated.old[this.key] = _getGeoHash(mutated.old[this.key]); + } + if (mutated.new[this.key] !== null && typeof mutated.new[this.key] === 'object') { + mutated.new[this.key] = _getGeoHash(mutated.new[this.key]); + } + super.handleRecordUpdate(path, mutated.old, mutated.new); + } + + build() { + return super.build({ + addCallback: (add, obj: { lat: number; long: number; }, recordPointer, metadata) => { + if (typeof obj !== 'object') { + this.logger.warn(`GeoIndex cannot index location because value "${obj}" is not an object`); + return; + } + if (typeof obj.lat !== 'number' || typeof obj.long !== 'number') { + this.logger.warn(`GeoIndex cannot index location because lat (${obj.lat}) or long (${obj.long}) are invalid`); + return; + } + const geohash = _getGeoHash(obj); + add(geohash, recordPointer, metadata); + return geohash; + }, + valueTypes: [VALUE_TYPES.OBJECT], + }); + } + + static get validOperators() { + return ['geo:nearby']; + } + + get validOperators() { + return GeoIndex.validOperators; + } + + test(obj: any, op: 'geo:nearby', val: { lat: number; long: number; radius: number }) { + if (!this.validOperators.includes(op)) { + throw new Error(`Unsupported operator "${op}"`); + } + if (obj == null || typeof obj !== 'object') { + // No source object + return false; + } + const src = obj[this.key] as { lat: number; long: number }; + if (typeof src !== 'object' || typeof src.lat !== 'number' || typeof src.long !== 'number') { + // source object is not geo + return false; + } + if (typeof val !== 'object' || typeof val.lat !== 'number' || typeof val.long !== 'number' || typeof val.radius !== 'number') { + // compare object is not geo with radius + return false; + } + + const isInCircle = (checkLat: number, checkLon: number, lat: number, lon: number, radiusM: number) => { + const deltaLon = checkLon - lon; + const deltaLat = checkLat - lat; + return Math.pow(deltaLon, 2) + Math.pow(deltaLat, 2) <= Math.pow(radiusM, 2); + }; + return isInCircle(src.lat, src.long, val.lat, val.long, val.radius); + } + + async query(op: string | BlacklistingSearchOperator, val?: IndexableValueOrArray, options?: { filter?: IndexQueryResults; }) { + if (op instanceof BlacklistingSearchOperator) { + throw new Error(`Not implemented: Can't query geo index with blacklisting operator yet`); + } + if (options) { + this.logger.warn('Not implemented: query options for geo indexes are ignored'); + } + if (op === 'geo:nearby') { + if (val === null || typeof val !== 'object' || !('lat' in val) || !('long' in val) || !('radius' in val) || typeof val.lat !== 'number' || typeof val.long !== 'number' || typeof val.radius !== 'number') { + throw new Error(`geo nearby query expects an object with numeric lat, long and radius properties`); + } + return this.nearby(val as { lat: number; long: number; radius: number }); + } + else { + throw new Error(`Geo indexes can only be queried with operators ${GeoIndex.validOperators.map(op => `"${op}"`).join(', ')}`); + } + } + /** + * @param op Only 'geo:nearby' is supported at the moment + */ + async nearby( + val: { + /** + * nearby query center latitude + */ + lat: number; + + /** + * nearby query center longitude + */ + long: number; + + /** + * nearby query radius in meters + */ + radius: number; + }, + ): Promise { + const op = 'geo:nearby'; + + // Check cache + const cached = this.cache(op, val); + if (cached) { + // Use cached results + return cached; + } + + if (typeof val.lat !== 'number' || typeof val.long !== 'number' || typeof val.radius !== 'number') { + throw new Error('geo:nearby query must supply an object with properties .lat, .long and .radius'); + } + const stats = new IndexQueryStats('geo_nearby_query', val, true); + + const precision = _getGeoRadiusPrecision(val.radius / 10); + const targetHashes = _hashesInRadius(val.lat, val.long, val.radius, precision); + + stats.queries = targetHashes.length; + + const promises = targetHashes.map(hash => { + return super.query('like', `${hash}*`); + }); + const resultSets= await Promise.all(promises); + + // Combine all results + const results = new IndexQueryResults(); + results.filterKey = this.key; + resultSets.forEach(set => { + set.forEach(match => results.push(match)); + }); + + stats.stop(results.length); + results.stats = stats; + + this.cache(op, val, results); + + return results; + } +} diff --git a/src/data-index/index.ts b/src/data-index/index.ts index bf11ee6..85e8929 100644 --- a/src/data-index/index.ts +++ b/src/data-index/index.ts @@ -1,10 +1,10 @@ -import { DataIndex } from './data-index'; -import { FullTextIndex } from './fulltext-index'; -import { GeoIndex } from './geo-index'; -import { ArrayIndex } from './array-index'; +import { DataIndex } from './data-index.js'; +import { FullTextIndex } from './fulltext-index.js'; +import { GeoIndex } from './geo-index.js'; +import { ArrayIndex } from './array-index.js'; export { DataIndex, FullTextIndex, GeoIndex, ArrayIndex }; -export { IndexQueryResults } from './query-results'; +export { IndexQueryResults } from './query-results.js'; DataIndex.KnownIndexTypes = { normal: DataIndex, diff --git a/src/data-index/query-results.ts b/src/data-index/query-results.ts index 921be0e..0834a60 100644 --- a/src/data-index/query-results.ts +++ b/src/data-index/query-results.ts @@ -1,7 +1,7 @@ -import { BPlusTreeLeafEntryValue } from '../btree/tree-leaf-entry-value'; -import { IndexQueryHint } from './query-hint'; -import { IndexQueryStats } from './query-stats'; -import { IndexableValue, IndexableValueOrArray, IndexMetaData } from './shared'; +import { BPlusTreeLeafEntryValue } from '../btree/tree-leaf-entry-value.js'; +import { IndexQueryHint } from './query-hint.js'; +import { IndexQueryStats } from './query-stats.js'; +import { IndexableValue, IndexableValueOrArray, IndexMetaData } from './shared.js'; export class IndexQueryResult { public values: BPlusTreeLeafEntryValue[]; diff --git a/src/data-index/shared.ts b/src/data-index/shared.ts index 526c104..c84d8b5 100644 --- a/src/data-index/shared.ts +++ b/src/data-index/shared.ts @@ -1,6 +1,6 @@ -import { NodeEntryKeyType } from '../btree/entry-key-type'; -import { LeafEntryMetaData } from '../btree/leaf-entry-metadata'; -import { LeafEntryRecordPointer } from '../btree/leaf-entry-recordpointer'; +import { NodeEntryKeyType } from '../btree/entry-key-type.js'; +import { LeafEntryMetaData } from '../btree/leaf-entry-metadata.js'; +import { LeafEntryRecordPointer } from '../btree/leaf-entry-recordpointer.js'; export type FileSystemError = Error & { code: string }; diff --git a/src/geohash.spec.ts b/src/geohash.spec.ts index 8f0e39a..8211f53 100644 --- a/src/geohash.spec.ts +++ b/src/geohash.spec.ts @@ -1,4 +1,4 @@ -import { encode, decode, neighbours, adjacent, bounds } from './geohash'; +import { encode, decode, neighbours, adjacent, bounds } from './geohash.js'; describe('Geohash', () => { it('encode', () => { diff --git a/src/index.ts b/src/index.ts index 80cc932..8b64d63 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,7 +9,7 @@ \_| |_/\___\___\____/ \__,_|___/\___| realtime database - Copyright 2018-2022 by Ewout Stortenbeker (me@appy.one) + Copyright 2018-2026 by Ewout Stortenbeker (me@appy.one) Published under MIT license See docs at https://github.com/appy-one/acebase @@ -38,7 +38,7 @@ export { PartialArray, } from 'acebase-core'; -import { AceBase } from './acebase-local'; +import { AceBase } from './acebase-local.js'; export default AceBase; // Use AceBase as default export, allows: `import AceBase from 'acebase'` export { @@ -46,11 +46,11 @@ export { AceBaseLocalSettings, LocalStorageSettings, IndexedDBStorageSettings, -} from './acebase-local'; +} from './acebase-local.js'; -export { AceBaseStorageSettings } from './storage/binary'; -export { SQLiteStorageSettings } from './storage/sqlite'; -export { MSSQLStorageSettings } from './storage/mssql'; +export { AceBaseStorageSettings } from './storage/binary/index.js'; +export { SQLiteStorageSettings } from './storage/sqlite/index.js'; +export { MSSQLStorageSettings } from './storage/mssql/index.js'; export { CustomStorageTransaction, @@ -58,11 +58,11 @@ export { CustomStorageHelpers, ICustomStorageNode, ICustomStorageNodeMetaData, -} from './storage/custom'; +} from './storage/custom/index.js'; export { StorageSettings, TransactionLogSettings, IPCClientSettings, SchemaValidationError, -} from './storage'; +} from './storage/index.js'; diff --git a/src/ipc/browser.ts b/src/ipc/browser.ts index c567e20..d41d1c3 100644 --- a/src/ipc/browser.ts +++ b/src/ipc/browser.ts @@ -1,7 +1,7 @@ import { ID, Transport } from 'acebase-core'; -import { AceBaseIPCPeer, IAceBaseIPCLock, IHelloMessage, IMessage } from './ipc'; -import { Storage } from '../storage'; -import { NotSupported } from '../not-supported'; +import { AceBaseIPCPeer, IAceBaseIPCLock, IHelloMessage, IMessage } from './ipc.js'; +import { Storage } from '../storage/index.js'; +import { NotSupported } from '../not-supported.js'; type MessageEventCallback = (event: MessageEvent) => any; diff --git a/src/ipc/index.ts b/src/ipc/index.ts index 74c300d..fada89a 100644 --- a/src/ipc/index.ts +++ b/src/ipc/index.ts @@ -1,137 +1,137 @@ -import { AceBaseIPCPeer, IHelloMessage, IMessage } from './ipc'; -import { Storage } from '../storage'; -import * as Cluster from 'cluster'; -const cluster = Cluster.default ?? Cluster as any as typeof Cluster.default; // ESM and CJS compatible approach -export { RemoteIPCPeer, RemoteIPCServerConfig } from './remote'; -export { IPCSocketPeer, NetIPCServer } from './socket'; - -const masterPeerId = '[master]'; - -interface INodeIPCMessage extends IMessage { - /** - * name of the target database. Needed when multiple database use the same communication channel - */ - dbname: string -} - -interface EventEmitterLike { - addListener(event: string, handler: (...args: any[]) => any): any; - removeListener(event: string, handler: (...args: any[]) => any): any; -} - -/** - * Node cluster functionality - enables vertical scaling with forked processes. AceBase will enable IPC at startup, so - * any forked process will communicate database changes and events automatically. Locking of resources will be done by - * the cluster's primary (previously master) process. NOTE: if the master process dies, all peers stop working - */ -export class IPCPeer extends AceBaseIPCPeer { - - constructor(storage: Storage, dbname: string) { - - // Throw eror on PM2 clusters --> they should use an AceBase IPC server - const pm2id = process.env?.NODE_APP_INSTANCE || process.env?.pm_id; - if (typeof pm2id === 'string' && pm2id !== '0') { - throw new Error(`To use AceBase with pm2 in cluster mode, use an AceBase IPC server to enable interprocess communication.`); - } - - const peerId = cluster.isMaster ? masterPeerId : cluster.worker.id.toString(); - super(storage, peerId, dbname); - - this.masterPeerId = masterPeerId; - this.ipcType = 'node.cluster'; - - /** Adds an event handler to a Node.js EventEmitter that is automatically removed upon IPC exit */ - const bindEventHandler = (target: EventEmitterLike, event: string, handler: (...args: any[]) => any) => { - target.addListener(event, handler); - this.on('exit', () => target.removeListener(event, handler)); - }; - - // Setup process exit handler - bindEventHandler(process, 'SIGINT', () => { - this.exit(); - }); - - if (cluster.isMaster) { - bindEventHandler(cluster, 'online', (worker: Cluster.Worker) => { - // A new worker is started - // Do not add yet, wait for "hello" message - a forked process might not use the same db - bindEventHandler(worker, 'error', err => { - this.logger.error(`Caught worker error:`, err); - }); - }); - - bindEventHandler(cluster, 'exit', (worker: Cluster.Worker) => { - // A worker has shut down - if (this.peers.find(peer => peer.id === worker.id.toString())) { - // Worker apparently did not have time to say goodbye, - // remove the peer ourselves - this.removePeer(worker.id.toString()); - - // Send "bye" message on their behalf - this.sayGoodbye(worker.id.toString()); - } - }); - } - - const handleMessage = (message: INodeIPCMessage) => { - if (typeof message !== 'object') { - // Ignore non-object IPC messages - return; - } - if (message.dbname !== this.dbname) { - // Ignore, message not meant for this database - return; - } - if (cluster.isMaster && message.to !== masterPeerId) { - // Message is meant for others (or all). Forward it - this.sendMessage(message); - } - if (message.to && message.to !== this.id) { - // Message is for somebody else. Ignore - return; - } - - return super.handleMessage(message); - }; - - if (cluster.isMaster) { - bindEventHandler(cluster, 'message', (worker: Cluster.Worker, message: INodeIPCMessage) => handleMessage(message)); - } - else { - bindEventHandler(cluster.worker, 'message', handleMessage); - } - - // if (!cluster.isMaster) { - // // Add master peer. Do we have to? - // this.addPeer(masterPeerId, false, false); - // } - - // Send hello to other peers - const helloMsg: IHelloMessage = { type: 'hello', from: this.id, data: undefined }; - this.sendMessage(helloMsg); - } - - public sendMessage(msg: IMessage) { - const message = msg as INodeIPCMessage; - message.dbname = this.dbname; - - if (cluster.isMaster) { - // If we are the master, send the message to the target worker(s) - this.peers - .filter(p => p.id !== message.from && (!message.to || p.id === message.to)) - .forEach(peer => { - const worker = cluster.workers[peer.id]; - worker && worker.send(message); // When debugging, worker might have stopped in the meantime - }); - } - else { - // Send the message to the master who will forward it to the target worker(s) - process.send(message); - } - } - - public async exit(code = 0) { - await super.exit(code); - } - -} +import { AceBaseIPCPeer, IHelloMessage, IMessage } from './ipc.js'; +import { Storage } from '../storage/index.js'; +import * as Cluster from 'cluster'; +const cluster = Cluster.default ?? Cluster as any as typeof Cluster.default; // ESM and CJS compatible approach +export { RemoteIPCPeer, RemoteIPCServerConfig } from './remote.js'; +export { IPCSocketPeer, NetIPCServer } from './socket.js'; + +const masterPeerId = '[master]'; + +interface INodeIPCMessage extends IMessage { + /** + * name of the target database. Needed when multiple database use the same communication channel + */ + dbname: string +} + +interface EventEmitterLike { + addListener(event: string, handler: (...args: any[]) => any): any; + removeListener(event: string, handler: (...args: any[]) => any): any; +} + +/** + * Node cluster functionality - enables vertical scaling with forked processes. AceBase will enable IPC at startup, so + * any forked process will communicate database changes and events automatically. Locking of resources will be done by + * the cluster's primary (previously master) process. NOTE: if the master process dies, all peers stop working + */ +export class IPCPeer extends AceBaseIPCPeer { + + constructor(storage: Storage, dbname: string) { + + // Throw eror on PM2 clusters --> they should use an AceBase IPC server + const pm2id = process.env?.NODE_APP_INSTANCE || process.env?.pm_id; + if (typeof pm2id === 'string' && pm2id !== '0') { + throw new Error(`To use AceBase with pm2 in cluster mode, use an AceBase IPC server to enable interprocess communication.`); + } + + const peerId = cluster.isMaster ? masterPeerId : cluster.worker.id.toString(); + super(storage, peerId, dbname); + + this.masterPeerId = masterPeerId; + this.ipcType = 'node.cluster'; + + /** Adds an event handler to a Node.js EventEmitter that is automatically removed upon IPC exit */ + const bindEventHandler = (target: EventEmitterLike, event: string, handler: (...args: any[]) => any) => { + target.addListener(event, handler); + this.on('exit', () => target.removeListener(event, handler)); + }; + + // Setup process exit handler + bindEventHandler(process, 'SIGINT', () => { + this.exit(); + }); + + if (cluster.isMaster) { + bindEventHandler(cluster, 'online', (worker: Cluster.Worker) => { + // A new worker is started + // Do not add yet, wait for "hello" message - a forked process might not use the same db + bindEventHandler(worker, 'error', err => { + this.logger.error(`Caught worker error:`, err); + }); + }); + + bindEventHandler(cluster, 'exit', (worker: Cluster.Worker) => { + // A worker has shut down + if (this.peers.find(peer => peer.id === worker.id.toString())) { + // Worker apparently did not have time to say goodbye, + // remove the peer ourselves + this.removePeer(worker.id.toString()); + + // Send "bye" message on their behalf + this.sayGoodbye(worker.id.toString()); + } + }); + } + + const handleMessage = (message: INodeIPCMessage) => { + if (typeof message !== 'object') { + // Ignore non-object IPC messages + return; + } + if (message.dbname !== this.dbname) { + // Ignore, message not meant for this database + return; + } + if (cluster.isMaster && message.to !== masterPeerId) { + // Message is meant for others (or all). Forward it + this.sendMessage(message); + } + if (message.to && message.to !== this.id) { + // Message is for somebody else. Ignore + return; + } + + return super.handleMessage(message); + }; + + if (cluster.isMaster) { + bindEventHandler(cluster, 'message', (worker: Cluster.Worker, message: INodeIPCMessage) => handleMessage(message)); + } + else { + bindEventHandler(cluster.worker, 'message', handleMessage); + } + + // if (!cluster.isMaster) { + // // Add master peer. Do we have to? + // this.addPeer(masterPeerId, false, false); + // } + + // Send hello to other peers + const helloMsg: IHelloMessage = { type: 'hello', from: this.id, data: undefined }; + this.sendMessage(helloMsg); + } + + public sendMessage(msg: IMessage) { + const message = msg as INodeIPCMessage; + message.dbname = this.dbname; + + if (cluster.isMaster) { + // If we are the master, send the message to the target worker(s) + this.peers + .filter(p => p.id !== message.from && (!message.to || p.id === message.to)) + .forEach(peer => { + const worker = cluster.workers[peer.id]; + worker && worker.send(message); // When debugging, worker might have stopped in the meantime + }); + } + else { + // Send the message to the master who will forward it to the target worker(s) + process.send(message); + } + } + + public async exit(code = 0) { + await super.exit(code); + } + +} diff --git a/src/ipc/ipc.ts b/src/ipc/ipc.ts index 398eb33..6441fef 100644 --- a/src/ipc/ipc.ts +++ b/src/ipc/ipc.ts @@ -1,6 +1,6 @@ import { ID, LoggerPlugin, SimpleEventEmitter } from 'acebase-core'; -import { NodeLocker, NodeLock, LOCK_STATE } from '../node-lock'; -import { Storage } from '../storage'; +import { NodeLocker, NodeLock, LOCK_STATE } from '../node-lock.js'; +import { Storage } from '../storage/index.js'; export class AceBaseIPCPeerExitingError extends Error { constructor(message: string) { super(`Exiting: ${message}`); } @@ -34,7 +34,7 @@ export abstract class AceBaseIPCPeer extends SimpleEventEmitter { storage.on('subscribe', (subscription: { path: string, event: string, callback: AceBaseSubscribeCallback }) => { // Subscription was added to db - this.logger.trace(`database subscription being added on peer ${this.id}`); + // this.logger.trace(`database subscription being added on peer ${this.id}`); const remoteSubscription = this.remoteSubscriptions.find(sub => sub.callback === subscription.callback); if (remoteSubscription) { @@ -452,7 +452,7 @@ export abstract class AceBaseIPCPeer extends SimpleEventEmitter { const req: IUnlockRequestMessage = { type: 'unlock-request', id: ID.generate(), from: this.id, to: this.masterPeerId, data: { id: lockInfo.lock.id } }; await this.request(req); lockInfo.lock.state = LOCK_STATE.DONE; - this.logger.trace(`Worker ${this.id} released lock ${lockInfo.lock.id} (tid ${lockInfo.lock.tid}, ${lockInfo.lock.comment}, "/${lockInfo.lock.path}", ${lockInfo.lock.forWriting ? 'write' : 'read'})`); + // this.logger.trace(`Worker ${this.id} released lock ${lockInfo.lock.id} (tid ${lockInfo.lock.tid}, ${lockInfo.lock.comment}, "/${lockInfo.lock.path}", ${lockInfo.lock.forWriting ? 'write' : 'read'})`); removeLock(lockInfo); }, moveToParent: async () => { diff --git a/src/ipc/remote.ts b/src/ipc/remote.ts index b643da6..1c922d0 100644 --- a/src/ipc/remote.ts +++ b/src/ipc/remote.ts @@ -1,326 +1,326 @@ -import { ID, Utils } from 'acebase-core'; -import { AceBaseIPCPeer, IMessage } from './ipc'; -import { Storage } from '../storage'; -import * as http from 'http'; - -import type * as wsTypes from 'ws'; // @types/ws must always available - -// type MessageEventCallback = (event: MessageEvent) => any; - -export interface RemoteIPCServerConfig { - dbname: string, - host?: string, - port: number, - ssl?: boolean, - token?: string, - role: 'master'|'worker', -} - -const masterPeerId = '[master]'; -const WS_CLOSE_PING_TIMEOUT = 1; -const WS_CLOSE_PROCESS_EXIT = 2; -// const WS_CLOSE_UNAUTHORIZED = 3; -// const WS_CLOSE_WRONG_CLIENT = 4; -// const WS_CLOSE_SERVER_ERROR = 5; - -/** - * Remote IPC using an external server. Database changes and events will be synchronized automatically. - * Locking of resources will be done by a single master that needs to be known up front. Preferably, the master - * is a process that handles no database updates itself and only manages data locking and allocation for workers. - * - * To use Remote IPC, you have to start the following processes: - * - 1 AceBase IPC Server process - * - 1 AceBase database master process (optional, used in example 1) - * - 1+ AceBase server worker processes - * - * NOTE if your IPC server will be running on a public host (not `localhost`), make sure to use `ssl` and a secret - * `token` in your IPC configuration. - * - * @example - * // IPC server process (start-ipc-server.js) - * const { AceBaseIPCServer } = require('acebase-ipc-server'); - * const server = new AceBaseIPCServer({ host: 'localhost', port: 9163 }) - * - * // Dedicated db master process (start-db-master.js) - * const { AceBase } = require('acebase'); - * const db = new AceBase('mydb', { storage: { ipc: { host: 'localhost', port: 9163, ssl: false, role: 'master' } } }); - * - * // Server worker processes (start-db-server.js) - * const { AceBaseServer } = require('acebase-server'); - * const server = new AceBaseServer('mydb', { host: 'localhost', port: 5757, storage: { ipc: { host: 'localhost', port: 9163, ssl: false, role: 'worker' } } }); - * - * // PM2 ecosystem.config.js: - * module.exports = { - * apps: [{ - * name: "AceBase IPC Server", - * script: "./start-ipc-server.js" - * }, { - * name: "AceBase database master", - * script: "./start-db-master.js" - * }, { - * name: "AceBase database server", - * script: "./start-db-server.js", - * instances: "-2", // Uses all CPUs minus 2 - * exec_mode: "cluster" // Enables PM2 load balancing, see https://pm2.keymetrics.io/docs/usage/cluster-mode/ - * }] - * } - * - * @description - * Instead of starting a dedicated db master process, you can also start 1 `AceBaseServer` with `role: "master"` manually. - * Note that the db master will also handle http requests for clients in this case, which might not be desirable because it - * also has to handle IPC master tasks for other clients. See the following example: - * - * @example - * // Another example using only 2 startup apps: - * - 1 instance: AceBase IPC server - * - Multiple instances of your app - * - * // IPC server process (start-ipc-server.js) - * const { AceBaseIPCServer } = require('acebase-ipc-server'); - * const server = new AceBaseIPCServer({ host: 'localhost', port: 9163 }) - * - * // Server worker processes (start-db-server.js) - * const { AceBaseServer } = require('acebase-server'); - * const role = process.env.NODE_APP_INSTANCE === '0' ? 'master' : 'worker'; - * const server = new AceBaseServer('mydb', { host: 'localhost', port: 5757, storage: { ipc: { host: 'localhost', port: 9163, ssl: false, role } } }); - * - * // PM2 ecosystem.config.js: - * module.exports = { - * apps: [{ - * name: "AceBase IPC Server", - * script: "./start-ipc-server.js", - * instances: 1 - * }, { - * name: "AceBase database server", - * script: "./start-db-server.js", - * instances: "-1", // Uses all CPUs minus 1 - * exec_mode: "cluster" // Enables PM2 load balancing - * }] - * } - */ -export class RemoteIPCPeer extends AceBaseIPCPeer { - - private get version() { return '1.0.0'; } - private ws: wsTypes.WebSocket; - private queue = true; - private pending: { - in: string[], - out: string[] - } = { in: [], out: [] }; - private maxPayload = 100; // Initial setting, will be overridden by server config once connected - - constructor(storage: Storage, private config: RemoteIPCServerConfig) { - super(storage, config.role === 'master' ? masterPeerId : ID.generate(), config.dbname); - this.masterPeerId = masterPeerId; - - this.connect().catch(err => { - this.logger.error(err.message); - this.exit(); - }); - } - - private async connect(options?: { maxRetries?: number }) { - const ws = await (async () => { - try { - return import('ws'); - } - catch { - throw new Error(`ws package is not installed. To fix this, run: npm install ws`); - } - })(); - return new Promise((resolve, reject) => { - let connected = false; - this.ws = new ws.WebSocket(`ws${this.config.ssl ? 's' : ''}://${this.config.host || 'localhost'}:${this.config.port}/${this.config.dbname}/connect?v=${this.version}&id=${this.id}&t=${this.config.token}`); // &role=${this.config.role} - - // Handle connection success - this.ws.addEventListener('open', async (/*event*/) => { - connected = true; - // Send any pending messages - this.pending.out.forEach(msg => { - this.ws.send(msg); - }); - this.pending.out = []; - this.queue = false; - resolve(); - }); - - // // Handle unexpected response (is documented at https://github.com/websockets/ws/blob/master/doc/ws.md#event-unexpected-response but doesn't appear to be working) - // (this.ws as any).addEventListener('unexpected-response', (req: http.ClientRequest, res: http.IncomingMessage) => { - // console.error(`Invalid response: ${res.statusCode} ${res.statusMessage}`); - // let closeCode; - // switch (res.statusCode) { - // case 401: closeCode = WS_CLOSE_UNAUTHORIZED; break; - // case 409: closeCode = WS_CLOSE_WRONG_CLIENT; break; - // case 500: closeCode = WS_CLOSE_SERVER_ERROR; break; - // } - // reject(new Error(`${res.statusCode} ${res.statusMessage}`)); - // }); - - // Handle connection error - this.ws.addEventListener('error', event => { - if (!connected) { - // We had no connection yet - if (event.message.includes('403')) { - reject(new Error('Cannot connect to IPC server: unauthorized')); - } - else if (event.message.includes('409')) { - reject(new Error('Cannot connect to IPC server: unsupported client version (too new or old)')); - } - else if (event.message.includes('500')) { - reject(new Error('Cannot connect to IPC server: server error')); - } - else if (typeof options?.maxRetries === 'undefined' || typeof options?.maxRetries === 'number' && options?.maxRetries > 0) { - const retryMs = 1000; // ms - this.logger.error(`Unable to connect to remote IPC server (${event.message}). Trying again in ${retryMs}ms`); - const retryOptions:{ maxRetries?: number } = {}; - if (typeof typeof options?.maxRetries === 'number') { retryOptions.maxRetries = options.maxRetries-1; } - const timeout = setTimeout(() => { this.connect(retryOptions); }, retryMs); - timeout.unref?.(); - } - else { - reject(event); - } - } - }); - - // Send pings if connection is idle to actively monitor connectivity - let lastMessageReceived = Date.now(); - const pingInterval = setInterval(() => { - if (this._exiting) { return; } - const ms = Date.now() - lastMessageReceived; - if (ms > 10000) { - // Timeout if we didn't get response within 10 seconds - this.ws.close(WS_CLOSE_PING_TIMEOUT); // close event that follows will reconnect - } - else if (ms > 5000) { - // No messages received for 5s. Sending ping to trigger pong response - this.ws.send('ping'); - } - }, 500); - pingInterval.unref?.(); - - // Close connection if we're exiting - process.once('exit', () => { - this.ws.close(WS_CLOSE_PROCESS_EXIT); - }); - - // Handle disconnect - this.ws.addEventListener('close', (/*event*/) => { - // Disconnected. Try reconnecting immediately - if (!connected) { return; } // We weren't connected yet. Don't reconnect here, retries will be executed automatically - if (this._exiting) { return; } - this.logger.error(`Connection to remote IPC server was lost. Trying to reconnect`); - clearInterval(pingInterval); - this.storage.invalidateCache?.(true, '', true, 'ipc_ws_disconnect'); // Make sure the entire cache is invalidated (AceBase storage has such cache) - this.connect(); - }); - - // Handle incoming messages - this.ws.addEventListener('message', async event => { - lastMessageReceived = Date.now(); - let str = event.data.toString(); - console.log(str); - if (str === 'pong') { - // We got a ping reply from the server - return; - } - else if (str.startsWith('welcome:')) { - // Welcome message with config - const config = JSON.parse(str.slice(8)); - this.maxPayload = config.maxPayload; - } - else if (str.startsWith('connect:')) { - // A new peer connected to the IPC server - // Do not add yet, wait for our own "hello" message - } - else if (str.startsWith('disconnect:')) { - // A peer has disconnected from the IPC server - const id = str.slice(11); - if (this.peers.find(peer => peer.id === id)) { - // Peer apparently did not have time to say goodbye, - // remove the peer ourselves - this.removePeer(id); - - // Send "bye" message on their behalf - this.sayGoodbye(id); - } - } - else if (str.startsWith('get:')) { - // Large message we have to fetch - const msgId = str.slice(4); - try { - str = await this.fetch('GET', `/${this.config.dbname}/receive?id=${this.id}&msg=${msgId}&t=${this.config.token}`); - const msg = JSON.parse(str); - super.handleMessage(msg); - } - catch (err) { - this.logger.error(`Failed to receive message ${msgId}:`, err); - } - } - else if (str.startsWith('{')) { - // Normal message - const msg = JSON.parse(str); - super.handleMessage(msg); - } - else { - // Unknown event - console.warn(`Received unknown IPC message: "${str}"`); - } - }); - }); - } - - sendMessage(message: IMessage) { - this.logger.trace(`[RemoteIPC] sending: `, message); - let json = JSON.stringify(message); - if (typeof message.to === 'string') { - // Send to specific peer only - json = `to:${message.to};${json}`; - } - if (this.queue) { - this.pending.out.push(json); - } - else if (json.length > this.maxPayload) { - this.fetch('POST', `/${this.dbname}/send?id=${this.id}&t=${this.config.token}`, json); - } - else { - this.ws.send(json); - } - } - - async fetch(method: 'GET'|'POST', path: string, postData?: string) { - const options = { - hostname: this.config.host || 'localhost', - port: this.config.port, - path, - method, - headers: { - 'Content-Type': 'application/json', - 'Content-Length': Buffer.byteLength(postData || ''), - }, - }; - return await new Promise((resolve, reject) => { - const req = http.request(options, (res) => { - // console.log(`STATUS: ${res.statusCode}`); - // console.log(`HEADERS: ${JSON.stringify(res.headers)}`); - res.setEncoding('utf8'); - - let data = ''; - res.on('data', (chunk) => { - data += chunk; - }); - res.on('end', () => { - resolve(data); - }); - }); - - req.on('error', reject); - - // Write data to request body - req.write(postData); - req.end(); - }); - } - - -} +import { ID, Utils } from 'acebase-core'; +import { AceBaseIPCPeer, IMessage } from './ipc.js'; +import { Storage } from '../storage/index.js'; +import * as http from 'http'; + +import type * as wsTypes from 'ws'; // @types/ws must always available + +// type MessageEventCallback = (event: MessageEvent) => any; + +export interface RemoteIPCServerConfig { + dbname: string, + host?: string, + port: number, + ssl?: boolean, + token?: string, + role: 'master'|'worker', +} + +const masterPeerId = '[master]'; +const WS_CLOSE_PING_TIMEOUT = 1; +const WS_CLOSE_PROCESS_EXIT = 2; +// const WS_CLOSE_UNAUTHORIZED = 3; +// const WS_CLOSE_WRONG_CLIENT = 4; +// const WS_CLOSE_SERVER_ERROR = 5; + +/** + * Remote IPC using an external server. Database changes and events will be synchronized automatically. + * Locking of resources will be done by a single master that needs to be known up front. Preferably, the master + * is a process that handles no database updates itself and only manages data locking and allocation for workers. + * + * To use Remote IPC, you have to start the following processes: + * - 1 AceBase IPC Server process + * - 1 AceBase database master process (optional, used in example 1) + * - 1+ AceBase server worker processes + * + * NOTE if your IPC server will be running on a public host (not `localhost`), make sure to use `ssl` and a secret + * `token` in your IPC configuration. + * + * @example + * // IPC server process (start-ipc-server.js) + * const { AceBaseIPCServer } = require('acebase-ipc-server'); + * const server = new AceBaseIPCServer({ host: 'localhost', port: 9163 }) + * + * // Dedicated db master process (start-db-master.js) + * const { AceBase } = require('acebase'); + * const db = new AceBase('mydb', { storage: { ipc: { host: 'localhost', port: 9163, ssl: false, role: 'master' } } }); + * + * // Server worker processes (start-db-server.js) + * const { AceBaseServer } = require('acebase-server'); + * const server = new AceBaseServer('mydb', { host: 'localhost', port: 5757, storage: { ipc: { host: 'localhost', port: 9163, ssl: false, role: 'worker' } } }); + * + * // PM2 ecosystem.config.js: + * module.exports = { + * apps: [{ + * name: "AceBase IPC Server", + * script: "./start-ipc-server.js" + * }, { + * name: "AceBase database master", + * script: "./start-db-master.js" + * }, { + * name: "AceBase database server", + * script: "./start-db-server.js", + * instances: "-2", // Uses all CPUs minus 2 + * exec_mode: "cluster" // Enables PM2 load balancing, see https://pm2.keymetrics.io/docs/usage/cluster-mode/ + * }] + * } + * + * @description + * Instead of starting a dedicated db master process, you can also start 1 `AceBaseServer` with `role: "master"` manually. + * Note that the db master will also handle http requests for clients in this case, which might not be desirable because it + * also has to handle IPC master tasks for other clients. See the following example: + * + * @example + * // Another example using only 2 startup apps: + * - 1 instance: AceBase IPC server + * - Multiple instances of your app + * + * // IPC server process (start-ipc-server.js) + * const { AceBaseIPCServer } = require('acebase-ipc-server'); + * const server = new AceBaseIPCServer({ host: 'localhost', port: 9163 }) + * + * // Server worker processes (start-db-server.js) + * const { AceBaseServer } = require('acebase-server'); + * const role = process.env.NODE_APP_INSTANCE === '0' ? 'master' : 'worker'; + * const server = new AceBaseServer('mydb', { host: 'localhost', port: 5757, storage: { ipc: { host: 'localhost', port: 9163, ssl: false, role } } }); + * + * // PM2 ecosystem.config.js: + * module.exports = { + * apps: [{ + * name: "AceBase IPC Server", + * script: "./start-ipc-server.js", + * instances: 1 + * }, { + * name: "AceBase database server", + * script: "./start-db-server.js", + * instances: "-1", // Uses all CPUs minus 1 + * exec_mode: "cluster" // Enables PM2 load balancing + * }] + * } + */ +export class RemoteIPCPeer extends AceBaseIPCPeer { + + private get version() { return '1.0.0'; } + private ws: wsTypes.WebSocket; + private queue = true; + private pending: { + in: string[], + out: string[] + } = { in: [], out: [] }; + private maxPayload = 100; // Initial setting, will be overridden by server config once connected + + constructor(storage: Storage, private config: RemoteIPCServerConfig) { + super(storage, config.role === 'master' ? masterPeerId : ID.generate(), config.dbname); + this.masterPeerId = masterPeerId; + + this.connect().catch(err => { + this.logger.error(err.message); + this.exit(); + }); + } + + private async connect(options?: { maxRetries?: number }) { + const ws = await (async () => { + try { + return import('ws'); + } + catch { + throw new Error(`ws package is not installed. To fix this, run: npm install ws`); + } + })(); + return new Promise((resolve, reject) => { + let connected = false; + this.ws = new ws.WebSocket(`ws${this.config.ssl ? 's' : ''}://${this.config.host || 'localhost'}:${this.config.port}/${this.config.dbname}/connect?v=${this.version}&id=${this.id}&t=${this.config.token}`); // &role=${this.config.role} + + // Handle connection success + this.ws.addEventListener('open', async (/*event*/) => { + connected = true; + // Send any pending messages + this.pending.out.forEach(msg => { + this.ws.send(msg); + }); + this.pending.out = []; + this.queue = false; + resolve(); + }); + + // // Handle unexpected response (is documented at https://github.com/websockets/ws/blob/master/doc/ws.md#event-unexpected-response but doesn't appear to be working) + // (this.ws as any).addEventListener('unexpected-response', (req: http.ClientRequest, res: http.IncomingMessage) => { + // console.error(`Invalid response: ${res.statusCode} ${res.statusMessage}`); + // let closeCode; + // switch (res.statusCode) { + // case 401: closeCode = WS_CLOSE_UNAUTHORIZED; break; + // case 409: closeCode = WS_CLOSE_WRONG_CLIENT; break; + // case 500: closeCode = WS_CLOSE_SERVER_ERROR; break; + // } + // reject(new Error(`${res.statusCode} ${res.statusMessage}`)); + // }); + + // Handle connection error + this.ws.addEventListener('error', event => { + if (!connected) { + // We had no connection yet + if (event.message.includes('403')) { + reject(new Error('Cannot connect to IPC server: unauthorized')); + } + else if (event.message.includes('409')) { + reject(new Error('Cannot connect to IPC server: unsupported client version (too new or old)')); + } + else if (event.message.includes('500')) { + reject(new Error('Cannot connect to IPC server: server error')); + } + else if (typeof options?.maxRetries === 'undefined' || typeof options?.maxRetries === 'number' && options?.maxRetries > 0) { + const retryMs = 1000; // ms + this.logger.error(`Unable to connect to remote IPC server (${event.message}). Trying again in ${retryMs}ms`); + const retryOptions:{ maxRetries?: number } = {}; + if (typeof typeof options?.maxRetries === 'number') { retryOptions.maxRetries = options.maxRetries-1; } + const timeout = setTimeout(() => { this.connect(retryOptions); }, retryMs); + timeout.unref?.(); + } + else { + reject(event); + } + } + }); + + // Send pings if connection is idle to actively monitor connectivity + let lastMessageReceived = Date.now(); + const pingInterval = setInterval(() => { + if (this._exiting) { return; } + const ms = Date.now() - lastMessageReceived; + if (ms > 10000) { + // Timeout if we didn't get response within 10 seconds + this.ws.close(WS_CLOSE_PING_TIMEOUT); // close event that follows will reconnect + } + else if (ms > 5000) { + // No messages received for 5s. Sending ping to trigger pong response + this.ws.send('ping'); + } + }, 500); + pingInterval.unref?.(); + + // Close connection if we're exiting + process.once('exit', () => { + this.ws.close(WS_CLOSE_PROCESS_EXIT); + }); + + // Handle disconnect + this.ws.addEventListener('close', (/*event*/) => { + // Disconnected. Try reconnecting immediately + if (!connected) { return; } // We weren't connected yet. Don't reconnect here, retries will be executed automatically + if (this._exiting) { return; } + this.logger.error(`Connection to remote IPC server was lost. Trying to reconnect`); + clearInterval(pingInterval); + this.storage.invalidateCache?.(true, '', true, 'ipc_ws_disconnect'); // Make sure the entire cache is invalidated (AceBase storage has such cache) + this.connect(); + }); + + // Handle incoming messages + this.ws.addEventListener('message', async event => { + lastMessageReceived = Date.now(); + let str = event.data.toString(); + console.log(str); + if (str === 'pong') { + // We got a ping reply from the server + return; + } + else if (str.startsWith('welcome:')) { + // Welcome message with config + const config = JSON.parse(str.slice(8)); + this.maxPayload = config.maxPayload; + } + else if (str.startsWith('connect:')) { + // A new peer connected to the IPC server + // Do not add yet, wait for our own "hello" message + } + else if (str.startsWith('disconnect:')) { + // A peer has disconnected from the IPC server + const id = str.slice(11); + if (this.peers.find(peer => peer.id === id)) { + // Peer apparently did not have time to say goodbye, + // remove the peer ourselves + this.removePeer(id); + + // Send "bye" message on their behalf + this.sayGoodbye(id); + } + } + else if (str.startsWith('get:')) { + // Large message we have to fetch + const msgId = str.slice(4); + try { + str = await this.fetch('GET', `/${this.config.dbname}/receive?id=${this.id}&msg=${msgId}&t=${this.config.token}`); + const msg = JSON.parse(str); + super.handleMessage(msg); + } + catch (err) { + this.logger.error(`Failed to receive message ${msgId}:`, err); + } + } + else if (str.startsWith('{')) { + // Normal message + const msg = JSON.parse(str); + super.handleMessage(msg); + } + else { + // Unknown event + console.warn(`Received unknown IPC message: "${str}"`); + } + }); + }); + } + + sendMessage(message: IMessage) { + this.logger.trace(`[RemoteIPC] sending: `, message); + let json = JSON.stringify(message); + if (typeof message.to === 'string') { + // Send to specific peer only + json = `to:${message.to};${json}`; + } + if (this.queue) { + this.pending.out.push(json); + } + else if (json.length > this.maxPayload) { + this.fetch('POST', `/${this.dbname}/send?id=${this.id}&t=${this.config.token}`, json); + } + else { + this.ws.send(json); + } + } + + async fetch(method: 'GET'|'POST', path: string, postData?: string) { + const options = { + hostname: this.config.host || 'localhost', + port: this.config.port, + path, + method, + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(postData || ''), + }, + }; + return await new Promise((resolve, reject) => { + const req = http.request(options, (res) => { + // console.log(`STATUS: ${res.statusCode}`); + // console.log(`HEADERS: ${JSON.stringify(res.headers)}`); + res.setEncoding('utf8'); + + let data = ''; + res.on('data', (chunk) => { + data += chunk; + }); + res.on('end', () => { + resolve(data); + }); + }); + + req.on('error', reject); + + // Write data to request body + req.write(postData); + req.end(); + }); + } + + +} diff --git a/src/ipc/service/index.ts b/src/ipc/service/index.ts index d783f99..d25f83b 100644 --- a/src/ipc/service/index.ts +++ b/src/ipc/service/index.ts @@ -1,113 +1,142 @@ -import { createServer, Socket } from 'net'; -import { getSocketPath } from './shared'; -import { AceBase, type AceBaseLocalSettings } from '../../'; -import { DebugLogger, LoggerPlugin } from 'acebase-core'; - -const ERROR = Object.freeze({ - ALREADY_RUNNING: { code: 'already_running', exitCode: 2 }, - UNKNOWN: { code: 'unknown', exitCode: 3 }, - NO_DB: { code: 'no_db', exitCode: 4 }, -}); - -export async function startServer( - dbFile: string, - options: { - /** path to code that returns an initialized logger plugin */ - loggerPluginPath?: string, - logLevel: AceBaseLocalSettings['logLevel'], - maxIdleTime: number, - exit: (code: number) => void - } -) { - const fileMatch = dbFile.match(/^(?.*([\\\/]))(?.+)\.acebase\2(?[a-z]+)\.db$/); - if (!fileMatch) { - return options.exit(ERROR.NO_DB.exitCode); - } - const { storagePath, dbName, storageType } = fileMatch.groups; - const logger = options.loggerPluginPath - ? await (async () => { - const logger = await import(options.loggerPluginPath); - return (logger.default ?? logger) as LoggerPlugin; - })() - : new DebugLogger(options.logLevel, `[IPC service ${dbName}:${storageType}]`); - let db: AceBase; // Will be opened when listening - - const sockets = [] as Socket[]; - - const socketPath = getSocketPath(dbFile); - logger.info(`[starting socket server on path ${socketPath}`); - - const server = createServer(); - server.listen({ - path: socketPath, - readableAll: true, - writableAll: true, - }); - - server.on('listening', () => { - // Started successful - process.on('SIGINT', () => server.close()); - process.on('exit', (code) => { - logger.info(`exiting with code ${code}`); - }); - - // Start the "master" IPC client - db = new AceBase(dbName, { logLevel: options.logLevel, logger, storage: { type: storageType, path: storagePath, ipc: server } }); - }); - - server.on('error', (err: Error & { code: string }) => { - if (err.code === 'EADDRINUSE') { - logger.info(`socket server already running`); - return options.exit(ERROR.ALREADY_RUNNING.exitCode); - } - logger.error(`socket server error ${err.code ?? err.message}`); - options.exit(ERROR.UNKNOWN.exitCode); - }); - - let connectionsMade = false; - server.on('connection', (socket) => { - // New socket connected handler - connectionsMade = true; - sockets.push(socket); - logger.info(`socket connected, total: ${sockets.length}`); - - socket.on('close', (hadError) => { - // Socket is closed - sockets.splice(sockets.indexOf(socket), 1); - logger.info(`socket disconnected${hadError ? ' because of an error' : ''}, total: ${sockets.length}`); - if (sockets.length === 0) { - const stop = () => { - logger.info(`closing server socket because there are no more connected clients, exiting with code 0`); - // Stop socket server - server.close((err) => { - options.exit(err ? ERROR.UNKNOWN.exitCode : 0); - }); - }; - if (options.maxIdleTime > 0) { - setTimeout(() => { - if (sockets.length === 0) { stop(); } - }, 5000); - } - else { - stop(); - } - } - }); - }); - - server.on('close', () => { - db.close(); - }); - - if (options.maxIdleTime > 0) { - setTimeout(() => { - if (!connectionsMade) { - logger.info(`closing server socket because no clients connected, exiting with code 0`); - // Stop socket server - server.close((err) => { - options.exit(err ? ERROR.UNKNOWN.exitCode : 0); - }); - } - }, options.maxIdleTime).unref(); - } -} +import { createServer, connect, Socket } from 'net'; +import { unlink } from 'fs'; +import { DebugLogger, LoggerPlugin } from 'acebase-core'; +import { getSocketPath } from './shared.js'; +import { AceBase, type AceBaseLocalSettings } from '../../index.js'; + +const ERROR = Object.freeze({ + ALREADY_RUNNING: { code: 'already_running', exitCode: 2 }, + UNKNOWN: { code: 'unknown', exitCode: 3 }, + NO_DB: { code: 'no_db', exitCode: 4 }, +}); + +export async function startServer( + dbFile: string, + options: { + /** path to code that returns an initialized logger plugin */ + loggerPluginPath?: string, + logLevel: AceBaseLocalSettings['logLevel'], + maxIdleTime: number, + exit: (code: number) => void + } +) { + const fileMatch = dbFile.match(/^(?.*([\\\/]))(?.+)\.acebase\2(?[a-z]+)\.db$/); + if (!fileMatch) { + return options.exit(ERROR.NO_DB.exitCode); + } + const { storagePath, dbName, storageType } = fileMatch.groups; + const logger = options.loggerPluginPath + ? await (async () => { + const logger = await import(options.loggerPluginPath); + return (logger.default ?? logger) as LoggerPlugin; + })() + : new DebugLogger(options.logLevel, `[IPC service ${dbName}:${storageType}]`); + let db: AceBase; // Will be opened when listening + + const sockets = [] as Socket[]; + + const socketPath = getSocketPath(dbFile); + logger.info(`[starting socket server on path ${socketPath}]`); + + const server = createServer(); + + server.on('listening', () => { + // Started successful + process.on('SIGINT', () => server.close()); + process.on('exit', (code) => { + logger.info(`exiting with code ${code}`); + }); + + // Start the "master" IPC client + db = new AceBase(dbName, { logLevel: options.logLevel, logger, storage: { type: storageType, path: storagePath, ipc: server } }); + }); + + server.on('error', (err: Error & { code: string }) => { + if (err.code === 'EADDRINUSE') { + // Race condition: another process grabbed the socket between our pre-check and listen call. + // Test-connect to confirm it's truly already running. + const testClient = connect({ path: socketPath }); + testClient.once('connect', () => { + testClient.destroy(); + logger.info(`socket server already running`); + options.exit(ERROR.ALREADY_RUNNING.exitCode); + }); + testClient.once('error', () => { + logger.error(`socket server error: EADDRINUSE but nothing is listening`); + options.exit(ERROR.UNKNOWN.exitCode); + }); + return; + } + logger.error(`socket server error ${err.code ?? err.message}`); + options.exit(ERROR.UNKNOWN.exitCode); + }); + + let connectionsMade = false; + server.on('connection', (socket) => { + // New socket connected handler + connectionsMade = true; + sockets.push(socket); + logger.info(`socket connected, total: ${sockets.length}`); + + socket.on('close', (hadError) => { + // Socket is closed + sockets.splice(sockets.indexOf(socket), 1); + logger.info(`socket disconnected${hadError ? ' because of an error' : ''}, total: ${sockets.length}`); + if (sockets.length === 0) { + const stop = () => { + logger.info(`closing server socket because there are no more connected clients, exiting with code 0`); + // Stop socket server + server.close((err) => { + options.exit(err ? ERROR.UNKNOWN.exitCode : 0); + }); + }; + if (options.maxIdleTime > 0) { + setTimeout(() => { + if (sockets.length === 0) { stop(); } + }, 5000); + } + else { + stop(); + } + } + }); + }); + + server.on('close', () => { + // Clean up socket file so subsequent runs don't hit EADDRINUSE + unlink(socketPath, () => { /* ignore errors on close cleanup */ }); + db?.close(); + }); + + if (options.maxIdleTime > 0) { + setTimeout(() => { + if (!connectionsMade) { + logger.info(`closing server socket because no clients connected, exiting with code 0`); + // Stop socket server + server.close((err) => { + options.exit(err ? ERROR.UNKNOWN.exitCode : 0); + }); + } + }, options.maxIdleTime).unref(); + } + + // Before listening, check whether a server is already running or a stale socket file exists. + // This is the common case on macOS: socket files persist after process exit. + const testClient = connect({ path: socketPath }); + testClient.once('connect', () => { + testClient.destroy(); + logger.info(`socket server already running`); + options.exit(ERROR.ALREADY_RUNNING.exitCode); + }); + testClient.once('error', () => { + // Nothing is listening (file absent, stale, or ECONNREFUSED) — remove any leftover and bind + unlink(socketPath, (unlinkErr) => { + if (unlinkErr && (unlinkErr as NodeJS.ErrnoException).code !== 'ENOENT') { + logger.error(`Failed to remove stale socket: ${unlinkErr.message}`); + options.exit(ERROR.UNKNOWN.exitCode); + return; + } + server.listen({ path: socketPath, readableAll: true, writableAll: true }); + }); + }); +} diff --git a/src/ipc/service/shared.ts b/src/ipc/service/shared.ts index 1ebb819..f2a6880 100644 --- a/src/ipc/service/shared.ts +++ b/src/ipc/service/shared.ts @@ -1,18 +1,17 @@ import { createHash } from 'crypto'; export function getSocketPath(filePath: string) { - let path = process.platform ==='win32' - ? `\\\\.\\pipe\\${filePath.replace(/^\//, '').replace(/\//g, '-')}` - : `${filePath}.sock`; - const maxLength = process.platform ==='win32' ? 256 : 108; - if (path.length > maxLength) { - // Use hash of filepath instead - const hash = createHash('sha256').update(path).digest('hex'); - path = process.platform ==='win32' - ? `\\\\.\\pipe\\${hash}` - : `${hash}.sock`; + const match = filePath.match(/[\\\/]([^\\\/]+)\.acebase[\\\/]([^\\\/]+)\.([^.\\\/]+)$/); + const dbName = match ? match[1] : 'db'; + const dbType = match ? match[2] : 'data'; + const shortHash = createHash('sha256').update(filePath).digest('hex').slice(0, 8); + if (process.platform === 'win32') { + // Create named pipe + return `\\\\.\\pipe\\${dbName}-${dbType}-${shortHash}.acebase`; + } else { + // Create Unix socket at /tmp/[dbName]-[dbType]-[shortHash].acebase.sock + return `/tmp/${dbName}-${dbType}-${shortHash}.acebase.sock`; } - return path; } // export const MSG_DELIMITER = String.fromCharCode(0) + String.fromCharCode(1) + String.fromCharCode(3) + String.fromCharCode(5) + String.fromCharCode(0); diff --git a/src/ipc/service/start.ts b/src/ipc/service/start.ts index f97395f..d72d2a7 100644 --- a/src/ipc/service/start.ts +++ b/src/ipc/service/start.ts @@ -1,29 +1,29 @@ -import { startServer } from '.'; -import { type AceBaseLocalSettings } from '../../'; - -(async () => { - try { - const dbFile = process.argv[2]; // full path to db storage file, eg '/home/ewout/project/db.acebase/data.db' - const settings = process.argv.slice(3).reduce((settings, arg, i, args) => { - switch (arg.toLowerCase()) { - case '--loglevel': settings.logLevel = args[i + 1] as AceBaseLocalSettings['logLevel']; break; - case '--maxidletime': settings.maxIdleTime = parseInt(args[i + 1]); break; - case '--logger': settings.loggerPluginPath = args[i + 1]; break; - } - return settings; - }, { logLevel: 'log', maxIdleTime: 5000 } as { loggerPluginPath?: string, logLevel: AceBaseLocalSettings['logLevel'], maxIdleTime: number }); - - await startServer(dbFile, { - loggerPluginPath: settings.loggerPluginPath, - logLevel: settings.logLevel, - maxIdleTime: settings.maxIdleTime, - exit: (code) => { - process.exit(code); - }, - }); - } - catch (err) { - console.error(`Start error:`, err); - process.exit(1); - } -})(); +import { startServer } from './index.js'; +import { type AceBaseLocalSettings } from '../../index.js'; + +(async () => { + try { + const dbFile = process.argv[2]; // full path to db storage file, eg '/home/ewout/project/db.acebase/data.db' + const settings = process.argv.slice(3).reduce((settings, arg, i, args) => { + switch (arg.toLowerCase()) { + case '--loglevel': settings.logLevel = args[i + 1] as AceBaseLocalSettings['logLevel']; break; + case '--maxidletime': settings.maxIdleTime = parseInt(args[i + 1]); break; + case '--logger': settings.loggerPluginPath = args[i + 1]; break; + } + return settings; + }, { logLevel: 'log', maxIdleTime: 5000 } as { loggerPluginPath?: string, logLevel: AceBaseLocalSettings['logLevel'], maxIdleTime: number }); + + await startServer(dbFile, { + loggerPluginPath: settings.loggerPluginPath, + logLevel: settings.logLevel, + maxIdleTime: settings.maxIdleTime, + exit: (code) => { + process.exit(code); + }, + }); + } + catch (err) { + console.error(`Start error:`, err); + process.exit(1); + } +})(); diff --git a/src/ipc/socket.ts b/src/ipc/socket.ts index 309abf1..6b94b6c 100644 --- a/src/ipc/socket.ts +++ b/src/ipc/socket.ts @@ -1,269 +1,272 @@ -import { Socket, connect, Server } from 'net'; -import { resolve as resolvePath } from 'path'; -import { spawn } from 'child_process'; -import { AceBaseIPCPeer, IHelloMessage, IMessage } from './ipc'; -import { Storage } from '../storage'; -import { DebugLogger, ID, Transport } from 'acebase-core'; -import { getSocketPath, MSG_DELIMITER } from './service/shared'; -import { startServer } from './service'; -export { Server as NetIPCServer } from 'net'; - -const masterPeerId = '[master]'; - -interface EventEmitterLike { - addListener?(event: string, handler: (...args: any[]) => any): any; - removeListener?(event: string, handler: (...args: any[]) => any): any; - on?(event: string, handler: (...args: any[]) => any): any; - off?(event: string, handler: (...args: any[]) => any): any; -} - -/** - * Socket IPC implementation. Peers will attempt starting up a dedicated service process for the target database, - * or connect to an already running process. The service acts as the IPC master and governs over locks, space allocation - * and communication between peers. Communication between the processes is done using (very fast in-memory) Unix sockets. - * This IPC implementation allows different processes on a single machine to access the same database simultaniously without - * them having to explicitly configure their IPC settings. - * Currently can be used by passing `ipc: 'socket'` in AceBase's `storage` settings, will become the default soon. - */ -export class IPCSocketPeer extends AceBaseIPCPeer { - - public server?: Server; - - constructor(storage: Storage, ipcSettings: { ipcName: string; server: Server | null; maxIdleTime?: number; loggerPluginPath?: string }) { - - const isMaster = storage.settings.ipc instanceof Server; - const peerId = isMaster ? masterPeerId : ID.generate(); - super(storage, peerId, ipcSettings.ipcName); - this.server = ipcSettings.server; - - this.masterPeerId = masterPeerId; - this.ipcType = 'node.socket'; - - const dbFile = resolvePath(storage.path, `${storage.settings.type}.db`); - const socketPath = getSocketPath(dbFile); - - /** Adds an event handler that is automatically removed upon IPC exit */ - const bindEventHandler = (target: EventEmitterLike, event: string, handler: (...args: any[]) => any) => { - (target.on ?? target.addListener).bind(target)(event, handler); - this.on('exit', () => (target.off ?? target.removeListener).bind(target)(event, handler)); - }; - - // Setup process exit handler - bindEventHandler(process, 'SIGINT', () => { - this.exit(); - }); - - if (!isMaster) { - // Try starting IPC service if it is not running yet. - const args = [ - __dirname + '/service/start.js', - dbFile, - ...(this.logger instanceof DebugLogger ? ['--loglevel', this.logger.level] : []), - ...(ipcSettings.loggerPluginPath ? ['--logger', ipcSettings.loggerPluginPath] : []), - '--maxidletime', ipcSettings.maxIdleTime?.toString() ?? '0', // Use maxIdleTime 0 to allow tests to remove database files when done - ]; - const service = spawn('node', args, { detached: true, stdio: 'ignore' }); - service.unref(); // Process is detached and allowed to keep running after we exit. Do not keep a reference to it, possibly preventing app exit. - - // // For testing: - // startServer(dbFile, { - // maxIdleTime: 0, - // ...(this.logger instanceof DebugLogger && { logLevel: this.logger.level }), - // ...(ipcSettings.loggerPluginPath && { loggerPluginPath: ipcSettings.loggerPluginPath }), - // exit: (code) => { - // this.logger.info(`[IPC ${ipcSettings.ipcName}] service exited with code ${code}`); - // }, - // }); - } - - /** - * Socket connection with master (workers only) - */ - let socket: Socket | null = null; - let connected = false; - const queue = [] as IMessage[]; - - /** - * Maps peers to IPC sockets (master only) - */ - const peerSockets = isMaster ? new Map() : null; - - const handleMessage = (socket: Socket, message: IMessage) => { - if (typeof message !== 'object') { - // Ignore non-object IPC messages - return; - } - if (isMaster && message.to !== masterPeerId) { - // Message is meant for others (or all). Forward it - this.sendMessage(message); - } - if (message.to && message.to !== this.id) { - // Message is for somebody else. Ignore - return; - } - if (this.isMaster) { - if (message.type === 'hello') { - // Bind peer id to incoming socket - peerSockets.set(message.from, socket); - } - else if (message.type === 'bye') { - // Remove bound socket for peer - peerSockets.delete(message.from); - } - } - return super.handleMessage(message); - }; - - if (isMaster) { - this.server.on('connection', (socket) => { - // New socket connected. We don't know which peer it is until we get a "hello" message - let buffer = Buffer.alloc(0); // Buffer to store incomplete messages - socket.on('data', chunk => { - // Received data from a worker - buffer = Buffer.concat([buffer, chunk]); - - while (buffer.length > 0) { - const delimiterIndex = buffer.indexOf(MSG_DELIMITER); - if (delimiterIndex === -1) { - break; // wait for more data - } - - // Extract message from buffer - const message = buffer.subarray(0, delimiterIndex); - buffer = buffer.subarray(delimiterIndex + MSG_DELIMITER.length); - - try { - const json = message.toString('utf-8'); - // this.logger.debug(`[IPC ${ipcSettings.ipcName}] Received socket message: `, json); - const serialized = JSON.parse(json); - const msg = Transport.deserialize2(serialized); - handleMessage(socket, msg); - } - catch (err) { - this.logger.error(`[IPC ${ipcSettings.ipcName}] Error parsing message: ${err}`); - } - } - }); - socket.on('close', (hadError) => { - // socket has disconnected. Find registered peer - for (const [peerId, peerSocket] of peerSockets.entries()) { - if (peerSocket === socket) { - // Worker apparently did not have time to say goodbye, - // remove the peer ourselves - this.removePeer(peerId); - - // Send "bye" message on their behalf - this.sayGoodbye(peerId); - break; - } - } - }); - }); - } - else { - const connectSocket = async (path: string) => { - const tryConnect = async (tries: number): Promise => { - try { - if (this._exiting) { return; } - const s = connect({ path }); - await new Promise((resolve, reject) => { - s.once('error', reject).unref(); - s.once('connect', resolve).unref(); - }); - this.logger.info(`[IPC ${ipcSettings.ipcName}] peer ${this.id} successfully established connection to the service`); - socket = s; - connected = true; - } - catch (err) { - if (tries < 100) { - // Retry in 10ms - await new Promise(resolve => setTimeout(resolve, 100)); - return tryConnect(tries + 1); - } - this.logger.error(`[IPC ${ipcSettings.ipcName}] peer ${this.id} cannot connect to service: ${err.message}`); - throw err; - } - }; - await tryConnect(1); - - this.once('exit', () => { - socket?.destroy(); - }); - - bindEventHandler(socket, 'close', (hadError) => { - // Connection to server closed - this.logger.info(`IPC peer ${this.id} lost its connection to the service${hadError ? ' because of an error' : ''}`); - }); - - let buffer = Buffer.alloc(0); // Buffer to store incomplete messages - bindEventHandler(socket, 'data', chunk => { - // Received data from server - buffer = Buffer.concat([buffer, chunk]); - - while (buffer.length > 0) { - const delimiterIndex = buffer.indexOf(MSG_DELIMITER); - if (delimiterIndex === -1) { - break; // wait for more data - } - - // Extract message from buffer - const message = buffer.subarray(0, delimiterIndex); - buffer = buffer.subarray(delimiterIndex + MSG_DELIMITER.length); - - try { - const json = message.toString('utf-8'); - // this.logger.debug(`Received server message: `, json); - const serialized = JSON.parse(json); - const msg = Transport.deserialize2(serialized); - handleMessage(socket, msg); - } - catch (err) { - this.logger.error(`Error parsing message: ${err}`); - } - } - }); - - connected = true; - while (queue.length) { - const message = queue.shift(); - this.sendMessage(message); - } - }; - connectSocket(socketPath); - } - - this.sendMessage = (message: IMessage) => { - const serialized = Transport.serialize2(message); - const buffer = Buffer.from(JSON.stringify(serialized) + MSG_DELIMITER); - if (this.isMaster) { - // We are the master, send the message to the target worker(s) - this.peers - .filter(p => p.id !== message.from && (!message.to || p.id === message.to)) - .forEach(peer => { - const socket = peerSockets.get(peer.id); - socket?.write(buffer); - }); - } - else if (connected) { - // Send the message to the master who will forward it to the target worker(s) - socket.write(buffer); - } - else { - // Not connected yet, queue message - queue.push(message); - } - }; - - // Send hello to other peers - const helloMsg: IHelloMessage = { type: 'hello', from: this.id, data: undefined }; - this.sendMessage(helloMsg); - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - sendMessage(message: IMessage) { throw new Error('Must be set by constructor'); } - - public async exit(code = 0) { - await super.exit(code); - } - -} +import { Socket, connect, Server } from 'net'; +import { resolve as resolvePath, join as joinPaths } from 'path'; +import { spawn } from 'child_process'; +import { AceBaseIPCPeer, IHelloMessage, IMessage } from './ipc.js'; +import { Storage } from '../storage/index.js'; +import { DebugLogger, ID, Transport } from 'acebase-core'; +import { getSocketPath, MSG_DELIMITER } from './service/shared.js'; +import { startServer } from './service/index.js'; +export { Server as NetIPCServer } from 'net'; + +const DEBUG_MODE = false; // Enable to start IPC service in this process for easy debugging +const masterPeerId = '[master]'; + +interface EventEmitterLike { + addListener?(event: string, handler: (...args: any[]) => any): any; + removeListener?(event: string, handler: (...args: any[]) => any): any; + on?(event: string, handler: (...args: any[]) => any): any; + off?(event: string, handler: (...args: any[]) => any): any; +} + +/** + * Socket IPC implementation. Peers will attempt starting up a dedicated service process for the target database, + * or connect to an already running process. The service acts as the IPC master and governs over locks, space allocation + * and communication between peers. Communication between the processes is done using (very fast in-memory) Unix sockets. + * This IPC implementation allows different processes on a single machine to access the same database simultaniously without + * them having to explicitly configure their IPC settings. + * Currently can be used by passing `ipc: 'socket'` in AceBase's `storage` settings, will become the default soon. + */ +export class IPCSocketPeer extends AceBaseIPCPeer { + + public server?: Server; + + constructor(storage: Storage, ipcSettings: { ipcName: string; server: Server | null; maxIdleTime?: number; loggerPluginPath?: string }) { + + const isMaster = storage.settings.ipc instanceof Server; + const peerId = isMaster ? masterPeerId : ID.generate(); + super(storage, peerId, ipcSettings.ipcName); + this.server = ipcSettings.server; + + this.masterPeerId = masterPeerId; + this.ipcType = 'node.socket'; + + const dbFile = resolvePath(storage.path, `${storage.settings.type}.db`); + const socketPath = getSocketPath(dbFile); + + /** Adds an event handler that is automatically removed upon IPC exit */ + const bindEventHandler = (target: EventEmitterLike, event: string, handler: (...args: any[]) => any) => { + (target.on ?? target.addListener).bind(target)(event, handler); + this.on('exit', () => (target.off ?? target.removeListener).bind(target)(event, handler)); + }; + + // Setup process exit handler + bindEventHandler(process, 'SIGINT', () => { + this.exit(); + }); + + if (!isMaster) { + if (DEBUG_MODE) { + // Easy debugging option: start the IPC service in this process + startServer(dbFile, { + maxIdleTime: 0, + logLevel: this.logger instanceof DebugLogger ? this.logger.level : 'warn', + ...(ipcSettings.loggerPluginPath && { loggerPluginPath: ipcSettings.loggerPluginPath }), + exit: (code) => { + this.logger.info(`[IPC ${ipcSettings.ipcName}] service exited with code ${code}`); + }, + }); + } else { + // Try starting IPC service if it is not running yet. + const args = [ + joinPaths(__dirname, '/service/start.js'), + dbFile, + ...(this.logger instanceof DebugLogger ? ['--loglevel', this.logger.level] : []), + ...(ipcSettings.loggerPluginPath ? ['--logger', ipcSettings.loggerPluginPath] : []), + '--maxidletime', ipcSettings.maxIdleTime?.toString() ?? '0', // Use maxIdleTime 0 to allow tests to remove database files when done + ]; + const service = spawn(process.execPath, args, { detached: true, stdio: 'ignore' }); + service.unref(); // Process is detached and allowed to keep running after we exit. Do not keep a reference to it, possibly preventing app exit. + } + } + + /** + * Socket connection with master (workers only) + */ + let socket: Socket | null = null; + let connected = false; + const queue = [] as IMessage[]; + + /** + * Maps peers to IPC sockets (master only) + */ + const peerSockets = isMaster ? new Map() : null; + + const handleMessage = (socket: Socket, message: IMessage) => { + if (typeof message !== 'object') { + // Ignore non-object IPC messages + return; + } + if (isMaster && message.to !== masterPeerId) { + // Message is meant for others (or all). Forward it + this.sendMessage(message); + } + if (message.to && message.to !== this.id) { + // Message is for somebody else. Ignore + return; + } + if (this.isMaster) { + if (message.type === 'hello') { + // Bind peer id to incoming socket + peerSockets.set(message.from, socket); + } + else if (message.type === 'bye') { + // Remove bound socket for peer + peerSockets.delete(message.from); + } + } + return super.handleMessage(message); + }; + + if (isMaster) { + this.server.on('connection', (socket) => { + // New socket connected. We don't know which peer it is until we get a "hello" message + let buffer = Buffer.alloc(0); // Buffer to store incomplete messages + socket.on('data', chunk => { + // Received data from a worker + buffer = Buffer.concat([buffer, chunk]); + + while (buffer.length > 0) { + const delimiterIndex = buffer.indexOf(MSG_DELIMITER); + if (delimiterIndex === -1) { + break; // wait for more data + } + + // Extract message from buffer + const message = buffer.subarray(0, delimiterIndex); + buffer = buffer.subarray(delimiterIndex + MSG_DELIMITER.length); + + try { + const json = message.toString('utf-8'); + // this.logger.debug(`[IPC ${ipcSettings.ipcName}] Received socket message: `, json); + const serialized = JSON.parse(json); + const msg = Transport.deserialize2(serialized); + handleMessage(socket, msg); + } + catch (err) { + this.logger.error(`[IPC ${ipcSettings.ipcName}] Error parsing message: ${err}`); + } + } + }); + socket.on('close', (hadError) => { + // socket has disconnected. Find registered peer + for (const [peerId, peerSocket] of peerSockets.entries()) { + if (peerSocket === socket) { + // Worker apparently did not have time to say goodbye, + // remove the peer ourselves + this.removePeer(peerId); + + // Send "bye" message on their behalf + this.sayGoodbye(peerId); + break; + } + } + }); + }); + } + else { + const connectSocket = async (path: string) => { + const tryConnect = async (tries: number): Promise => { + try { + if (this._exiting) { return; } + const s = connect({ path }); + await new Promise((resolve, reject) => { + s.once('error', reject).unref(); + s.once('connect', resolve).unref(); + }); + this.logger.info(`[IPC ${ipcSettings.ipcName}] peer ${this.id} successfully established connection to the service`); + socket = s; + connected = true; + } + catch (err) { + if (tries < 100) { + // Retry in 10ms + await new Promise(resolve => setTimeout(resolve, 100)); + return tryConnect(tries + 1); + } + this.logger.error(`[IPC ${ipcSettings.ipcName}] peer ${this.id} cannot connect to service: ${err.message}`); + throw err; + } + }; + await tryConnect(1); + + this.once('exit', () => { + socket?.destroy(); + }); + + bindEventHandler(socket, 'close', (hadError) => { + // Connection to server closed + this.logger.info(`IPC peer ${this.id} lost its connection to the service${hadError ? ' because of an error' : ''}`); + }); + + let buffer = Buffer.alloc(0); // Buffer to store incomplete messages + bindEventHandler(socket, 'data', chunk => { + // Received data from server + buffer = Buffer.concat([buffer, chunk]); + + while (buffer.length > 0) { + const delimiterIndex = buffer.indexOf(MSG_DELIMITER); + if (delimiterIndex === -1) { + break; // wait for more data + } + + // Extract message from buffer + const message = buffer.subarray(0, delimiterIndex); + buffer = buffer.subarray(delimiterIndex + MSG_DELIMITER.length); + + try { + const json = message.toString('utf-8'); + // this.logger.debug(`Received server message: `, json); + const serialized = JSON.parse(json); + const msg = Transport.deserialize2(serialized); + handleMessage(socket, msg); + } + catch (err) { + this.logger.error(`Error parsing message: ${err}`); + } + } + }); + + connected = true; + while (queue.length) { + const message = queue.shift(); + this.sendMessage(message); + } + }; + connectSocket(socketPath); + } + + this.sendMessage = (message: IMessage) => { + const serialized = Transport.serialize2(message); + const buffer = Buffer.from(JSON.stringify(serialized) + MSG_DELIMITER); + if (this.isMaster) { + // We are the master, send the message to the target worker(s) + this.peers + .filter(p => p.id !== message.from && (!message.to || p.id === message.to)) + .forEach(peer => { + const socket = peerSockets.get(peer.id); + socket?.write(buffer); + }); + } + else if (connected) { + // Send the message to the master who will forward it to the target worker(s) + socket.write(buffer); + } + else { + // Not connected yet, queue message + queue.push(message); + } + }; + + // Send hello to other peers + const helloMsg: IHelloMessage = { type: 'hello', from: this.id, data: undefined }; + this.sendMessage(helloMsg); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + sendMessage(message: IMessage) { throw new Error('Must be set by constructor'); } + + public async exit(code = 0) { + await super.exit(code); + } + +} diff --git a/src/node-cache.ts b/src/node-cache.ts index 5137cf6..0045aee 100644 --- a/src/node-cache.ts +++ b/src/node-cache.ts @@ -1,7 +1,7 @@ // TODO: Rename to NodeInfoCache -import { NodeInfo } from './node-info'; import { PathInfo } from 'acebase-core'; +import { NodeInfo } from './node-info.js'; // const SECOND = 1000; const MINUTE = 60_000; diff --git a/src/node-info.ts b/src/node-info.ts index 0c310d2..da1dcf8 100644 --- a/src/node-info.ts +++ b/src/node-info.ts @@ -1,6 +1,6 @@ -import { NodeValueType, getValueTypeName } from './node-value-types'; import { PathInfo } from 'acebase-core'; -import { NodeAddress } from './node-address'; +import { NodeValueType, getValueTypeName } from './node-value-types.js'; +import { NodeAddress } from './node-address.js'; export class NodeInfo { path?: string; diff --git a/src/node-lock.ts b/src/node-lock.ts index 4aa9d61..1e46c93 100644 --- a/src/node-lock.ts +++ b/src/node-lock.ts @@ -1,5 +1,5 @@ import { PathInfo, ID, LoggerPlugin } from 'acebase-core'; -import { assert } from './assert'; +import { assert } from './assert.js'; const DEBUG_MODE = false; const DEFAULT_LOCK_TIMEOUT = 120; // in seconds diff --git a/src/node-transaction.spec.ts b/src/node-transaction.spec.ts index ed9e5f6..0155614 100644 --- a/src/node-transaction.spec.ts +++ b/src/node-transaction.spec.ts @@ -1,5 +1,5 @@ // Test the new NodeTransaction class being developed -import { TransactionManager, NodeLockIntention } from './node-transaction'; +import { TransactionManager, NodeLockIntention } from './node-transaction.js'; describe('NodeTransaction (beta)', () => { diff --git a/src/node-transaction.ts b/src/node-transaction.ts index 429c145..37a5a55 100644 --- a/src/node-transaction.ts +++ b/src/node-transaction.ts @@ -1,5 +1,5 @@ import { PathInfo } from 'acebase-core'; -import { IPCPeer } from './ipc'; +import { IPCPeer } from './ipc/index.js'; const SECOND = 1_000; const MINUTE = 60_000; diff --git a/src/node.ts b/src/node.ts index 7c74ee4..75cddd8 100644 --- a/src/node.ts +++ b/src/node.ts @@ -1,4 +1,4 @@ -import { VALUE_TYPES } from './node-value-types'; +import { VALUE_TYPES } from './node-value-types.js'; export class Node { static get VALUE_TYPES() { diff --git a/src/query.ts b/src/query.ts index f60652b..e180d6a 100644 --- a/src/query.ts +++ b/src/query.ts @@ -1,10 +1,10 @@ import { ID, PathInfo } from 'acebase-core'; import type { EventSubscriptionCallback, Query, QueryOptions, QueryFilter, QueryOrder } from 'acebase-core'; -import { VALUE_TYPES } from './node-value-types'; -import { NodeNotFoundError } from './node-errors'; -import { DataIndex, FullTextIndex, IndexQueryResults } from './data-index'; -import { AsyncTaskBatch } from './async-task-batch'; -import type { LocalApi } from './api-local'; +import { VALUE_TYPES } from './node-value-types.js'; +import { NodeNotFoundError } from './node-errors.js'; +import { DataIndex, FullTextIndex, IndexQueryResults } from './data-index/index.js'; +import { AsyncTaskBatch } from './async-task-batch.js'; +import type { LocalApi } from './api-local.js'; // eslint-disable-next-line @typescript-eslint/no-empty-function const noop = () => {}; diff --git a/src/quicksort.spec.ts b/src/quicksort.spec.ts index f4c2d9e..ae80a92 100644 --- a/src/quicksort.spec.ts +++ b/src/quicksort.spec.ts @@ -1,4 +1,4 @@ -import quickSort from './quicksort'; +import quickSort from './quicksort.js'; import { performance } from 'perf_hooks'; describe('quicksort', () => { diff --git a/src/storage/binary/browser.ts b/src/storage/binary/browser.ts index ba6c858..b80808a 100644 --- a/src/storage/binary/browser.ts +++ b/src/storage/binary/browser.ts @@ -1,4 +1,4 @@ -import { NotSupported } from '../../not-supported'; +import { NotSupported } from '../../not-supported.js'; /** * Not supported in browser context diff --git a/src/storage/binary/node-address.ts b/src/storage/binary/node-address.ts index a012b61..3493564 100644 --- a/src/storage/binary/node-address.ts +++ b/src/storage/binary/node-address.ts @@ -1,4 +1,4 @@ -import { NodeAddress } from '../../node-address'; +import { NodeAddress } from '../../node-address.js'; export class BinaryNodeAddress extends NodeAddress { constructor( diff --git a/src/storage/binary/node-info.ts b/src/storage/binary/node-info.ts index 44684ee..fb75f94 100644 --- a/src/storage/binary/node-info.ts +++ b/src/storage/binary/node-info.ts @@ -1,5 +1,5 @@ -import { NodeInfo } from '../../node-info'; -import { BinaryNodeAddress } from './node-address'; +import { NodeInfo } from '../../node-info.js'; +import { BinaryNodeAddress } from './node-address.js'; export class BinaryNodeInfo extends NodeInfo { address?: BinaryNodeAddress; diff --git a/src/storage/binary/node-reader.ts b/src/storage/binary/node-reader.ts index fdca092..c0b675a 100644 --- a/src/storage/binary/node-reader.ts +++ b/src/storage/binary/node-reader.ts @@ -5,7 +5,7 @@ import { BinaryBPlusTree, BinaryBPlusTreeLeaf } from '../../btree/index.js'; import { BinaryNodeAddress } from './node-address.js'; import { RecordInfo } from './record-info.js'; import { AceBaseStorage } from './binary-storage.js'; -import { IAceBaseIPCLock } from '../../ipc/ipc'; +import { IAceBaseIPCLock } from '../../ipc/ipc.js'; import { NodeAllocation } from './node-allocation.js'; import { BinaryNodeInfo } from './node-info.js'; import { StorageAddressRange } from './binary-storage-address-range.js'; diff --git a/src/storage/binary/serialized-key-value.ts b/src/storage/binary/serialized-key-value.ts index 42cabe6..2e080c9 100644 --- a/src/storage/binary/serialized-key-value.ts +++ b/src/storage/binary/serialized-key-value.ts @@ -1,4 +1,4 @@ -import { BinaryNodeAddress } from './node-address'; +import { BinaryNodeAddress } from './node-address.js'; // TODO @appy-one consider converting to interface export class SerializedKeyValue { diff --git a/src/storage/binary/test.spec.ts b/src/storage/binary/test.spec.ts index dce2c15..60ec01a 100644 --- a/src/storage/binary/test.spec.ts +++ b/src/storage/binary/test.spec.ts @@ -1,5 +1,5 @@ -import { AceBase } from '../..'; -import { createTempDB } from '../../test/tempdb'; +import { AceBase } from '../../index.js'; +import { createTempDB } from '../../test/tempdb.js'; describe('issue', () => { let db: AceBase, removeDB: () => Promise; diff --git a/src/storage/context.ts b/src/storage/context.ts index 7697c63..712c9ee 100644 --- a/src/storage/context.ts +++ b/src/storage/context.ts @@ -1,11 +1,11 @@ -import type { LoggerPlugin } from 'acebase-core'; -import type { Storage } from '.'; -import type { DataIndex } from '../data-index'; -import type { AceBaseIPCPeer } from '../ipc/ipc'; - -export interface IndexesContext { - storage: Storage, - logger: LoggerPlugin, - ipc: AceBaseIPCPeer, - indexes: DataIndex[], -} +import type { LoggerPlugin } from 'acebase-core'; +import type { Storage } from './index.js'; +import type { DataIndex } from '../data-index/index.js'; +import type { AceBaseIPCPeer } from '../ipc/ipc.js'; + +export interface IndexesContext { + storage: Storage, + logger: LoggerPlugin, + ipc: AceBaseIPCPeer, + indexes: DataIndex[], +} diff --git a/src/storage/create-index.ts b/src/storage/create-index.ts index 786ca6f..224baf8 100644 --- a/src/storage/create-index.ts +++ b/src/storage/create-index.ts @@ -1,127 +1,127 @@ -import { ColorStyle } from 'acebase-core'; -import { DataIndex, ArrayIndex, FullTextIndex, GeoIndex } from '../data-index'; -import { pfs } from '../promise-fs'; -import { IndexesContext } from './context'; - -export interface CreateIndexOptions { - rebuild?: boolean; - - /** - * special index to create: 'array', 'fulltext' or 'geo' - */ - type?: 'normal' | 'array' | 'fulltext' | 'geo'; - - /** - * keys to include with the indexed values. Can be used to speed up results sorting and - * to quickly apply additional filters. - */ - include?: string[]; - - /** - * Specifies whether texts should be indexed using case sensitivity. Setting this to `true` - * will cause words with mixed casings (eg "word", "Word" and "WORD") to be indexed separately. - * Default is `false` - * @default false - */ - caseSensitive?: boolean; - - /** - * Specifies the default locale of indexed texts. Used to convert indexed strings - * to lowercase if `caseSensitive` is set to `true`. - * Should be a 2-character language code such as "en" for English and "nl" for Dutch, - * or an LCID string for country specific locales such as "en-us" for American English, - * "en-gb" for British English, etc - */ - textLocale?: string; - - /** - * Specifies a key in the source data that contains the locale to use - * instead of the default specified in `textLocale` - */ - textLocaleKey?: string; - - /** - * additional index-specific configuration settings - */ - config?: any -} - -/** -* Creates an index on specified path and key(s) -* @param path location of objects to be indexed. Eg: "users" to index all children of the "users" node; or "chats/*\/members" to index all members of all chats -* @param key for now - one key to index. Once our B+tree implementation supports nested trees, we can allow multiple fields -*/ -export async function createIndex( - context: IndexesContext, - path: string, - key: string, - options: CreateIndexOptions, -): Promise { - if (!context.storage.indexes.supported) { - throw new Error('Indexes are not supported in current environment because it requires Node.js fs'); - } - // path = path.replace(/\/\*$/, ""); // Remove optional trailing "/*" - const { ipc, logger, indexes, storage } = context; - - 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 && options.config) { - // Additional index config params are not saved to index files, apply them to the in-memory index now - (existingIndex as any).config = options.config; - } - - if (existingIndex && rebuild !== true) { - logger.info(`Index on "/${path}/*/${key}" already exists`.colorize(ColorStyle.inverse)); - return existingIndex; - } - - if (!ipc.isMaster) { - // Pass create request to master - const result = await ipc.sendRequest({ type: 'index.create', path, key, options }); - if (result.ok) { - return storage.indexes.add(result.fileName); - } - throw new Error(result.reason); - } - - await pfs.mkdir(`${storage.settings.path}/${storage.name}.acebase`).catch(err => { - if (err.code !== 'EEXIST') { - throw err; - } - }); - - const index = existingIndex || (() => { - const { include, caseSensitive, textLocale, textLocaleKey } = options; - const indexOptions = { include, caseSensitive, textLocale, textLocaleKey }; - switch (indexType) { - case 'array': return new ArrayIndex(storage, path, key, { ...indexOptions }); - case 'fulltext': return new FullTextIndex(storage, path, key, { ...indexOptions, config: options.config }); - case 'geo': return new GeoIndex(storage, path, key, { ...indexOptions }); - default: return new DataIndex(storage, path, key, { ...indexOptions }); - } - })(); - if (!existingIndex) { - indexes.push(index); - } - try { - await index.build(); - } - catch(err) { - context.logger.error(`Index build on "/${path}/*/${key}" failed: ${err.message} (code: ${err.code})`.colorize(ColorStyle.red)); - if (!existingIndex) { - // Only remove index if we added it. Build may have failed because someone tried creating the index more than once, or rebuilding it while it was building... - indexes.splice(indexes.indexOf(index), 1); - } - throw err; - } - ipc.sendNotification({ type: 'index.created', fileName: index.fileName, path, key, options }); - return index; -} +import { ColorStyle } from 'acebase-core'; +import { DataIndex, ArrayIndex, FullTextIndex, GeoIndex } from '../data-index/index.js'; +import { pfs } from '../promise-fs/index.js'; +import { IndexesContext } from './context.js'; + +export interface CreateIndexOptions { + rebuild?: boolean; + + /** + * special index to create: 'array', 'fulltext' or 'geo' + */ + type?: 'normal' | 'array' | 'fulltext' | 'geo'; + + /** + * keys to include with the indexed values. Can be used to speed up results sorting and + * to quickly apply additional filters. + */ + include?: string[]; + + /** + * Specifies whether texts should be indexed using case sensitivity. Setting this to `true` + * will cause words with mixed casings (eg "word", "Word" and "WORD") to be indexed separately. + * Default is `false` + * @default false + */ + caseSensitive?: boolean; + + /** + * Specifies the default locale of indexed texts. Used to convert indexed strings + * to lowercase if `caseSensitive` is set to `true`. + * Should be a 2-character language code such as "en" for English and "nl" for Dutch, + * or an LCID string for country specific locales such as "en-us" for American English, + * "en-gb" for British English, etc + */ + textLocale?: string; + + /** + * Specifies a key in the source data that contains the locale to use + * instead of the default specified in `textLocale` + */ + textLocaleKey?: string; + + /** + * additional index-specific configuration settings + */ + config?: any +} + +/** +* Creates an index on specified path and key(s) +* @param path location of objects to be indexed. Eg: "users" to index all children of the "users" node; or "chats/*\/members" to index all members of all chats +* @param key for now - one key to index. Once our B+tree implementation supports nested trees, we can allow multiple fields +*/ +export async function createIndex( + context: IndexesContext, + path: string, + key: string, + options: CreateIndexOptions, +): Promise { + if (!context.storage.indexes.supported) { + throw new Error('Indexes are not supported in current environment because it requires Node.js fs'); + } + // path = path.replace(/\/\*$/, ""); // Remove optional trailing "/*" + const { ipc, logger, indexes, storage } = context; + + 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 && options.config) { + // Additional index config params are not saved to index files, apply them to the in-memory index now + (existingIndex as any).config = options.config; + } + + if (existingIndex && rebuild !== true) { + logger.info(`Index on "/${path}/*/${key}" already exists`.colorize(ColorStyle.inverse)); + return existingIndex; + } + + if (!ipc.isMaster) { + // Pass create request to master + const result = await ipc.sendRequest({ type: 'index.create', path, key, options }); + if (result.ok) { + return storage.indexes.add(result.fileName); + } + throw new Error(result.reason); + } + + await pfs.mkdir(`${storage.settings.path}/${storage.name}.acebase`).catch(err => { + if (err.code !== 'EEXIST') { + throw err; + } + }); + + const index = existingIndex || (() => { + const { include, caseSensitive, textLocale, textLocaleKey } = options; + const indexOptions = { include, caseSensitive, textLocale, textLocaleKey }; + switch (indexType) { + case 'array': return new ArrayIndex(storage, path, key, { ...indexOptions }); + case 'fulltext': return new FullTextIndex(storage, path, key, { ...indexOptions, config: options.config }); + case 'geo': return new GeoIndex(storage, path, key, { ...indexOptions }); + default: return new DataIndex(storage, path, key, { ...indexOptions }); + } + })(); + if (!existingIndex) { + indexes.push(index); + } + try { + await index.build(); + } + catch(err) { + context.logger.error(`Index build on "/${path}/*/${key}" failed: ${err.message} (code: ${err.code})`.colorize(ColorStyle.red)); + if (!existingIndex) { + // Only remove index if we added it. Build may have failed because someone tried creating the index more than once, or rebuilding it while it was building... + indexes.splice(indexes.indexOf(index), 1); + } + throw err; + } + ipc.sendNotification({ type: 'index.created', fileName: index.fileName, path, key, options }); + return index; +} diff --git a/src/storage/custom/index.ts b/src/storage/custom/index.ts index 9f03aa7..87b23c0 100644 --- a/src/storage/custom/index.ts +++ b/src/storage/custom/index.ts @@ -1,14 +1,14 @@ import { ID, PathReference, PathInfo, ascii85, ColorStyle, Utils, DebugLogger } from 'acebase-core'; const { compareValues } = Utils; -import { NodeInfo } from '../../node-info'; -import { NodeLock, NodeLocker } from '../../node-lock'; -import { NodeValueType, VALUE_TYPES } from '../../node-value-types'; -import { NodeNotFoundError, NodeRevisionError } from '../../node-errors'; -import { Storage, StorageEnv, StorageSettings } from '../index'; -import { CustomStorageHelpers } from './helpers'; -import { NodeAddress } from '../../node-address'; -import { assert } from '../../assert'; -export { CustomStorageHelpers } from './helpers'; +import { NodeInfo } from '../../node-info.js'; +import { NodeLock, NodeLocker } from '../../node-lock.js'; +import { NodeValueType, VALUE_TYPES } from '../../node-value-types.js'; +import { NodeNotFoundError, NodeRevisionError } from '../../node-errors.js'; +import { Storage, StorageEnv, StorageSettings } from '../index.js'; +import { CustomStorageHelpers } from './helpers.js'; +import { NodeAddress } from '../../node-address.js'; +import { assert } from '../../assert.js'; +export { CustomStorageHelpers } from './helpers.js'; /** Interface for metadata being stored for nodes */ export class ICustomStorageNodeMetaData { diff --git a/src/storage/custom/indexed-db/index.ts b/src/storage/custom/indexed-db/index.ts index fd15434..918406b 100644 --- a/src/storage/custom/indexed-db/index.ts +++ b/src/storage/custom/indexed-db/index.ts @@ -1,8 +1,8 @@ import { SimpleCache } from 'acebase-core'; -import { CustomStorageSettings, ICustomStorageNode } from '..'; -import { AceBase } from '../../..'; -import { IndexedDBStorageSettings } from './settings'; -import { IndexedDBStorageTransaction, IndexedDBTransactionContext } from './transaction'; +import { CustomStorageSettings, ICustomStorageNode } from '../index.js'; +import { AceBase } from '../../../index.js'; +import { IndexedDBStorageSettings } from './settings.js'; +import { IndexedDBStorageTransaction, IndexedDBTransactionContext } from './transaction.js'; export function createIndexedDBInstance(dbname: string, init: Partial = {}) { const settings = new IndexedDBStorageSettings(init); diff --git a/src/storage/custom/indexed-db/settings.ts b/src/storage/custom/indexed-db/settings.ts index aa78f63..119a679 100644 --- a/src/storage/custom/indexed-db/settings.ts +++ b/src/storage/custom/indexed-db/settings.ts @@ -1,5 +1,5 @@ import { LoggingLevel } from 'acebase-core'; -import { StorageSettings } from '../..'; +import { StorageSettings } from '../../index.js'; export class IndexedDBStorageSettings extends StorageSettings { /** diff --git a/src/storage/custom/indexed-db/transaction.ts b/src/storage/custom/indexed-db/transaction.ts index e740b65..1e65e2c 100644 --- a/src/storage/custom/indexed-db/transaction.ts +++ b/src/storage/custom/indexed-db/transaction.ts @@ -1,6 +1,6 @@ import { SimpleCache } from 'acebase-core'; -import { CustomStorageHelpers, CustomStorageTransaction, ICustomStorageNode, ICustomStorageNodeMetaData } from '..'; -import { IPCPeer } from '../../../ipc'; +import { CustomStorageHelpers, CustomStorageTransaction, ICustomStorageNode, ICustomStorageNodeMetaData } from '../index.js'; +import { IPCPeer } from '../../../ipc/index.js'; interface IIndexedDBNodeData { path: string; diff --git a/src/storage/custom/local-storage/index.ts b/src/storage/custom/local-storage/index.ts index 114d2b7..9ca8692 100644 --- a/src/storage/custom/local-storage/index.ts +++ b/src/storage/custom/local-storage/index.ts @@ -1,7 +1,7 @@ -import { CustomStorageSettings } from '..'; -import { AceBase } from '../../..'; -import { LocalStorageSettings } from './settings'; -import { LocalStorageTransaction } from './transaction'; +import { CustomStorageSettings } from '../index.js'; +import { AceBase } from '../../../index.js'; +import { LocalStorageSettings } from './settings.js'; +import { LocalStorageTransaction } from './transaction.js'; export { LocalStorageSettings, LocalStorageTransaction }; diff --git a/src/storage/custom/local-storage/settings.ts b/src/storage/custom/local-storage/settings.ts index 2a042a7..263fac4 100644 --- a/src/storage/custom/local-storage/settings.ts +++ b/src/storage/custom/local-storage/settings.ts @@ -1,6 +1,6 @@ import { LoggingLevel } from 'acebase-core'; -import { StorageSettings } from '../..'; -import { LocalStorageLike } from './interface'; +import { StorageSettings } from '../../index.js'; +import { LocalStorageLike } from './interface.js'; export class LocalStorageSettings extends StorageSettings { constructor(settings: Partial) { diff --git a/src/storage/custom/local-storage/transaction.ts b/src/storage/custom/local-storage/transaction.ts index a3f5522..8718ff0 100644 --- a/src/storage/custom/local-storage/transaction.ts +++ b/src/storage/custom/local-storage/transaction.ts @@ -1,5 +1,5 @@ -import { CustomStorageHelpers, CustomStorageTransaction, ICustomStorageNode, ICustomStorageNodeMetaData } from '..'; -import { LocalStorageLike } from './interface'; +import { CustomStorageHelpers, CustomStorageTransaction, ICustomStorageNode, ICustomStorageNodeMetaData } from '../index.js'; +import { LocalStorageLike } from './interface.js'; // Setup CustomStorageTransaction for browser's LocalStorage export class LocalStorageTransaction extends CustomStorageTransaction { diff --git a/src/storage/indexes.ts b/src/storage/indexes.ts index 5debf16..af7e359 100644 --- a/src/storage/indexes.ts +++ b/src/storage/indexes.ts @@ -1 +1 @@ -export { createIndex, CreateIndexOptions } from './create-index'; +export { createIndex, CreateIndexOptions } from './create-index.js'; diff --git a/src/storage/mssql/browser.ts b/src/storage/mssql/browser.ts index 329d729..4116c48 100644 --- a/src/storage/mssql/browser.ts +++ b/src/storage/mssql/browser.ts @@ -1,4 +1,4 @@ -import { NotSupported } from '../../not-supported'; +import { NotSupported } from '../../not-supported.js'; /** * Not supported in browser context diff --git a/src/storage/mssql/index.ts b/src/storage/mssql/index.ts index 166ef3b..5cc3ebf 100644 --- a/src/storage/mssql/index.ts +++ b/src/storage/mssql/index.ts @@ -1,11 +1,11 @@ import { ID, PathReference, PathInfo, ascii85, ColorStyle } from 'acebase-core'; -import { Storage, StorageEnv, StorageSettings } from '..'; -import { NodeInfo } from '../../node-info'; -import { NodeValueType, VALUE_TYPES } from '../../node-value-types'; -import { NodeNotFoundError, NodeRevisionError } from '../../node-errors'; -import { pfs } from '../../promise-fs'; -import { NodeAddress } from '../../node-address'; -import { assert } from '../../assert'; +import { Storage, StorageEnv, StorageSettings } from '../index.js'; +import { NodeInfo } from '../../node-info.js'; +import { NodeValueType, VALUE_TYPES } from '../../node-value-types.js'; +import { NodeNotFoundError, NodeRevisionError } from '../../node-errors.js'; +import { pfs } from '../../promise-fs/index.js'; +import { NodeAddress } from '../../node-address.js'; +import { assert } from '../../assert.js'; export class MSSQLNodeAddress extends NodeAddress { constructor(containerPath: string) { diff --git a/src/storage/sqlite/browser.ts b/src/storage/sqlite/browser.ts index 448b655..f5c0396 100644 --- a/src/storage/sqlite/browser.ts +++ b/src/storage/sqlite/browser.ts @@ -1,4 +1,4 @@ -import { NotSupported } from '../../not-supported'; +import { NotSupported } from '../../not-supported.js'; /** * Not supported in browser context diff --git a/src/storage/sqlite/index.ts b/src/storage/sqlite/index.ts index 15e142a..3d01f0a 100644 --- a/src/storage/sqlite/index.ts +++ b/src/storage/sqlite/index.ts @@ -1,12 +1,12 @@ import { ID, PathReference, PathInfo, ascii85, ColorStyle } from 'acebase-core'; -import { Storage, StorageEnv, StorageSettings } from '..'; -import { NodeInfo } from '../../node-info'; -import { VALUE_TYPES } from '../../node-value-types'; -import { NodeNotFoundError, NodeRevisionError } from '../../node-errors'; -import { pfs } from '../../promise-fs'; -import { ThreadSafe } from '../../thread-safe'; -import { NodeAddress } from '../../node-address'; -import { assert } from '../../assert'; +import { Storage, StorageEnv, StorageSettings } from '../index.js'; +import { NodeInfo } from '../../node-info.js'; +import { VALUE_TYPES } from '../../node-value-types.js'; +import { NodeNotFoundError, NodeRevisionError } from '../../node-errors.js'; +import { pfs } from '../../promise-fs/index.js'; +import { ThreadSafe } from '../../thread-safe.js'; +import { NodeAddress } from '../../node-address.js'; +import { assert } from '../../assert.js'; export class SQLiteNodeAddress extends NodeAddress { constructor(containerPath: string) { diff --git a/src/test/arrays.spec.ts b/src/test/arrays.spec.ts index 673cb2d..22dc8a4 100644 --- a/src/test/arrays.spec.ts +++ b/src/test/arrays.spec.ts @@ -1,4 +1,4 @@ -import { createTempDB } from './tempdb'; +import { createTempDB } from './tempdb.js'; describe('arrays', () => { it('test array updates', async () => { diff --git a/src/test/bulk-import.spec.ts b/src/test/bulk-import.spec.ts index 3ba961a..b48da38 100644 --- a/src/test/bulk-import.spec.ts +++ b/src/test/bulk-import.spec.ts @@ -1,5 +1,5 @@ -import { AceBase } from '..'; -import { createTempDB } from './tempdb'; +import { AceBase } from '../index.js'; +import { createTempDB } from './tempdb.js'; import { ID } from 'acebase-core'; // This test takes at least an hour on a fast system, enable only if you have time diff --git a/src/test/constructor.spec.ts b/src/test/constructor.spec.ts index 24ef637..8904c30 100644 --- a/src/test/constructor.spec.ts +++ b/src/test/constructor.spec.ts @@ -1,4 +1,4 @@ -import { AceBase, ID } from '..'; +import { AceBase, ID } from '../index.js'; import { readdir, rm, rmdir } from 'fs/promises'; const removeDB = async (db: AceBase) => { diff --git a/src/test/corruption-handling.spec.ts b/src/test/corruption-handling.spec.ts index e3e209c..8eb24ef 100644 --- a/src/test/corruption-handling.spec.ts +++ b/src/test/corruption-handling.spec.ts @@ -1,5 +1,5 @@ -import { AceBase, DataSnapshot } from '..'; -import { createTempDB } from './tempdb'; +import { AceBase, DataSnapshot } from '../index.js'; +import { createTempDB } from './tempdb.js'; describe('Corrupted records', () => { let db: AceBase, removeDB: () => Promise; diff --git a/src/test/data-proxy.spec.ts b/src/test/data-proxy.spec.ts index 25f9b51..2b087f1 100644 --- a/src/test/data-proxy.spec.ts +++ b/src/test/data-proxy.spec.ts @@ -1,7 +1,7 @@ -import { createTempDB } from './tempdb'; +import { createTempDB } from './tempdb.js'; import { proxyAccess, ObjectCollection, ILiveDataProxyValue } from 'acebase-core'; import * as Util from 'util'; -import { readDataSet } from './dataset'; +import { readDataSet } from './dataset.js'; const util: typeof Util = (Util as any).default ?? Util; describe('DataProxy', () => { diff --git a/src/test/data-types.spec.ts b/src/test/data-types.spec.ts index c75a039..783fdf4 100644 --- a/src/test/data-types.spec.ts +++ b/src/test/data-types.spec.ts @@ -1,6 +1,6 @@ import { PathReference } from 'acebase-core'; -import { AceBase } from '..'; -import { createTempDB } from './tempdb'; +import { AceBase } from '../index.js'; +import { createTempDB } from './tempdb.js'; describe('Data type', () => { let db: AceBase, removeDB: () => Promise; diff --git a/src/test/dataset.ts b/src/test/dataset.ts index 4b9af12..f8a267c 100644 --- a/src/test/dataset.ts +++ b/src/test/dataset.ts @@ -8,6 +8,6 @@ export async function readDataSet(name: string) { } export function getDataSetPath(name: string) { - const path = resolve(__dirname, '../../../spec/dataset'); // dir relative to dist/[cjs|esm]/test + const path = resolve(process.cwd(), 'spec/dataset'); return `${path}/${name}.json`; } diff --git a/src/test/events.spec.ts b/src/test/events.spec.ts index 5a40c75..bed5777 100644 --- a/src/test/events.spec.ts +++ b/src/test/events.spec.ts @@ -1,6 +1,6 @@ import { MutationsDataSnapshot } from 'acebase-core'; -import { AceBase, DataSnapshot } from '..'; -import { createTempDB } from './tempdb'; +import { AceBase, DataSnapshot } from '../index.js'; +import { createTempDB } from './tempdb.js'; describe('Event', () => { let db: AceBase, removeDB: () => Promise; diff --git a/src/test/examples.spec.ts b/src/test/examples.spec.ts index 96ec926..8dd757f 100644 --- a/src/test/examples.spec.ts +++ b/src/test/examples.spec.ts @@ -1,5 +1,5 @@ -import { AceBase } from '..'; -import { createTempDB } from './tempdb'; +import { AceBase } from '../index.js'; +import { createTempDB } from './tempdb.js'; describe('Examples', () => { let db: AceBase, removeDB: () => Promise; @@ -95,9 +95,12 @@ describe('Examples', () => { const now = new Date(); await questionRef.update({ edited: now } as any); - // In the next tick, the live proxy value will have updated: - process.nextTick(() => { - liveQuestion.edited === now; // true - }); + // In the next tick, the live proxy value will have updated. + // When IPC is used, this may take up to a few ms + await new Promise((resolve) => questionProxy.on('mutation', resolve)); + expect(liveQuestion.edited).toBe(now); + // process.nextTick(() => { + // liveQuestion.edited === now; // true + // }); }); }); diff --git a/src/test/export-import.spec.ts b/src/test/export-import.spec.ts index db373e2..bf9fde7 100644 --- a/src/test/export-import.spec.ts +++ b/src/test/export-import.spec.ts @@ -1,8 +1,8 @@ -import { createTempDB } from './tempdb'; +import { createTempDB } from './tempdb.js'; import { openSync, closeSync, read } from 'fs'; import { PathReference } from 'acebase-core'; -import { AceBase } from '..'; -import { getDataSetPath } from './dataset'; +import { AceBase } from '../index.js'; +import { getDataSetPath } from './dataset.js'; describe('export/import', () => { let db: AceBase, removeDB: () => Promise; diff --git a/src/test/include-exclude-filters.spec.ts b/src/test/include-exclude-filters.spec.ts index 38fbb40..6b4f377 100644 --- a/src/test/include-exclude-filters.spec.ts +++ b/src/test/include-exclude-filters.spec.ts @@ -1,6 +1,6 @@ -import { AceBase } from '..'; -import { readDataSet } from './dataset'; -import { createTempDB } from './tempdb'; +import { AceBase } from '../index.js'; +import { readDataSet } from './dataset.js'; +import { createTempDB } from './tempdb.js'; describe('Include/exclude filters', () => { let db: AceBase, removeDB: () => Promise; diff --git a/src/test/indexes.spec.ts b/src/test/indexes.spec.ts index f4d113d..43ae9f7 100644 --- a/src/test/indexes.spec.ts +++ b/src/test/indexes.spec.ts @@ -1,7 +1,7 @@ -import { createTempDB } from './tempdb'; -import { AceBase, ID } from '..'; +import { createTempDB } from './tempdb.js'; +import { AceBase, ID } from '../index.js'; import { ObjectCollection } from 'acebase-core'; -import { readDataSet } from './dataset'; +import { readDataSet } from './dataset.js'; // TODO: MANY MORE index options to spec diff --git a/src/test/issue-225.spec.ts b/src/test/issue-225.spec.ts index 8339c1f..d29de28 100644 --- a/src/test/issue-225.spec.ts +++ b/src/test/issue-225.spec.ts @@ -1,5 +1,5 @@ -import { AceBase } from '..'; -import { createTempDB } from './tempdb'; +import { AceBase } from '../index.js'; +import { createTempDB } from './tempdb.js'; describe('issue #225', () => { let db: AceBase; diff --git a/src/test/json-data-export.spec.ts b/src/test/json-data-export.spec.ts index 036d29f..92dea90 100644 --- a/src/test/json-data-export.spec.ts +++ b/src/test/json-data-export.spec.ts @@ -1,5 +1,5 @@ -import { AceBase } from '..'; -import { createTempDB } from './tempdb'; +import { AceBase } from '../index.js'; +import { createTempDB } from './tempdb.js'; describe('JSON data export', () => { let db: AceBase, removeDB: () => Promise; diff --git a/src/test/keys.spec.ts b/src/test/keys.spec.ts index 5d30013..7275174 100644 --- a/src/test/keys.spec.ts +++ b/src/test/keys.spec.ts @@ -1,5 +1,5 @@ -import { AceBase } from '..'; -import { createTempDB } from './tempdb'; +import { AceBase } from '../index.js'; +import { createTempDB } from './tempdb.js'; describe('Keys', () => { let db: AceBase, removeDB: () => Promise; diff --git a/src/test/node-lock.spec.ts b/src/test/node-lock.spec.ts index d64a8a3..9030984 100644 --- a/src/test/node-lock.spec.ts +++ b/src/test/node-lock.spec.ts @@ -1,4 +1,4 @@ -import { createTempDB } from './tempdb'; +import { createTempDB } from './tempdb.js'; describe('node locking', () => { it('should not cause deadlocks', async () => { diff --git a/src/test/query.spec.ts b/src/test/query.spec.ts index a25a49b..84ebeba 100644 --- a/src/test/query.spec.ts +++ b/src/test/query.spec.ts @@ -1,7 +1,7 @@ import { DataReference, DataSnapshotsArray, DataReferencesArray, DataReferenceQuery, ObjectCollection, DataSnapshot } from 'acebase-core'; -import { AceBase, ID } from '..'; -import { readDataSet } from './dataset'; -import { createTempDB } from './tempdb'; +import { AceBase, ID } from '../index.js'; +import { readDataSet } from './dataset.js'; +import { createTempDB } from './tempdb.js'; describe('Query', () => { let db: AceBase, removeDB: () => Promise; diff --git a/src/test/readonly.spec.ts b/src/test/readonly.spec.ts index 79d3258..e6b7ecf 100644 --- a/src/test/readonly.spec.ts +++ b/src/test/readonly.spec.ts @@ -1,5 +1,5 @@ -import { createTempDB } from './tempdb'; -import { AceBase } from '..'; +import { createTempDB } from './tempdb.js'; +import { AceBase } from '../index.js'; describe('readonly databases', () => { @@ -21,7 +21,7 @@ describe('readonly databases', () => { await db.close(); // Open it readonly - db = new AceBase(db.name, { logLevel: 'verbose', storage: { path: __dirname, readOnly: true } }); + db = new AceBase(db.name, { logLevel: 'verbose', storage: { path: db.api.storage.settings.path, readOnly: true } }); try { // Try writing to it @@ -43,7 +43,7 @@ describe('readonly databases', () => { await db.close(); // Open it readonly - db = new AceBase(db.name, { logLevel: 'verbose', storage: { path: __dirname, readOnly: true } }); + db = new AceBase(db.name, { logLevel: 'verbose', storage: { path: db.api.storage.settings.path, readOnly: true } }); // Try reading from it const snap = await db.ref('test').get(); diff --git a/src/test/recovery.spec.ts b/src/test/recovery.spec.ts index fb97b34..78dbb25 100644 --- a/src/test/recovery.spec.ts +++ b/src/test/recovery.spec.ts @@ -1,7 +1,7 @@ -import { createTempDB } from './tempdb'; -import { AceBase } from '..'; +import { createTempDB } from './tempdb.js'; +import { AceBase } from '../index.js'; import { ObjectCollection } from 'acebase-core'; -import { readDataSet } from './dataset'; +import { readDataSet } from './dataset.js'; describe('database recovery', () => { let db: AceBase; diff --git a/src/test/schema.spec.ts b/src/test/schema.spec.ts index acbb071..fb60391 100644 --- a/src/test/schema.spec.ts +++ b/src/test/schema.spec.ts @@ -1,5 +1,5 @@ -import { AceBase } from '..'; -import { createTempDB } from './tempdb'; +import { AceBase } from '../index.js'; +import { createTempDB } from './tempdb.js'; const ok = { ok: true }; describe('schema', () => { diff --git a/src/test/tempdb.ts b/src/test/tempdb.ts index 7480537..35e766c 100644 --- a/src/test/tempdb.ts +++ b/src/test/tempdb.ts @@ -1,22 +1,23 @@ -import { AceBase, ID, AceBaseLocalSettings } from '..'; +import { AceBase, ID, AceBaseLocalSettings } from '../index.js'; import { readdir, rm, rmdir } from 'fs/promises'; +import { tmpdir } from 'os'; // import { resolve as resolvePath } from 'path'; -// import customLogger from './custom-logger'; +// import customLogger from './custom-logger.js'; export async function createTempDB(enable: { transactionLogging?: boolean; logLevel?: 'verbose'|'log'|'warn'|'error'; config?: (options: any) => void } = {}) { // Create temp db const dbname = 'test-' + ID.generate(); - const options: Partial = { storage: { path: __dirname }, logLevel: enable.logLevel || 'log' }; + const options: Partial = { storage: { path: tmpdir() }, logLevel: enable.logLevel || 'log' }; if (enable.transactionLogging === true) { options.transactions = { log: true }; } if (typeof enable.config === 'function') { enable.config(options); } - // if (!options.storage) { options.storage = {}; } + if (!options.storage) { options.storage = {}; } // options.storage.lockTimeout = 0.001; - // options.storage.ipc = 'socket'; + options.storage.ipc = 'socket'; // options.storage.ipc = { role: 'socket', maxIdleTime: 0, loggerPluginPath: resolvePath(__dirname, 'custom-logger.js') }; // options.logger = customLogger; // options.logColors = false; @@ -29,7 +30,7 @@ export async function createTempDB(enable: { transactionLogging?: boolean; logLe await db.close(); // Remove database - const dbdir = `${__dirname}/${dbname}.acebase`; + const dbdir = `${tmpdir()}/${dbname}.acebase`; if (nodeVersion.major < 12) { // console.error(`Node ${process.version} cannot remove temp database directory ${dbdir}. Remove it manually!`); diff --git a/src/test/transaction-logs.spec.ts b/src/test/transaction-logs.spec.ts index 2d83b7d..aa73fe6 100644 --- a/src/test/transaction-logs.spec.ts +++ b/src/test/transaction-logs.spec.ts @@ -1,7 +1,7 @@ import { SimpleEventEmitter } from 'acebase-core'; -import { AceBase } from '..'; -import type { AceBaseStorage } from '../storage/binary'; -import { createTempDB } from './tempdb'; +import { AceBase } from '../index.js'; +import type { AceBaseStorage } from '../storage/binary/index.js'; +import { createTempDB } from './tempdb.js'; describe('BETA - Transaction logging', () => { let db: AceBase, removeDB: () => Promise; diff --git a/src/test/transactions.spec.ts b/src/test/transactions.spec.ts index 4758aa2..6ed0238 100644 --- a/src/test/transactions.spec.ts +++ b/src/test/transactions.spec.ts @@ -1,5 +1,5 @@ import { DataSnapshot } from 'acebase-core'; -import { createTempDB } from './tempdb'; +import { createTempDB } from './tempdb.js'; describe('transactions', () => { diff --git a/tsconfig-cjs.json b/tsconfig-cjs.json index 1d5c0b0..a1dc0fc 100644 --- a/tsconfig-cjs.json +++ b/tsconfig-cjs.json @@ -3,6 +3,7 @@ "compilerOptions": { "target": "es2017", "module": "commonjs", + "moduleResolution": "node", "outDir": "./dist/cjs", "declaration": false, "declarationMap": false, diff --git a/tsconfig.json b/tsconfig.json index 6d2bf6c..960bc82 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,7 @@ "module": "es2020", "rootDir": "./src", "outDir": "./dist/esm", - "moduleResolution": "node", + "moduleResolution": "bundler", "target": "es2020", "pretty": true, "declaration": true,