diff --git a/dist/browser.html b/dist/browser.html
new file mode 100644
index 0000000..16abdc0
--- /dev/null
+++ b/dist/browser.html
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dist/browser.js b/dist/browser.js
new file mode 100644
index 0000000..f0cb4ff
--- /dev/null
+++ b/dist/browser.js
@@ -0,0 +1,8389 @@
+(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i= 7 && stringLength <= 10) return true;
+ return false;
+};
+
+cuid.fingerprint = fingerprint;
+
+module.exports = cuid;
+
+},{"./lib/fingerprint.js":2,"./lib/pad.js":3}],2:[function(require,module,exports){
+var pad = require('./pad.js');
+
+var env = typeof window === 'object' ? window : self;
+var globalCount = Object.keys(env).length;
+var mimeTypesLength = navigator.mimeTypes ? navigator.mimeTypes.length : 0;
+var clientId = pad((mimeTypesLength +
+ navigator.userAgent.length).toString(36) +
+ globalCount.toString(36), 4);
+
+module.exports = function fingerprint () {
+ return clientId;
+};
+
+},{"./pad.js":3}],3:[function(require,module,exports){
+module.exports = function pad (num, size) {
+ var s = '000000000' + num;
+ return s.substr(s.length - size);
+};
+
+},{}],4:[function(require,module,exports){
+/**
+ ________________________________________________________________________________
+
+ ___ ______
+ / _ \ | ___ \
+ / /_\ \ ___ ___| |_/ / __ _ ___ ___
+ | _ |/ __/ _ \ ___ \/ _` / __|/ _ \
+ | | | | (_| __/ |_/ / (_| \__ \ __/
+ \_| |_/\___\___\____/ \__,_|___/\___|
+
+ Copyright 2018 by Ewout Stortenbeker (me@appy.one)
+ Published under MIT license
+ ________________________________________________________________________________
+
+ */
+const { EventEmitter } = require('events');
+const { DataReference, DataReferenceQuery } = require('./data-reference');
+const { TypeMappings } = require('./type-mappings');
+const debug = require('./debug');
+
+class AceBaseSettings {
+ constructor(options) {
+ // if (typeof options.api !== 'object') {
+ // throw new Error(`No api passed to AceBaseSettings constructor`);
+ // }
+ this.logLevel = options.logLevel || "log";
+ // this.api = options.api;
+ }
+}
+
+class AceBaseBase extends EventEmitter {
+
+ /**
+ *
+ * @param {string} dbname | Name of the database to open or create
+ * @param {AceBaseSettings} options |
+ */
+ constructor(dbname, options) {
+ super();
+
+ if (!options) { options = {}; }
+ if (options.logLevel) {
+ debug.setLevel(options.logLevel);
+ }
+
+ this.once("ready", () => {
+ this._ready = true;
+ });
+
+ // Specific api given such as web api, or browser api etc
+ // this.api = new options.api.class(dbname, options.api.settings, ready => {
+ // this.emit("ready");
+ // });
+
+ this.types = new TypeMappings(this);
+ }
+
+ /**
+ *
+ * @param {()=>void} [callback] (optional) callback function that is called when ready. You can also use the returned promise
+ * @returns {Promise} returns a promise that resolves when ready
+ */
+ ready(callback = undefined) {
+ if (this._ready === true) {
+ // ready event was emitted before
+ callback && callback();
+ return Promise.resolve();
+ }
+ else {
+ // Wait for ready event
+ let resolve;
+ const promise = new Promise(res => resolve = res);
+ this.on("ready", () => {
+ resolve();
+ callback && callback();
+ });
+ return promise;
+ }
+ }
+
+ /**
+ * Creates a reference to a node
+ * @param {string} path
+ * @returns {DataReference} reference to the requested node
+ */
+ ref(path) {
+ return new DataReference(this, path);
+ }
+
+ /**
+ * Get a reference to the root database node
+ * @returns {DataReference} reference to root node
+ */
+ get root() {
+ return this.ref("");
+ }
+
+ /**
+ * Creates a query on the requested node
+ * @param {string} path
+ * @returns {DataReferenceQuery} query for the requested node
+ */
+ query(path) {
+ const ref = new DataReference(this, path);
+ return new DataReferenceQuery(ref);
+ }
+
+ 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 {string} path path to the container node
+ * @param {string} key name of the key to index every container child node
+ * @param {object} [options] any additional options
+ * @param {string} [options.type] special index type, such as 'fulltext', or 'geo'
+ * @param {string[]} [options.include] keys to include in the index. Speeds up sorting on these columns when the index is used (and dramatically increases query speed when .take(n) is used in addition)
+ * @param {object} [options.config] additional index-specific configuration settings
+ */
+ create: (path, key, options) => {
+ return this.api.createIndex(path, key, options);
+ }
+ };
+ }
+
+}
+
+module.exports = { AceBaseBase, AceBaseSettings };
+},{"./data-reference":7,"./debug":9,"./type-mappings":16,"events":40}],5:[function(require,module,exports){
+
+class Api {
+ // interface for local and web api's
+ stats(options = undefined) {}
+
+ /**
+ *
+ * @param {string} path | reference
+ * @param {string} event | event to subscribe to ("value", "child_added" etc)
+ * @param {function} callback | callback function(err, path, value)
+ */
+ subscribe(path, event, callback) {}
+
+ // TODO: add jsdoc comments
+
+ unsubscribe(path, event, callback) {}
+ update(path, updates) {}
+ set(path, value) {}
+ get(path, options) {}
+ exists(path) {}
+ query(path, query, options) {}
+ createIndex(path, key) {}
+ getIndexes() {}
+}
+
+module.exports = { Api };
+},{}],6:[function(require,module,exports){
+const c = function(input, length, result) {
+ var i, j, n, b = [0, 0, 0, 0, 0];
+ for(i = 0; i < length; i += 4){
+ n = ((input[i] * 256 + input[i+1]) * 256 + input[i+2]) * 256 + input[i+3];
+ if(!n){
+ result.push("z");
+ }else{
+ for(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 ascii85 = {
+ encode: function(arr) {
+ // summary: encodes input data in ascii85 string
+ // input: ArrayLike
+ if (arr instanceof ArrayBuffer) {
+ arr = new Uint8Array(arr, 0, arr.byteLength);
+ }
+ var input = arr;
+ var result = [], remainder = input.length % 4, length = input.length - remainder;
+ c(input, length, result);
+ if(remainder){
+ var t = new Uint8Array(4);
+ t.set(input.slice(length), 0);
+ c(t, 4, result);
+ var x = result.pop();
+ if(x == "z"){ x = "!!!!!"; }
+ result.push(x.substr(0, remainder + 1));
+ }
+ var ret = result.join(""); // String
+ ret = '<~' + ret + '~>';
+ return ret;
+ },
+ 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);
+ var n = input.length, r = [], b = [0, 0, 0, 0, 0], i, j, t, x, y, d;
+ for(i = 0; i < n; ++i) {
+ if(input.charAt(i) == "z"){
+ r.push(0, 0, 0, 0);
+ continue;
+ }
+ for(j = 0; j < 5; ++j){ b[j] = input.charCodeAt(i + j) - 33; }
+ d = n - i;
+ if(d < 5){
+ for(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(j = d; j < 5; ++j, r.pop());
+ i += 4;
+ }
+ const data = new Uint8Array(r);
+ return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
+ }
+};
+
+module.exports = ascii85;
+},{}],7:[function(require,module,exports){
+const { DataSnapshot } = require('./data-snapshot');
+const { EventStream, EventPublisher } = require('./subscription');
+const { ID } = require('./id');
+const debug = require('./debug');
+const { PathInfo } = require('./path-info');
+
+class DataRetrievalOptions {
+ /**
+ * Options for data retrieval, allows selective loading of object properties
+ * @param {{ include?: Array, exclude?: Array, child_objects?: boolean }} options
+ */
+ 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`);
+ }
+
+ /**
+ * @property {string[]} include - child keys to include (will exclude other keys), can include wildcards (eg "messages/*\/title")
+ */
+ this.include = options.include || undefined;
+ /**
+ * @property {string[]} exclude - child keys to exclude (will include other keys), can include wildcards (eg "messages/*\/replies")
+ */
+ this.exclude = options.exclude || undefined;
+ /**
+ * @property {boolean} child_objects - whether or not to include any child objects
+ */
+ this.child_objects = typeof options.child_objects === "boolean" ? options.child_objects : undefined;
+ }
+}
+
+class QueryDataRetrievalOptions extends DataRetrievalOptions {
+ /**
+ * Options for data retrieval, allows selective loading of object properties
+ * @param {{ snapshots?: boolean, include?: Array, exclude?: Array, child_objects?: boolean }} options
+ */
+ constructor(options) {
+ super(options);
+ if (typeof options.snapshots !== 'undefined' && typeof options.snapshots !== 'boolean') {
+ throw new TypeError(`options.snapshots must be an array`);
+ }
+ /**
+ * @property {boolean} snapshots - whether to return snapshots of matched nodes (include data), or references only (no data)
+ */
+ this.snapshots = typeof options.snapshots === 'boolean' ? options.snapshots : undefined;
+ }
+}
+
+const _private = Symbol("private");
+class DataReference {
+ /**
+ * Creates a reference to a node
+ * @param {AceBase} db
+ * @param {string} path
+ */
+ constructor (db, path, vars) {
+ if (!path) { path = ""; }
+ path = path.replace(/^\/|\/$/g, ""); // Trim slashes
+ const pathInfo = PathInfo.get(path);
+ const key = pathInfo.key; //path.length === 0 ? "" : path.substr(path.lastIndexOf("/") + 1); //path.match(/(?:^|\/)([a-z0-9_$]+)$/i)[1];
+ // const query = {
+ // filters: [],
+ // skip: 0,
+ // take: 0,
+ // order: []
+ // };
+ const callbacks = [];
+ this[_private] = {
+ get path() { return path; },
+ get key() { return key; },
+ get callbacks() { return callbacks; },
+ vars: vars || {}
+ };
+ this.db = db; //Object.defineProperty(this, "db", ...)
+ }
+
+ /**
+ * The path this instance was created with
+ * @type {string}
+ */
+ get path() { return this[_private].path; }
+
+ /**
+ * The key or index of this node
+ * @type {string|number}
+ */
+ get key() { return this[_private].key; }
+
+ /**
+ * Returns a new reference to this node's parent
+ * @type {DataReference}
+ */
+ get parent() {
+ const info = PathInfo.get(this.path);
+ if (info.parentPath === null) {
+ return null;
+ }
+ return new DataReference(this.db, info.parentPath);
+ }
+
+ /**
+ * Contains values of the variables/wildcards used in a subscription path if this reference was
+ * created by an event ("value", "child_added" etc)
+ * @type {{ [name: string]: string|number, wildcards?: Array }}
+ */
+ get vars() {
+ return this[_private].vars;
+ }
+
+ /**
+ * Returns a new reference to a child node
+ * @param {string} childPath Child key or path
+ * @returns {DataReference} reference to the child
+ */
+ child(childPath) {
+ childPath = childPath.replace(/^\/|\/$/g, "");
+ return new DataReference(this.db, `${this.path}/${childPath}`);
+ }
+
+ /**
+ * Sets or overwrites the stored value
+ * @param {any} value value to store in database
+ * @param {(err: Error, ref: DataReference) => void} [onComplete] completion callback to use instead of returning promise
+ * @returns {Promise} promise that resolves with this reference when completed (when not using onComplete callback)
+ */
+ set(value, onComplete = undefined) {
+ if (this.isWildcardPath) {
+ throw new Error(`Cannot set the value of a path with wildcards and/or variables`);
+ }
+ if (this.parent === null) {
+ throw new Error(`Cannot set the root object. Use update, or set individual child properties`);
+ }
+ if (typeof value === 'undefined') {
+ throw new TypeError(`Cannot store value undefined`);
+ }
+ value = this.db.types.serialize(this.path, value);
+ // let flags;
+ // if (this.__pushed) {
+ // flags = { pushed: true };
+ // }
+ const promise = this.db.api.set(this.path, value);
+ if (typeof onComplete === 'function') {
+ promise.then(res => {
+ onComplete(null, this);
+ })
+ .catch(err => {
+ try { onComplete(err); } catch(err) { console.error(`Error in onComplete callback:`, err); }
+ });
+ }
+ else {
+ return promise.then(res => this);
+ }
+ }
+
+ /**
+ * Updates properties of the referenced node
+ * @param {object} updates object containing the properties to update
+ * @param {(err: Error, ref: DataReference) => void} [onComplete] completion callback to use instead of returning promise
+ * @return {Promise} returns promise that resolves with this reference once completed (when not using onComplete callback)
+ */
+ update(updates, onComplete = undefined) {
+ if (this.isWildcardPath) {
+ throw new Error(`Cannot update the value of a path with wildcards and/or variables`);
+ }
+ let promise;
+ if (typeof updates !== "object" || updates instanceof Array || updates instanceof ArrayBuffer || updates instanceof Date) {
+ promise = this.set(updates);
+ }
+ else {
+ updates = this.db.types.serialize(this.path, updates);
+ promise = this.db.api.update(this.path, updates);
+ }
+ if (typeof onComplete === 'function') {
+ promise.then(() => {
+ onComplete(null, this);
+ })
+ .catch(err => {
+ try { onComplete(err); } catch(err) { console.error(`Error in onComplete callback:`, err); }
+ });
+ }
+ else {
+ return promise.then(() => this);
+ }
+ }
+
+ /**
+ * Sets the value a node using a transaction: it runs you callback function with the current value, uses its return value as the new value to store.
+ * @param {(currentValue: DataSnapshot) => void} callback - callback function(currentValue) => newValue: is called with a snapshot of the current value, must return a new value to store in the database
+ * @returns {Promise} returns a promise that resolves with the DataReference once the transaction has been processed
+ */
+ transaction(callback) {
+ if (this.isWildcardPath) {
+ throw new Error(`Cannot start a transaction on a path with wildcards and/or variables`);
+ }
+ let cb = (currentValue) => {
+ currentValue = this.db.types.deserialize(this.path, currentValue);
+ const snap = new DataSnapshot(this, currentValue);
+ let newValue;
+ try {
+ newValue = callback(snap);
+ }
+ catch(err) {
+ // Make sure an exception thrown in client code cancels the transaction
+ return;
+ }
+ if (newValue instanceof Promise) {
+ return newValue.then((val) => {
+ return this.db.types.serialize(this.path, val);
+ });
+ }
+ else {
+ return this.db.types.serialize(this.path, newValue);
+ }
+ }
+ return this.db.api.transaction(this.path, cb)
+ .then(result => {
+ return this;
+ });
+ }
+
+ /**
+ * Subscribes to an event. Supported events are "value", "child_added", "child_changed", "child_removed",
+ * which will run the callback with a snapshot of the data. If you only wish to receive notifications of the
+ * event (without the data), use the "notify_value", "notify_child_added", "notify_child_changed",
+ * "notify_child_removed" events instead, which will run the callback with a DataReference to the changed
+ * data. This enables you to manually retreive data upon changes (eg if you want to exclude certain child
+ * data from loading)
+ * @param {string} event - Name of the event to subscribe to
+ * @param {((snapshotOrReference:DataSnapshot|DataReference) => void)|boolean} callback - Callback function(snapshot) or whether or not to run callbacks on current values when using "value" or "child_added" events
+ * @returns {EventStream} returns an EventStream
+ */
+ on(event, callback, cancelCallbackOrContext, context) {
+ const cancelCallback = typeof cancelCallbackOrContext === 'function' && cancelCallbackOrContext;
+ context = typeof cancelCallbackOrContext === 'object' ? cancelCallbackOrContext : context
+
+ const useCallback = typeof callback === 'function';
+
+ /** @type {EventPublisher} */
+ let eventPublisher = null;
+ const eventStream = new EventStream(publisher => { eventPublisher = publisher });
+
+ // Map OUR callback to original callback, so .off can remove the right callback
+ let cb = {
+ subscr: eventStream,
+ original: callback,
+ ours: (err, path, newValue, oldValue) => {
+ if (err) {
+ debug.error(`Error getting data for event ${event} on path "${path}"`, err);
+ return;
+ }
+ let ref = this.db.ref(path);
+ ref[_private].vars = PathInfo.extractVariables(this.path, path);
+
+ let callbackObject;
+ if (event.startsWith('notify_')) {
+ // No data event, callback with reference
+ callbackObject = ref;
+ }
+ else {
+ const isRemoved = event === "child_removed";
+ const val = this.db.types.deserialize(path, isRemoved ? oldValue : newValue);
+ const snap = new DataSnapshot(ref, val, isRemoved);
+ callbackObject = snap;
+ }
+
+ useCallback && callback.call(context || null, callbackObject);
+ let keep = eventPublisher.publish(callbackObject);
+ if (!keep && !useCallback) {
+ // If no callback was used, unsubscribe
+ let callbacks = this[_private].callbacks;
+ callbacks.splice(callbacks.indexOf(cb), 1);
+ this.db.api.unsubscribe(this.path, event, cb.ours);
+ }
+ }
+ };
+ this[_private].callbacks.push(cb);
+
+ let authorized = this.db.api.subscribe(this.path, event, cb.ours);
+ 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();
+ })
+ .catch(err => {
+ // Access denied?
+ // Cancel subscription
+ let callbacks = this[_private].callbacks;
+ callbacks.splice(callbacks.indexOf(cb), 1);
+ this.db.api.unsubscribe(this.path, event, cb.ours);
+
+ // Call cancelCallbacks
+ eventPublisher.cancel(err.message);
+ cancelCallback && cancelCallback(err.message);
+ });
+ }
+ else {
+ // Local API, always authorized
+ eventPublisher.start();
+ }
+
+ if (callback && !this.isWildcardPath) {
+ // 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);
+ useCallback && callback(snap);
+ });
+ }
+ else if (event === "child_added") {
+ this.get(snap => {
+ const val = snap.val();
+ if (val === null || typeof val !== "object") { return; }
+ Object.keys(val).forEach(key => {
+ let childSnap = new DataSnapshot(this.child(key), val[key]);
+ eventPublisher.publish(childSnap);
+ useCallback && callback(childSnap);
+ });
+ });
+ }
+ }
+
+ return eventStream;
+ }
+
+ /**
+ * Unsubscribes from a previously added event
+ * @param {string} event | Name of the event
+ * @param {Function} callback | callback function to remove
+ */
+ off(event = undefined, callback = undefined) {
+ const callbacks = this[_private].callbacks;
+ if (callback) {
+ const cb = callbacks.find(cb => cb.original === callback);
+ if (!cb) {
+ debug.error(`Can't find specified callback to unsubscribe from (path: "${this.path}", event: ${event}, callback: ${callback})`);
+ return;
+ }
+ callbacks.splice(callbacks.indexOf(cb), 1);
+ callback = cb.ours;
+ cb.subscr.unsubscribe(callback);
+ }
+ else {
+ callbacks.splice(0, callbacks.length).forEach(cb => {
+ cb.subscr.unsubscribe();
+ });
+ }
+ this.db.api.unsubscribe(this.path, event, callback);
+ return this;
+ }
+
+ /**
+ * Gets a snapshot of the stored value. Shorthand method for .once("value")
+ * @param {DataRetrievalOptions|((snapshot:DataSnapshot) => void)} [optionsOrCallback] data retrieval options to include or exclude specific child keys, or callback
+ * @param {(snapshot:DataSnapshot) => void} [callback] callback function to run with a snapshot of the data instead of returning a promise
+ * @returns {Promise|void} returns a promise that resolves with a snapshot of the data, or nothing if callback is used
+ */
+ get(optionsOrCallback = undefined, callback = undefined) {
+ if (this.isWildcardPath) {
+ throw new Error(`Cannot get the value of a path with wildcards and/or variables. Use .query() instead`);
+ }
+
+ callback =
+ typeof optionsOrCallback === 'function'
+ ? optionsOrCallback
+ : typeof callback === 'function'
+ ? callback
+ : undefined;
+
+ const options =
+ typeof optionsOrCallback === 'object'
+ ? optionsOrCallback
+ : undefined;
+
+ const promise = this.db.api.get(this.path, options).then(value => {
+ value = this.db.types.deserialize(this.path, value);
+ const snapshot = new DataSnapshot(this, value);
+ return snapshot;
+ });
+
+ if (callback) {
+ promise.then(callback);
+ return;
+ }
+ else {
+ return promise;
+ }
+ }
+
+ /**
+ * Waits for an event to occur
+ * @param {string} event - Name of the event, eg "value", "child_added", "child_changed", "child_removed"
+ * @param {DataRetrievalOptions} options - data retrieval options, to include or exclude specific child keys
+ * @returns {Promise} - 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, reject) => {
+ const callback = (snap) => {
+ this.off(event, snap); // unsubscribe directly
+ resolve(snap);
+ }
+ this.on(event, callback);
+ });
+ }
+
+ /**
+ * Creates a new child with a unique key and returns the new reference.
+ * If a value is passed as an argument, it will be stored to the database directly.
+ * The returned reference can be used as a promise that resolves once the
+ * given value is stored in the database
+ * @param {any} value optional value to store into the database right away
+ * @param {function} onComplete optional callback function to run once value has been stored
+ * @returns {DataReference|Promise} returns a reference to the new child, or a promise that resolves with the reference after the passed value has been stored
+ * @example
+ * // Create a new user in "game_users"
+ * db.ref("game_users")
+ * .push({ name: "Betty Boop", points: 0 })
+ * .then(ref => {
+ * // ref is a new reference to the newly created object,
+ * // eg to: "game_users/7dpJMeLbhY0tluMyuUBK27"
+ * });
+ * @example
+ * // Create a new child reference with a generated key,
+ * // but don't store it yet
+ * let userRef = db.ref("users").push();
+ * // ... to store it later:
+ * userRef.set({ name: "Popeye the Sailor" })
+ */
+ push(value = undefined, onComplete = undefined) {
+ if (this.isWildcardPath) {
+ throw new Error(`Cannot push to a path with wildcards and/or variables`);
+ }
+
+ const id = ID.generate(); //uuid62.v1({ node: [0x61, 0x63, 0x65, 0x62, 0x61, 0x73] });
+ const ref = this.child(id);
+ ref.__pushed = true;
+
+ if (typeof value !== 'undefined') {
+ return ref.set(value, onComplete).then(res => ref);
+ }
+ else {
+ return ref;
+ }
+ }
+
+ /**
+ * Removes this node and all children
+ */
+ remove() {
+ if (this.isWildcardPath) {
+ throw new Error(`Cannot remove a path with wildcards and/or variables. Use query().remove instead`);
+ }
+ if (this.parent === null) {
+ throw new Error(`Cannot remove the top node`);
+ }
+ return this.set(null);
+ }
+
+ /**
+ * Quickly checks if this reference has a value in the database, without returning its data
+ * @returns {Promise} | returns a promise that resolves with a boolean value
+ */
+ exists() {
+ if (this.isWildcardPath) {
+ throw new Error(`Cannot push to a path with wildcards and/or variables`);
+ }
+ return this.db.api.exists(this.path);
+ }
+
+ get isWildcardPath() {
+ return this.path.indexOf('*') >= 0 || this.path.indexOf('$') >= 0;
+ }
+
+ query() {
+ return new DataReferenceQuery(this);
+ }
+
+ reflect(type, args) {
+ if (this.pathHasVariables) {
+ throw new Error(`Cannot reflect on a path with wildcards and/or variables`);
+ }
+ return this.db.api.reflect(this.path, type, args);
+ }
+}
+
+class DataReferenceQuery {
+
+ /**
+ * Creates a query on a reference
+ * @param {DataReference} ref
+ */
+ constructor(ref) {
+ this.ref = ref;
+ this[_private] = {
+ filters: [],
+ skip: 0,
+ take: 0,
+ order: []
+ };
+ }
+
+ /**
+ * 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 {string|number} key | property to test value of
+ * @param {string} op | operator to use
+ * @param {any} compare | value to compare with
+ * @returns {DataReferenceQuery}
+ */
+ 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`;
+ // }
+ 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;
+ }
+
+ /**
+ * @deprecated use .filter instead
+ */
+ where(key, op, compare) {
+ return this.filter(key, op, compare)
+ }
+
+ /**
+ * Limits the number of query results to n
+ * @param {number} n
+ * @returns {DataReferenceQuery}
+ */
+ take(n) {
+ this[_private].take = n;
+ return this;
+ }
+
+ /**
+ * Skips the first n query results
+ * @param {number} n
+ * @returns {DataReferenceQuery}
+ */
+ skip(n) {
+ this[_private].skip = n;
+ return this;
+ }
+
+ /**
+ * Sorts the query results
+ * @param {string} key
+ * @param {boolean} [ascending=true]
+ * @returns {DataReferenceQuery}
+ */
+ sort(key, ascending = true) {
+ if (typeof key !== "string") {
+ throw `key must be a string`;
+ }
+ this[_private].order.push({ key, ascending });
+ return this;
+ }
+
+ /**
+ * @deprecated use .sort instead
+ */
+ order(key, ascending = true) {
+ return this.sort(key, ascending);
+ }
+
+ /**
+ * Executes the query
+ * @param {((snapshotsOrReferences:DataSnapshotsArray|DataReferencesArray) => void)|QueryDataRetrievalOptions} [optionsOrCallback] data retrieval options (to include or exclude specific child data, and whether to return snapshots (default) or references only), or callback
+ * @param {(snapshotsOrReferences:DataSnapshotsArray|DataReferencesArray) => void} [callback] callback to use instead of returning a promise
+ * @returns {Promise|Promise|void} returns an Promise that resolves with an array of DataReferences or DataSnapshots, or void if a callback is used instead
+ */
+ get(optionsOrCallback = undefined, callback = undefined) {
+ callback =
+ typeof optionsOrCallback === 'function'
+ ? optionsOrCallback
+ : typeof callback === 'function'
+ ? callback
+ : undefined;
+
+ const options =
+ typeof optionsOrCallback === 'object'
+ ? optionsOrCallback
+ : new QueryDataRetrievalOptions({ snapshots: true });
+
+ if (typeof options.snapshots === 'undefined') {
+ options.snapshots = true;
+ }
+ options.eventHandler = ev => {
+ if (!this._events || !this._events[ev.event]) { return; }
+ const listeners = this._events[ev.event];
+ listeners.forEach(callback => { try { callback(ev); } catch(e) {} });
+ };
+ const db = this.ref.db;
+ return db.api.query(this.ref.path, this[_private], options)
+ .catch(err => {
+ throw new Error(err);
+ })
+ .then(results => {
+ results.forEach((result, index) => {
+ if (options.snapshots) {
+ const val = db.types.deserialize(result.path, result.val);
+ results[index] = new DataSnapshot(db.ref(result.path), val);
+ }
+ else {
+ results[index] = db.ref(result);
+ }
+ });
+ if (options.snapshots) {
+ return DataSnapshotsArray.from(results);
+ }
+ else {
+ return DataReferencesArray.from(results);
+ }
+ })
+ .then(results => {
+ callback && callback(results);
+ return results;
+ });
+ }
+
+ /**
+ * Executes the query and returns references. Short for .get({ snapshots: false })
+ * @param {(references:DataReferencesArray) => void} [callback] callback to use instead of returning a promise
+ * @returns {Promise|void} returns an Promise that resolves with an array of DataReferences, or void when using a callback
+ */
+ getRefs(callback = undefined) {
+ return this.get({ snapshots: false }, callback);
+ }
+
+ /**
+ * Executes the query, removes all matches from the database
+ * @returns {Promise|void} | returns an Promise that resolves once all matches have been removed, or void if a callback is used
+ */
+ remove(callback) {
+ return this.get({ snapshots: false })
+ .then(refs => {
+ const promises = [];
+ return Promise.all(refs.map(ref => ref.remove()))
+ .then(() => {
+ callback && callback();
+ });
+ });
+ }
+
+ on(event, callback) {
+ if (!this._events) { this._events = {}; };
+ if (!this._events[event]) { this._events[event] = []; }
+ this._events[event].push(callback);
+ return this;
+ }
+
+ off(event, callback) {
+ if (!this._events || !this._events[event]) { return this; }
+ const index = !this._events[event].indexOf(callback);
+ if (!~index) { return this; }
+ this._events[event].splice(index, 1);
+ return this;
+ }
+}
+
+class DataSnapshotsArray extends Array {
+ /**
+ *
+ * @param {DataSnapshot[]} snaps
+ */
+ static from(snaps) {
+ const arr = new DataSnapshotsArray(snaps.length);
+ snaps.forEach((snap, i) => arr[i] = snap);
+ return arr;
+ }
+ getValues() {
+ return this.map(snap => snap.val());
+ }
+}
+
+class DataReferencesArray extends Array {
+ /**
+ *
+ * @param {DataReference[]} refs
+ */
+ static from(refs) {
+ const arr = new DataReferencesArray(refs.length);
+ refs.forEach((ref, i) => arr[i] = ref);
+ return arr;
+ }
+ getPaths() {
+ return this.map(ref => ref.path);
+ }
+}
+
+module.exports = {
+ DataReference,
+ DataReferenceQuery,
+ DataRetrievalOptions,
+ QueryDataRetrievalOptions
+};
+},{"./data-snapshot":8,"./debug":9,"./id":10,"./path-info":12,"./subscription":14}],8:[function(require,module,exports){
+const { DataReference } = require('./data-reference');
+const { getPathKeys } = require('./path-info');
+
+const getChild = (snapshot, path) => {
+ if (!snapshot.exists()) { return null; }
+ let child = snapshot.val();
+ //path.split("/").every...
+ getPathKeys(path).every(key => {
+ child = child[key];
+ return typeof child !== "undefined";
+ });
+ return child || null;
+};
+
+const getChildren = (snapshot) => {
+ if (!snapshot.exists()) { return []; }
+ let value = snapshot.val();
+ if (value instanceof Array) {
+ return new Array(value.length).map((v,i) => i);
+ }
+ if (typeof value === "object") {
+ return Object.keys(value);
+ }
+ return [];
+};
+
+class DataSnapshot {
+
+ /**
+ *
+ * @param {DataReference} ref
+ * @param {any} value
+ */
+ constructor(ref, value, isRemoved = false) {
+ this.ref = ref;
+ this.val = () => { return value; };
+ this.exists = () => {
+ if (isRemoved) { return false; }
+ return value !== null && typeof value !== "undefined";
+ }
+ }
+
+ /**
+ * Gets a new snapshot for a child node
+ * @param {string} path child key or path
+ * @returns {DataSnapshot}
+ */
+ child(path) {
+ // Create new snapshot for child data
+ let child = getChild(this, path);
+ return new DataSnapshot(this.ref.child(path), child);
+ }
+
+ /**
+ * Checks if the snapshot's value has a child with the given key or path
+ * @param {string} path child key or path
+ * @returns {boolean}
+ */
+ hasChild(path) {
+ return getChild(this, path) !== null;
+ }
+
+ /**
+ * Indicates whether the the snapshot's value has any child nodes
+ * @returns {boolean}
+ */
+ hasChildren() {
+ return getChildren(this).length > 0;
+ }
+
+ /**
+ * The number of child nodes in this snapshot
+ * @returns {number}
+ */
+ numChildren() {
+ return getChildren(this).length;
+ }
+
+ /**
+ * Runs a callback function for each child node in this snapshot until the callback returns false
+ * @param {(child: DataSnapshot) => boolean} 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 {void}
+ */
+ forEach(callback) {
+ const value = this.val();
+ return getChildren(this).every((key, i) => {
+ const snap = new DataSnapshot(this.ref.child(key), value[key]);
+ return callback(snap);
+ });
+ }
+
+ /**
+ * @type {string|number}
+ */
+ get key() { return this.ref.key; }
+
+ // /**
+ // * Convenience method to update this snapshot's value AND commit the changes to the database
+ // * @param {object} updates
+ // */
+ // update(updates) {
+ // return this.ref.update(updates)
+ // .then(ref => {
+ // const isRemoved = updates === null;
+ // let value = this.val();
+ // if (!isRemoved && typeof updates === 'object' && typeof value === 'object') {
+ // Object.assign(value, updates);
+ // }
+ // else {
+ // value = updates;
+ // }
+ // this.val = () => { return value; };
+ // this.exists = () => {
+ // return value !== null && typeof value !== "undefined";
+ // }
+ // return this;
+ // });
+ // }
+}
+
+module.exports = { DataSnapshot };
+},{"./data-reference":7,"./path-info":12}],9:[function(require,module,exports){
+const debug = {
+ setLevel(level) {
+ this.log = ["log"].indexOf(level) >= 0 ? console.log.bind(console) : ()=>{};
+ this.warn = ["log", "warn"].indexOf(level) >= 0 ? console.warn.bind(console) : ()=>{};
+ this.error = ["log", "warn", "error"].indexOf(level) >= 0 ? console.error.bind(console) : ()=>{};
+ }
+};
+debug.setLevel("log"); // default
+
+module.exports = debug;
+},{}],10:[function(require,module,exports){
+const cuid = require('cuid');
+// const uuid62 = require('uuid62');
+
+class ID {
+ static generate() {
+ // Could also use https://www.npmjs.com/package/pushid for Firebase style 20 char id's
+ return cuid().slice(1); // Cuts off the always leading 'c'
+ // return uuid62.v1();
+ }
+}
+
+module.exports = { ID };
+},{"cuid":1}],11:[function(require,module,exports){
+const { AceBaseBase, AceBaseSettings } = require('./acebase-base');
+const { Api } = require('./api');
+const { DataReference, DataReferenceQuery, DataRetrievalOptions, QueryDataRetrievalOptions } = require('./data-reference');
+const { DataSnapshot } = require('./data-snapshot');
+const debug = require('./debug');
+const { ID } = require('./id');
+const { PathReference } = require('./path-reference');
+const { EventStream, EventPublisher, EventSubscription } = require('./subscription');
+const Transport = require('./transport');
+const { TypeMappings, TypeMappingOptions } = require('./type-mappings');
+const Utils = require('./utils');
+const { PathInfo } = require('./path-info');
+const ascii85 = require('./ascii85');
+
+module.exports = {
+ AceBaseBase, AceBaseSettings,
+ Api,
+ DataReference, DataReferenceQuery, DataRetrievalOptions, QueryDataRetrievalOptions,
+ DataSnapshot,
+ debug,
+ ID,
+ PathReference,
+ EventStream, EventPublisher, EventSubscription,
+ Transport,
+ TypeMappings, TypeMappingOptions,
+ Utils,
+ PathInfo,
+ ascii85
+};
+},{"./acebase-base":4,"./api":5,"./ascii85":6,"./data-reference":7,"./data-snapshot":8,"./debug":9,"./id":10,"./path-info":12,"./path-reference":13,"./subscription":14,"./transport":15,"./type-mappings":16,"./utils":17}],12:[function(require,module,exports){
+
+/**
+ *
+ * @param {string} path
+ * @returns {Array}
+ */
+function getPathKeys(path) {
+ if (path.length === 0) { return []; }
+ let keys = path.replace(/\[/g, "/[").split("/");
+ keys.forEach((key, index) => {
+ if (key.startsWith("[")) {
+ keys[index] = parseInt(key.substr(1, key.length - 2));
+ }
+ });
+ return keys;
+}
+
+function getPathInfo(path) {
+ if (path.length === 0) {
+ return { parent: null, key: "" };
+ }
+ const i = Math.max(path.lastIndexOf("/"), path.lastIndexOf("["));
+ const parentPath = i < 0 ? "" : path.substr(0, i);
+ let key = i < 0 ? path : path.substr(i);
+ if (key.startsWith("[")) {
+ key = parseInt(key.substr(1, key.length - 2));
+ }
+ else if (key.startsWith("/")) {
+ key = key.substr(1); // Chop off leading slash
+ }
+ if (parentPath === path) {
+ parentPath = null;
+ }
+ return {
+ parent: parentPath,
+ key
+ };
+}
+
+/**
+ *
+ * @param {string} path
+ * @param {string|number} key
+ * @returns {string}
+ */
+function getChildPath(path, key) {
+ if (path.length === 0) {
+ if (typeof key === "number") { throw new TypeError("Cannot add array index to root path!"); }
+ return key;
+ }
+ if (typeof key === "number") {
+ return `${path}[${key}]`;
+ }
+ return `${path}/${key}`;
+}
+//const _pathVariableRegex = /^\$(\{[a-z0-9]+\})$/i;
+
+class PathInfo {
+ /** @returns {PathInfo} */
+ static get(path) {
+ return new PathInfo(path);
+ }
+
+ /** @returns {string} */
+ static getChildPath(path, childKey) {
+ return getChildPath(path, childKey);
+ }
+
+ /** @returns {Array} */
+ static getPathKeys(path) {
+ return getPathKeys(path);
+ }
+
+ /**
+ * @param {string} path
+ */
+ constructor(path) {
+ this.path = path;
+ }
+
+ /** @type {string|number} */
+ get key() {
+ return getPathInfo(this.path).key;
+ }
+
+ /** @type {string} */
+ get parentPath() {
+ return getPathInfo(this.path).parent;
+ }
+
+ /**
+ * @param {string|number} childKey
+ * @returns {string}
+ * */
+ childPath(childKey) {
+ return getChildPath(`${this.path}`, childKey);
+ }
+
+ /** @returns {Array} */
+ get pathKeys() {
+ return getPathKeys(this.path);
+ }
+
+ // /**
+ // * If varPath contains variables or wildcards, it will return them with the values found in fullPath
+ // * @param {string} varPath
+ // * @param {string} fullPath
+ // * @returns {Array<{ name?: string, value: string|number }>}
+ // * @example
+ // * PathInfo.extractVariables('users/$uid/posts/$postid', 'users/ewout/posts/post1/title') === [
+ // * { name: '$uid', value: 'ewout' },
+ // * { name: '$postid', value: 'post1' }
+ // * ];
+ // *
+ // * PathInfo.extractVariables('users/*\/posts/*\/$property', 'users/ewout/posts/post1/title') === [
+ // * { value: 'ewout' },
+ // * { value: 'post1' },
+ // * { name: '$property', value: 'title' }
+ // * ]
+ // */
+ // static extractVariables(varPath, fullPath) {
+ // if (varPath.indexOf('*') < 0 && varPath.indexOf('$') < 0) {
+ // 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);
+ // const variables = [];
+ // keys.forEach((key, index) => {
+ // const pathKey = pathKeys[index];
+ // if (key === '*') {
+ // variables.push({ value: pathKey });
+ // }
+ // else if (typeof key === 'string' && key[0] === '$') {
+ // variables.push({ name: key, value: pathKey });
+ // }
+ // });
+ // return variables;
+ // }
+
+ /**
+ * 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;
+ }
+ }
+ });
+ return variables;
+ }
+
+ /**
+ * If varPath contains variables or wildcards, it will return a path with the variables replaced by the keys found in fullPath.
+ * @param {string} varPath
+ * @param {string} fullPath
+ * @returns {string}
+ * @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);
+ let merged = keys.map((key, index) => {
+ if (key === pathKeys[index] || index >= pathKeys.length) {
+ return key;
+ }
+ else if (typeof key === 'string' && (key === '*' || key[0] === '$')) {
+ return pathKeys[index];
+ }
+ else {
+ throw new Error(`Path "${fullPath}" cannot be used to fill variables of path "${this.path}" because they do not match`);
+ }
+ });
+ let mergedPath = '';
+ merged.forEach(key => {
+ if (typeof key === 'number') {
+ mergedPath += `[${key}]`;
+ }
+ else {
+ if (mergedPath.length > 0) { mergedPath += '/'; }
+ mergedPath += key;
+ }
+ });
+ return mergedPath;
+ }
+
+ /**
+ * Checks if a given path matches this path, eg "posts/*\/title" matches "posts/12344/title" and "users/123/name" matches "users/$uid/name"
+ * @param {string} otherPath
+ * @returns {boolean}
+ */
+ equals(otherPath) {
+ if (this.path === otherPath) { return true; } // they are identical
+ const keys = getPathKeys(this.path);
+ const otherKeys = getPathKeys(otherPath);
+ if (keys.length !== otherKeys.length) { return false; }
+ return keys.every((key, index) => {
+ const otherKey = otherKeys[index];
+ return otherKey === key
+ || (typeof otherKey === 'string' && (otherKey === "*" || otherKey[0] === '$'))
+ || (typeof key === 'string' && (key === "*" || key[0] === '$'));
+ });
+ }
+
+ /**
+ * Checks if a given path is an ancestor, eg "posts" is an ancestor of "posts/12344/title"
+ * @param {string} otherPath
+ * @returns {boolean}
+ */
+ isAncestorOf(descendantPath) {
+ if (descendantPath === '' || this.path === descendantPath) { return false; }
+ if (this.path === '') { return true; }
+ const ancestorKeys = getPathKeys(this.path);
+ const descendantKeys = getPathKeys(descendantPath);
+ if (ancestorKeys.length >= descendantKeys.length) { return false; }
+ return ancestorKeys.every((key, index) => {
+ const otherKey = descendantKeys[index];
+ return otherKey === key
+ || (typeof otherKey === 'string' && (otherKey === "*" || otherKey[0] === '$'))
+ || (typeof key === 'string' && (key === "*" || key[0] === '$'));
+ });
+ }
+
+ /**
+ * Checks if a given path is a descendant, eg "posts/1234/title" is a descendant of "posts"
+ * @param {string} otherPath
+ * @returns {boolean}
+ */
+ isDescendantOf(ancestorPath) {
+ if (this.path === '' || this.path === ancestorPath) { return false; }
+ if (ancestorPath === '') { return true; }
+ const ancestorKeys = getPathKeys(ancestorPath);
+ const descendantKeys = getPathKeys(this.path);
+ if (ancestorKeys.length >= descendantKeys.length) { return false; }
+ return ancestorKeys.every((key, index) => {
+ const otherKey = descendantKeys[index];
+ return otherKey === key
+ || (typeof otherKey === 'string' && (otherKey === "*" || otherKey[0] === '$'))
+ || (typeof key === 'string' && (key === "*" || key[0] === '$'));
+ });
+ }
+
+ /**
+ * Checks if a given path is a direct child, eg "posts/1234/title" is a child of "posts/1234"
+ * @param {string} otherPath
+ * @returns {boolean}
+ */
+ isChildOf(otherPath) {
+ if (this.path === '') { return false; } // If our path is the root, it's nobody's child...
+ const parentInfo = PathInfo.get(this.parentPath);
+ return parentInfo.equals(otherPath);
+ }
+
+ /**
+ * Checks if a given path is its parent, eg "posts/1234" is the parent of "posts/1234/title"
+ * @param {string} otherPath
+ * @returns {boolean}
+ */
+ isParentOf(otherPath) {
+ if (otherPath === '') { return false; } // If the other path is the root, this path cannot be its parent...
+ const parentInfo = PathInfo.get(PathInfo.get(otherPath).parentPath);
+ return parentInfo.equals(this.path);
+ }
+}
+
+module.exports = { getPathInfo, getChildPath, getPathKeys, PathInfo };
+},{}],13:[function(require,module,exports){
+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 {string} path
+ */
+ constructor(path) {
+ this.path = path;
+ }
+}
+module.exports = { PathReference };
+},{}],14:[function(require,module,exports){
+class EventSubscription {
+ /**
+ *
+ * @param {() => void} stop function that stops the subscription from receiving future events
+ * @param {(callback?: () => void) => Promise} activated function that runs optional callback when subscription is activated, and returns a promise that resolves once activated
+ */
+ constructor(stop) {
+ this.stop = stop;
+ this._internal = {
+ state: 'init',
+ cancelReason: undefined,
+ /** @type {{ callback?: (activated: boolean, cancelReason?: string) => void, resolve?: () => void, reject?: (reason: any) => void}[]} */
+ activatePromises: []
+ };
+ }
+
+ /**
+ * Notifies when subscription is activated or canceled
+ * @param {callback?: (activated: boolean, cancelReason?: string) => void} [callback] optional callback when subscription is activated or canceled
+ * @returns {Promise} returns a promise that resolves once activated, or rejects when it is denied (and no callback was supplied)
+ */
+ activated(callback = undefined) {
+ 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
+
+ // if (this._internal.state === 'active') {
+ // return Promise.resolve();
+ // }
+ // else if (this._internal.state === 'canceled') {
+ // if (callback) {
+ // // Do not reject when callback is used
+ // return new Promise(() => {});
+ // }
+ // return Promise.reject(new Error(this._internal.cancelReason));
+ // }
+ return new Promise((resolve, reject) => {
+ if (this._internal.state === 'active') {
+ return resolve();
+ }
+ else if (this._internal.state === 'canceled' && !callback) {
+ return reject(new Error(this._internal.cancelReason));
+ }
+ this._internal.activatePromises.push({
+ resolve,
+ reject: callback ? () => {} : reject // Don't reject when callback is used: let callback handle this (prevents UnhandledPromiseRejection if only callback is used)
+ });
+ });
+ }
+
+ _setActivationState(activated, cancelReason) {
+ this._internal.cancelReason = cancelReason;
+ this._internal.state = activated ? 'active' : 'canceled';
+ while (this._internal.activatePromises.length > 0) {
+ const p = this._internal.activatePromises.shift();
+ if (activated) {
+ p.callback && p.callback(true);
+ p.resolve && p.resolve();
+ }
+ else {
+ p.callback && p.callback(false, cancelReason);
+ p.reject && p.reject(cancelReason);
+ }
+ }
+ }
+}
+
+class EventPublisher {
+ /**
+ *
+ * @param {(val: any) => boolean} publish function that publishes a new value to subscribers, return if there are any active subscribers
+ * @param {() => void} start function that notifies subscribers their subscription is activated
+ * @param {(reason: string) => void} 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;
+ }
+}
+
+class EventStream {
+
+ /**
+ *
+ * @param {(eventPublisher: EventPublisher) => void} eventPublisherCallback
+ */
+ constructor(eventPublisherCallback) {
+ const subscribers = [];
+ let activationState;
+
+ /**
+ * Subscribe to new value events in the stream
+ * @param {function} callback | function(val) to run once a new value is published
+ * @param {(activated: boolean, cancelReason?: string) => void} activationCallback callback that notifies activation or cancelation of the subscription by the publisher.
+ * @returns {EventSubscription} returns a subscription to the requested event
+ */
+ this.subscribe = (callback, activationCallback) => {
+ if (typeof callback !== "function") {
+ throw new TypeError("callback must be a function");
+ }
+
+ const sub = {
+ callback,
+ activationCallback: function(activated, cancelReason) {
+ activationCallback && activationCallback(activated, cancelReason);
+ this.subscription._setActivationState(activated, cancelReason);
+ },
+ // stop() {
+ // subscribers.splice(subscribers.indexOf(this), 1);
+ // },
+ subscription: new EventSubscription(function() {
+ subscribers.splice(subscribers.indexOf(this), 1);
+ })
+ };
+ subscribers.push(sub);
+
+ if (typeof activationState !== 'undefined') {
+ if (activationState === true) {
+ activationCallback && activationCallback(true);
+ sub.subscription._setActivationState(true);
+ }
+ else if (typeof activationState === 'string') {
+ activationCallback && activationCallback(false, activationState);
+ sub.subscription._setActivationState(false, activationState);
+ }
+ }
+ return sub.subscription;
+ };
+
+ /**
+ * Stops monitoring new value events
+ * @param {function} callback | (optional) specific callback to remove. Will remove all callbacks when omitted
+ */
+ this.unsubscribe = (callback = undefined) => {
+ const remove = callback
+ ? subscribers.filter(sub => sub.callback === callback)
+ : subscribers;
+ remove.forEach(sub => {
+ const i = subscribers.indexOf(sub);
+ subscribers.splice(i, 1);
+ });
+ };
+
+
+ /**
+ * For publishing side: adds a value that will trigger callbacks to all subscribers
+ * @param {any} val
+ * @returns {boolean} returns whether there are subscribers left
+ */
+ const publish = (val) => {
+ subscribers.forEach(sub => {
+ try {
+ sub.callback(val);
+ }
+ catch(err) {
+ debug.error(`Error running subscriber callback: ${err.message}`);
+ }
+ });
+ return subscribers.length > 0;
+ };
+
+ /**
+ * For publishing side: let subscribers know their subscription is activated. Should be called only once
+ */
+ const start = () => {
+ activationState = true;
+ subscribers.forEach(sub => {
+ sub.activationCallback && sub.activationCallback(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 => {
+ sub.activationCallback && sub.activationCallback(false, reason || 'unknown reason');
+ });
+ subscribers.splice(); // Clear all
+ }
+
+ const publisher = new EventPublisher(publish, start, cancel);
+ eventPublisherCallback(publisher);
+ }
+}
+
+module.exports = { EventStream, EventPublisher, EventSubscription };
+},{}],15:[function(require,module,exports){
+const { PathReference } = require('./path-reference');
+//const { DataReference } = require('./data-reference');
+const { cloneObject } = require('./utils');
+const ascii85 = require('./ascii85');
+
+module.exports = {
+ deserialize(data) {
+ if (data.map === null || typeof data.map === "undefined") {
+ return data.val;
+ }
+ const deserializeValue = (type, val) => {
+ if (type === "date") {
+ // Date was serialized as a string (UTC)
+ return new Date(val);
+ }
+ else if (type === "binary") {
+ // ascii85 encoded binary data
+ return ascii85.decode(val);
+ }
+ else if (type === "reference") {
+ return new PathReference(val);
+ }
+ 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.replace(/\[/g, "/[").split("/");
+ keys.forEach((key, index) => {
+ if (key.startsWith("[")) {
+ keys[index] = parseInt(key.substr(1, key.length - 2));
+ }
+ });
+ let parent = data;
+ let key = "val";
+ let val = data.val;
+ keys.forEach(k => {
+ key = k;
+ parent = val;
+ val = val[key]; // If an error occurs here, there's something wrong with the calling code...
+ });
+ parent[key] = deserializeValue(type, val);
+ });
+
+ return data.val;
+ },
+
+ serialize(obj) {
+ // Recursively find dates and binary data
+ if (obj === null || typeof obj !== "object" || obj instanceof Date || obj instanceof ArrayBuffer || obj instanceof PathReference) {
+ // Single value
+ const ser = this.serialize({ value: obj });
+ return {
+ map: ser.map.value,
+ val: ser.val.value
+ };
+ }
+ obj = cloneObject(obj); // Make sure we don't alter the original object
+ const process = (obj, mappings, prefix) => {
+ Object.keys(obj).forEach(key => {
+ const val = obj[key];
+ const path = prefix.length === 0 ? key : `${prefix}/${key}`;
+ if (val instanceof Date) {
+ // serialize date to UTC string
+ obj[key] = val.toISOString();
+ mappings[path] = "date";
+ }
+ else if (val instanceof ArrayBuffer) {
+ // Serialize binary data with ascii85
+ obj[key] = ascii85.encode(val); //ascii85.encode(Buffer.from(val)).toString();
+ mappings[path] = "binary";
+ }
+ else if (val instanceof PathReference) {
+ obj[key] = val.path;
+ mappings[path] = "reference";
+ }
+ else if (typeof val === "object" && val !== null) {
+ process(val, mappings, path);
+ }
+ });
+ };
+ const mappings = {};
+ process(obj, mappings, "");
+ return {
+ map: mappings,
+ val: obj
+ };
+ }
+};
+},{"./ascii85":6,"./path-reference":13,"./utils":17}],16:[function(require,module,exports){
+const { cloneObject } = require('./utils');
+const { PathInfo } = require('./path-info');
+const { AceBaseBase } = require('./acebase-base');
+const { DataReference } = require('./data-reference');
+const { DataSnapshot } = require('./data-snapshot');
+
+/**
+ * (for internal use) - gets the mapping set for a specific path
+ * @param {TypeMappings} mappings
+ * @param {string} path
+ */
+const get = (mappings, path) => {
+ // path points to the mapped (object container) location
+ path = path.replace(/^\/|\/$/g, ''); // trim slashes
+ // const keys = path.length > 0 ? path.split("/") : [];
+ const keys = PathInfo.getPathKeys(path);
+ const mappedPath = Object.keys(mappings).find(mpath => {
+ // const mkeys = mpath.length > 0 ? mpath.split("/") : [];
+ const mkeys = PathInfo.getPathKeys(mpath);
+ if (mkeys.length !== keys.length) {
+ return false; // Can't be a match
+ }
+ return mkeys.every((mkey, index) => {
+ if (mkey === '*' || 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
+ * @param {TypeMappings} mappings
+ * @param {string} path
+ */
+const map = (mappings, path) => {
+ // path points to the object location, it's parent should have the mapping
+// path = path.replace(/^\/|\/$/g, ""); // trim slashes
+// const targetPath = path.substring(0, path.lastIndexOf("/"));
+ const targetPath = 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
+ * @param {TypeMappings} mappings
+ * @param {string} entryPath
+ * @returns {Array