mirror of
https://github.com/appy-one/acebase.git
synced 2026-06-30 06:02:02 -06:00
23392 lines
1.3 MiB
23392 lines
1.3 MiB
(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<t.length;i++)o(t[i]);return o}return r})()({1:[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) {
|
|
if (typeof options !== 'object') {
|
|
options = {};
|
|
}
|
|
this.logLevel = options.logLevel || 'log';
|
|
this.logColors = typeof options.logColors === 'boolean' ? options.logColors : true;
|
|
this.info = typeof options.info === 'string' ? options.info : undefined;
|
|
this.sponsor = typeof options.sponsor === 'boolean' ? options.sponsor : false;
|
|
}
|
|
}
|
|
exports.AceBaseBaseSettings = AceBaseBaseSettings;
|
|
class AceBaseBase extends simple_event_emitter_1.SimpleEventEmitter {
|
|
/**
|
|
* @param dbname Name of the database to open or create
|
|
*/
|
|
constructor(dbname, options) {
|
|
super();
|
|
options = new AceBaseBaseSettings(options || {});
|
|
this.name = dbname;
|
|
// Setup console logging
|
|
this.debug = new debug_1.DebugLogger(options.logLevel, `[${dbname}]`);
|
|
// 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"
|
|
this.debug.write(logo.colorize(logoStyle));
|
|
info && this.debug.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;
|
|
});
|
|
}
|
|
/**
|
|
*
|
|
* @param {()=>void} [callback] (optional) callback function that is called when ready. You can also use the returned promise
|
|
* @returns {Promise<void>} 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;
|
|
}
|
|
}
|
|
get isReady() {
|
|
return this._ready === true;
|
|
}
|
|
/**
|
|
* Allow specific observable implementation to be used
|
|
* @param {Observable} Observable Implementation to use
|
|
*/
|
|
setObservable(Observable) {
|
|
(0, optional_observable_1.setObservable)(Observable);
|
|
}
|
|
/**
|
|
* Creates a reference to a node
|
|
* @param {string} path
|
|
* @returns {DataReference} reference to the requested node
|
|
*/
|
|
ref(path) {
|
|
return new data_reference_1.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 data_reference_1.DataReference(this, path);
|
|
return new data_reference_1.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] type of index to create, such as `fulltext`, `geo`, `array` or `normal` (default)
|
|
* @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 {boolean} [options.rebuild] whether to rebuild the index if it exists already
|
|
* @param {string} [options.textLocale] If the indexed values are strings, which default locale to use
|
|
* @param {object} [options.config] additional index-specific configuration settings
|
|
*/
|
|
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);
|
|
},
|
|
};
|
|
}
|
|
get schema() {
|
|
return {
|
|
get: (path) => {
|
|
return this.api.getSchema(path);
|
|
},
|
|
set: (path, schema) => {
|
|
return this.api.setSchema(path, schema);
|
|
},
|
|
all: () => {
|
|
return this.api.getSchemas();
|
|
},
|
|
check: (path, value, isUpdate) => {
|
|
return 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":25}],2:[function(require,module,exports){
|
|
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.Api = void 0;
|
|
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 {
|
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
constructor(dbname, settings, readyCallback) { }
|
|
/**
|
|
* Provides statistics
|
|
* @param options
|
|
*/
|
|
stats(options) { throw new NotImplementedError('stats'); }
|
|
/**
|
|
* @param path
|
|
* @param event event to subscribe to ("value", "child_added" etc)
|
|
* @param callback callback function
|
|
*/
|
|
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, arg, options) { throw new NotImplementedError('export'); }
|
|
import(path, stream, 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) { 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;
|
|
|
|
},{}],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');
|
|
}
|
|
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]));
|
|
}
|
|
}
|
|
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));
|
|
}
|
|
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;
|
|
}
|
|
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;
|
|
}
|
|
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";
|
|
/**
|
|
* 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);
|
|
}
|
|
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
|
|
|
|
},{"./fingerprint":4,"./pad":6}],6:[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.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 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);
|
|
});
|
|
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
|
|
}
|
|
}
|
|
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) {
|
|
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 });
|
|
}
|
|
else {
|
|
console.warn(`Cached value of live data proxy on "${ref.path}" appears outdated, will be reloaded`);
|
|
await reload();
|
|
}
|
|
});
|
|
// 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;
|
|
}
|
|
// Add current (new) values to mutations
|
|
mutations.forEach(mutation => {
|
|
mutation.value = (0, utils_1.cloneObject)(getTargetValue(cache, mutation.target));
|
|
});
|
|
// 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 });
|
|
});
|
|
// 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);
|
|
}
|
|
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 });
|
|
}
|
|
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 } });
|
|
}
|
|
}
|
|
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(err => {
|
|
clientEventEmitter.emit('error', { source: 'update', message: `Error processing update of "/${ref.path}"`, details: err });
|
|
// console.warn(`Proxy could not update DB, should rollback (${update.type}) the proxy value of "${update.ref.path}" to: `, update.previous);
|
|
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);
|
|
}
|
|
}, 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)) });
|
|
}
|
|
// 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 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;
|
|
}
|
|
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 {
|
|
// 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] : {};
|
|
}
|
|
}
|
|
});
|
|
}
|
|
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').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);
|
|
}
|
|
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 /*, previous, isRemote, context */) => {
|
|
subscriber.next(value);
|
|
});
|
|
return function unsubscribe() {
|
|
subscription.stop();
|
|
};
|
|
};
|
|
if (flag === 'subscribe') {
|
|
return subscribe;
|
|
}
|
|
// 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;
|
|
}
|
|
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);
|
|
}
|
|
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 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
|
|
}
|
|
// 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);
|
|
},
|
|
};
|
|
}
|
|
}
|
|
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') {
|
|
// Remove it
|
|
targetObject instanceof Array ? targetObject.splice(prop, 1) : delete targetObject[prop];
|
|
}
|
|
else {
|
|
// Set or update it
|
|
targetObject[prop] = value;
|
|
}
|
|
}
|
|
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);
|
|
}
|
|
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
|
|
}
|
|
// 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);
|
|
}
|
|
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;
|
|
}
|
|
const isArray = target instanceof Array;
|
|
if (prop === 'toString') {
|
|
return function toString() {
|
|
return `[LiveDataProxy for "${targetRef.path}"]`;
|
|
};
|
|
}
|
|
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;
|
|
}
|
|
};
|
|
}
|
|
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);
|
|
}
|
|
});
|
|
}
|
|
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;
|
|
}
|
|
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":8,"./data-snapshot":9,"./id":11,"./optional-observable":14,"./path-info":16,"./path-reference":17,"./process":18,"./simple-event-emitter":22,"./utils":26}],8:[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) {
|
|
if (!path) {
|
|
path = '';
|
|
}
|
|
path = path.replace(/^\/|\/$/g, ''); // Trim slashes
|
|
const pathInfo = path_info_1.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 || {},
|
|
context: {},
|
|
pushed: false,
|
|
cursor: null,
|
|
};
|
|
this.db = db; //Object.defineProperty(this, "db", ...)
|
|
}
|
|
context(context = undefined, 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() { return this[_private].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)
|
|
*/
|
|
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 completion callback to use instead of returning promise
|
|
* @returns promise that resolves with this reference when completed (when not using onComplete callback)
|
|
*/
|
|
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 object containing the properties to update
|
|
* @param onComplete completion callback to use instead of returning promise
|
|
* @return returns promise that resolves with this reference once completed (when not using onComplete callback)
|
|
*/
|
|
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;
|
|
}
|
|
/**
|
|
* 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 retrieve data upon changes (eg if you want to exclude certain child
|
|
* data from loading)
|
|
* @param event Name of the event to subscribe to
|
|
* @param callback Callback function, event settings, or whether or not to run callbacks on current values when using "value" or "child_added" events
|
|
* @param cancelCallback Function to call when the subscription is not allowed, or denied access later on
|
|
* @returns returns an EventStream
|
|
*/
|
|
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.debug.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.debug.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);
|
|
// typeof callback === 'function' && 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 => {
|
|
const childSnap = new data_snapshot_1.DataSnapshot(this.child(key), val[key]);
|
|
eventPublisher.publish(childSnap);
|
|
// typeof callback === 'function' && callback(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 = () => {
|
|
this.db.api.reflect(this.path, 'children', { limit, skip })
|
|
.then(children => {
|
|
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();
|
|
}
|
|
}
|
|
};
|
|
if (this.db.isReady) {
|
|
subscribe();
|
|
}
|
|
else {
|
|
this.db.ready(subscribe);
|
|
}
|
|
return eventStream;
|
|
}
|
|
/**
|
|
* Unsubscribes from a previously added event
|
|
* @param event Name of the event
|
|
* @param callback callback function to remove
|
|
* @returns returns this `DataReference` instance
|
|
*/
|
|
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.debug.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; // only return promise if no callback is used
|
|
}
|
|
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) {
|
|
// 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;
|
|
}
|
|
else {
|
|
return promise;
|
|
}
|
|
}
|
|
/**
|
|
* 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);
|
|
});
|
|
}
|
|
/**
|
|
* @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);
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
/**
|
|
* 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);
|
|
}
|
|
/**
|
|
* Quickly checks if this reference has a value in the database, without returning its data
|
|
* @returns {Promise<boolean>} | 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);
|
|
}
|
|
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();
|
|
}
|
|
return this.db.api.export(this.path, write, 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.debug.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) {
|
|
// 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;
|
|
}
|
|
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' ? [] : {};
|
|
}
|
|
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`);
|
|
}
|
|
// 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;
|
|
}
|
|
/**
|
|
* @deprecated use .filter instead
|
|
*/
|
|
where(key, op, compare) {
|
|
return this.filter(key, op, compare);
|
|
}
|
|
/**
|
|
* Limits the number of query results to n
|
|
*/
|
|
take(n) {
|
|
this[_private].take = n;
|
|
return this;
|
|
}
|
|
/**
|
|
* Skips the first n query results
|
|
*/
|
|
skip(n) {
|
|
this[_private].skip = n;
|
|
return this;
|
|
}
|
|
/**
|
|
* Sorts the query results
|
|
*/
|
|
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;
|
|
}
|
|
/**
|
|
* @deprecated use .sort instead
|
|
*/
|
|
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 ref = new DataReference(this.ref.db, ev.path);
|
|
const eventData = { name: ev.name };
|
|
if (options.snapshots && ev.name !== 'remove') {
|
|
const val = db.types.deserialize(ev.path, ev.value);
|
|
eventData.snapshot = new data_snapshot_1.DataSnapshot(ref, val, false);
|
|
}
|
|
else {
|
|
eventData.ref = ref;
|
|
}
|
|
ev = eventData;
|
|
}
|
|
listeners.forEach(callback => { try {
|
|
callback(ev);
|
|
}
|
|
catch (e) { } });
|
|
};
|
|
// 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
|
|
*/
|
|
count() {
|
|
return this.get({ snapshots: false }).then(refs => refs.length);
|
|
}
|
|
/**
|
|
* Executes the query and returns if there are any results
|
|
*/
|
|
exists() {
|
|
return this.count().then(count => count > 0);
|
|
}
|
|
/**
|
|
* Executes the query, removes all matches from the database
|
|
* @returns returns an Promise that resolves once all matches have been removed, or void if a callback is used
|
|
*/
|
|
remove(callback) {
|
|
const promise = this.get({ snapshots: false })
|
|
.then((refs) => {
|
|
return Promise.all(refs.map(ref => ref.remove()
|
|
.then(() => {
|
|
return { success: true, ref };
|
|
})
|
|
.catch(err => {
|
|
return { success: false, error: err, ref };
|
|
})))
|
|
.then(results => {
|
|
callback && callback(results);
|
|
return results;
|
|
});
|
|
});
|
|
if (!callback) {
|
|
return promise;
|
|
}
|
|
}
|
|
/**
|
|
* Subscribes to an event. Supported events are:
|
|
* "stats": receive information about query performance.
|
|
* "hints": receive query or index optimization hints
|
|
* "add", "change", "remove": receive real-time query result changes
|
|
* @param event Name of the event to subscribe to
|
|
* @param callback Callback function
|
|
* @returns returns reference to this query
|
|
*/
|
|
on(event, callback) {
|
|
if (!this[_private].events[event]) {
|
|
this[_private].events[event] = [];
|
|
}
|
|
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;
|
|
}
|
|
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');
|
|
}
|
|
// Get all query results. This could be tweaked further using paging
|
|
const refs = await this.getRefs();
|
|
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.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.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":23}],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 {
|
|
/**
|
|
* 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';
|
|
};
|
|
this.context = () => { return context || {}; };
|
|
}
|
|
exists() { return false; }
|
|
/**
|
|
* Creates a DataSnapshot instance (for internal AceBase usage only)
|
|
*/
|
|
static for(ref, value) {
|
|
return new DataSnapshot(ref, value);
|
|
}
|
|
/**
|
|
* Gets a new snapshot for a child node
|
|
* @param path child key or path
|
|
* @returns Returns a DataSnapshot of the child
|
|
*/
|
|
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 {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 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();
|
|
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);
|
|
});
|
|
}
|
|
/**
|
|
* @type {string|number}
|
|
*/
|
|
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;
|
|
};
|
|
}
|
|
/**
|
|
* 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();
|
|
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);
|
|
});
|
|
}
|
|
/**
|
|
* Gets a snapshot of a mutated node
|
|
* @param index index of the mutation
|
|
* @returns Returns a DataSnapshot of the mutated node
|
|
*/
|
|
child(index) {
|
|
if (typeof index !== 'number') {
|
|
throw new Error('child index must be a number');
|
|
}
|
|
const mutation = this.val()[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.prefix = prefix;
|
|
this.setLevel(level);
|
|
}
|
|
setLevel(level) {
|
|
const prefix = this.prefix ? this.prefix + ' %s' : '';
|
|
this.level = level;
|
|
this.verbose = ['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.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.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);
|
|
}
|
|
};
|
|
}
|
|
}
|
|
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 {
|
|
static set timeBias(bias) {
|
|
if (typeof bias !== 'number') {
|
|
return;
|
|
}
|
|
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.PartialArray = exports.ObjectCollection = exports.SchemaDefinition = exports.Colorize = exports.ColorStyle = exports.SimpleEventEmitter = exports.proxyAccess = 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.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 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 data_proxy_1 = require("./data-proxy");
|
|
Object.defineProperty(exports, "proxyAccess", { enumerable: true, get: function () { return data_proxy_1.proxyAccess; } });
|
|
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 object_collection_1 = require("./object-collection");
|
|
Object.defineProperty(exports, "ObjectCollection", { enumerable: true, get: function () { return object_collection_1.ObjectCollection; } });
|
|
var partial_array_1 = require("./partial-array");
|
|
Object.defineProperty(exports, "PartialArray", { enumerable: true, get: function () { return partial_array_1.PartialArray; } });
|
|
|
|
},{"./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,"./subscription":23,"./transport":24,"./type-mappings":25,"./utils":26}],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.ObservableShim = exports.setObservable = exports.getObservable = void 0;
|
|
let _observable;
|
|
function getObservable() {
|
|
if (_observable) {
|
|
return _observable;
|
|
}
|
|
if (typeof window !== 'undefined' && window.Observable) {
|
|
_observable = window.Observable;
|
|
return _observable;
|
|
}
|
|
try {
|
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
const { Observable } = require('rxjs'); // fails in ESM module, need an elegant way to handle this. Can't use dynamic import() because it 1) requires Node 12+ and 2) causes Webpack build to fail if rxjs is not installed
|
|
if (!Observable) {
|
|
throw new Error('not loaded');
|
|
}
|
|
_observable = Observable;
|
|
return Observable;
|
|
}
|
|
catch (err) {
|
|
throw new Error('RxJS Observable could not be loaded. If you are using a browser build, add it to AceBase using db.setObservable. For node.js builds, add it to your project with: npm i rxjs');
|
|
}
|
|
}
|
|
exports.getObservable = getObservable;
|
|
function setObservable(Observable) {
|
|
if (Observable === 'shim') {
|
|
console.warn('Using AceBase\'s simple Observable shim. Only use this if you know what you\'re doing.');
|
|
Observable = ObservableShim;
|
|
}
|
|
_observable = Observable;
|
|
}
|
|
exports.setObservable = setObservable;
|
|
/**
|
|
* 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 ObservableShim {
|
|
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;
|
|
}
|
|
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.ObservableShim = ObservableShim;
|
|
|
|
},{"rxjs":27}],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];
|
|
}
|
|
}
|
|
}
|
|
else if (sparseArray) {
|
|
Object.assign(this, sparseArray);
|
|
}
|
|
}
|
|
}
|
|
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 {
|
|
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}]`, '');
|
|
}
|
|
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);
|
|
}
|
|
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') {
|
|
childKey = getPathKeys(childKey);
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
});
|
|
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 += '/';
|
|
}
|
|
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);
|
|
}
|
|
}, '');
|
|
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 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] === '$'));
|
|
});
|
|
}
|
|
/**
|
|
* 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;
|
|
|
|
},{}],17:[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;
|
|
|
|
},{}],18:[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);
|
|
},
|
|
};
|
|
|
|
},{}],19:[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;
|
|
}
|
|
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 >= '0' && c <= '9')) {
|
|
nr += c;
|
|
pos++;
|
|
}
|
|
type.value = nr.includes('.') ? parseFloat(nr) : 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] === '}') {
|
|
break;
|
|
}
|
|
consumeCharacter(',');
|
|
}
|
|
consumeCharacter('}');
|
|
}
|
|
else if (definition[pos] === '/') {
|
|
// Read regular expression defintion
|
|
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', 'undefined', 'String', 'Number', 'Boolean'].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') {
|
|
// 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>, string[][] becomes Array<Array<string>>
|
|
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) {
|
|
// 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(', ')}` };
|
|
}
|
|
// 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 };
|
|
}
|
|
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 };
|
|
}
|
|
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; i < wildcardChildKeys.length && result.ok; i++) {
|
|
const childKey = wildcardChildKeys[i];
|
|
result = checkProperty({ name: childKey, types: wildcardProperty.types, optional: true, wildcard: true });
|
|
}
|
|
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.
|
|
}
|
|
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)) { // !(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;
|
|
}
|
|
// 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 Array: throw new Error('Schema error: Array cannot be used without a type. Use string[] or Array<string> instead');
|
|
default: throw new Error(`Schema error: unknown type used: ${val.name}`);
|
|
}
|
|
}
|
|
class SchemaDefinition {
|
|
constructor(definition) {
|
|
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'].includes(typeof val)) {
|
|
throw new Error(`Type definition for key "${key}" must be a string, number, boolean, object, regular expression, or one of these classes: String, Number, Boolean, Date`);
|
|
}
|
|
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) {
|
|
return checkType(path, this.type, value, partial, trailKeys);
|
|
}
|
|
}
|
|
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 * 1000) : Infinity;
|
|
/**
|
|
* 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 SimpleCache {
|
|
constructor(options) {
|
|
var _a;
|
|
this.enabled = true;
|
|
if (typeof options === 'number') {
|
|
// Old signature: only expirySeconds given
|
|
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();
|
|
// Cleanup every minute
|
|
const interval = setInterval(() => { this.cleanUp(); }, 60 * 1000);
|
|
(_a = interval.unref) === null || _a === void 0 ? void 0 : _a.call(interval);
|
|
}
|
|
get size() { return this.cache.size; }
|
|
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;
|
|
} // 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.SimpleCache = SimpleCache;
|
|
|
|
},{"./utils":26}],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");
|
|
// 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;
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
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) {
|
|
let resolve;
|
|
const promise = new Promise(rs => {
|
|
if (!callback) {
|
|
// No callback used, promise only
|
|
resolve = rs;
|
|
}
|
|
else {
|
|
// Callback used, maybe also returned promise
|
|
resolve = (data) => {
|
|
rs(data); // resolve promise
|
|
callback(data); // trigger callback
|
|
};
|
|
}
|
|
});
|
|
if (this._oneTimeEvents.has(event)) {
|
|
runCallback(resolve, this._oneTimeEvents.get(event));
|
|
}
|
|
else {
|
|
this._subscriptions.push({ event, callback: resolve, once: true });
|
|
}
|
|
return promise;
|
|
}
|
|
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;
|
|
}
|
|
try {
|
|
s.callback(data);
|
|
}
|
|
catch (err) {
|
|
console.error('Error in subscription callback', err);
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
exports.SimpleEventEmitter = SimpleEventEmitter;
|
|
|
|
},{}],23:[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
|
|
* @param {} 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',
|
|
activatePromises: [],
|
|
};
|
|
}
|
|
/**
|
|
* Notifies when subscription is activated or canceled
|
|
* @param callback optional callback when subscription is activated or canceled
|
|
* @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)
|
|
});
|
|
});
|
|
}
|
|
_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 {
|
|
/**
|
|
*
|
|
* @param eventPublisherCallback
|
|
*/
|
|
constructor(eventPublisherCallback) {
|
|
const subscribers = [];
|
|
let noMoreSubscribersCallback;
|
|
let activationState;
|
|
const _stoppedState = 'stopped (no more subscribers)';
|
|
this.subscribe = (callback, activationCallback) => {
|
|
if (typeof callback !== 'function') {
|
|
throw new TypeError('callback must be a function');
|
|
}
|
|
else if (activationState === _stoppedState) {
|
|
throw new Error('stream can\'t be used anymore because all subscribers were stopped');
|
|
}
|
|
const sub = {
|
|
callback,
|
|
activationCallback: function (activated, cancelReason) {
|
|
activationCallback && 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 && activationCallback(true);
|
|
sub.subscription._setActivationState(true);
|
|
}
|
|
else if (typeof activationState === 'string') {
|
|
activationCallback && activationCallback(false, activationState);
|
|
sub.subscription._setActivationState(false, activationState);
|
|
}
|
|
}
|
|
return sub.subscription;
|
|
};
|
|
const checkActiveSubscribers = () => {
|
|
let ret;
|
|
if (subscribers.length === 0) {
|
|
ret = noMoreSubscribersCallback && noMoreSubscribersCallback();
|
|
activationState = _stoppedState;
|
|
}
|
|
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 {any} val
|
|
* @returns {boolean} returns whether there are subscribers left
|
|
*/
|
|
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;
|
|
};
|
|
/**
|
|
* 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 => {
|
|
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 || new Error('unknown reason'));
|
|
});
|
|
subscribers.splice(0); // Clear all
|
|
};
|
|
const publisher = new EventPublisher(publish, start, cancel);
|
|
eventPublisherCallback(publisher);
|
|
}
|
|
}
|
|
exports.EventStream = EventStream;
|
|
|
|
},{}],24:[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;
|
|
}
|
|
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);
|
|
}
|
|
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;
|
|
}
|
|
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,
|
|
};
|
|
}
|
|
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 (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_1.ascii85.encode(val); //ascii85.encode(Buffer.from(val)).toString();
|
|
mappings[path] = 'binary';
|
|
}
|
|
else if (val instanceof path_reference_1.PathReference) {
|
|
obj[key] = val.path;
|
|
mappings[path] = 'reference';
|
|
}
|
|
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';
|
|
}
|
|
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 (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 {
|
|
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;
|
|
}
|
|
}
|
|
else {
|
|
// Primitive value. Don't serialize
|
|
return val;
|
|
}
|
|
};
|
|
const serialized = getSerializedValue(obj);
|
|
if (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;
|
|
}
|
|
switch (data['.type']) {
|
|
case 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;
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
case 'array': {
|
|
// partial ("sparse") array, deserialize children into a copy
|
|
const copy = {};
|
|
for (const index in data) {
|
|
copy[index] = (0, exports.deserialize2)(data[index]);
|
|
}
|
|
delete copy['.type'];
|
|
return new partial_array_1.PartialArray(copy);
|
|
}
|
|
case 'date': {
|
|
// Date was serialized as a string (UTC)
|
|
const val = data['.val'];
|
|
return new Date(val);
|
|
}
|
|
case 'binary': {
|
|
// ascii85 encoded binary data
|
|
const val = data['.val'];
|
|
return ascii85_1.ascii85.decode(val);
|
|
}
|
|
case 'reference': {
|
|
const val = data['.val'];
|
|
return new path_reference_1.PathReference(val);
|
|
}
|
|
case '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);
|
|
}
|
|
}
|
|
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":26}],25:[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 === '*' || 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
|
|
}
|
|
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] === '*' || mkeys[0][0] === '$');
|
|
}
|
|
else {
|
|
mkeys.every((mkey, index) => {
|
|
if (index >= keys.length) {
|
|
return false; // stop .every loop
|
|
}
|
|
else if (mkey === '*' || mkey[0] === '$' || mkey === keys[index]) {
|
|
return true; // continue .every loop
|
|
}
|
|
else {
|
|
isMatch = false;
|
|
return false; // stop .every loop
|
|
}
|
|
});
|
|
}
|
|
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 (action === 'deserialize') {
|
|
// deserialize this object
|
|
const snap = new data_snapshot_1.DataSnapshot(ref, obj);
|
|
obj = mapping.type.deserialize(snap);
|
|
}
|
|
return;
|
|
}
|
|
// Find all nested objects at this trail path
|
|
const process = (parentPath, parent, keys) => {
|
|
if (obj === null || typeof obj !== 'object') {
|
|
return obj;
|
|
}
|
|
const key = keys[0];
|
|
let children = [];
|
|
if (key === '*' || 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 {
|
|
// Get the 1 child
|
|
const child = parent[key];
|
|
if (typeof child === 'object') {
|
|
children.push({ 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) {
|
|
// 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));
|
|
}
|
|
});
|
|
};
|
|
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 {
|
|
/**
|
|
*
|
|
* @param {AceBaseBase} db
|
|
*/
|
|
constructor(db) {
|
|
this.db = db;
|
|
this[_mappings] = {};
|
|
}
|
|
get mappings() { return this[_mappings]; }
|
|
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
|
|
* @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(obj, ref) method if it exists, or execute: new YourClass(obj, ref).
|
|
* 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.
|
|
*/
|
|
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 {
|
|
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') {
|
|
// 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 {
|
|
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;
|
|
},
|
|
};
|
|
}
|
|
/**
|
|
* Serializes any child in given object that has a type mapping
|
|
* @param {string} path | path to the object's location
|
|
* @param {object} obj | object to serialize
|
|
*/
|
|
serialize(path, obj) {
|
|
return process(this.db, this[_mappings], path, obj, 'serialize');
|
|
}
|
|
/**
|
|
* Deserialzes any child in given object that has a type mapping
|
|
* @param {string} path | path to the object's location
|
|
* @param {object} obj | object to deserialize
|
|
*/
|
|
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":26}],26:[function(require,module,exports){
|
|
(function (Buffer){(function (){
|
|
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
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) {
|
|
if (bytes.length !== 8) {
|
|
throw new TypeError('must be 8 bytes');
|
|
}
|
|
const bin = new Uint8Array(bytes);
|
|
const view = new DataView(bin.buffer);
|
|
const nr = view.getFloat64(0);
|
|
return nr;
|
|
}
|
|
exports.bytesToNumber = bytesToNumber;
|
|
const big = {
|
|
zero: BigInt(0),
|
|
one: BigInt(1),
|
|
two: BigInt(2),
|
|
eight: BigInt(8),
|
|
ff: BigInt(0xff),
|
|
};
|
|
function bigintToBytes(number) {
|
|
if (typeof number !== 'bigint') {
|
|
throw new Error('number must be a bigint');
|
|
}
|
|
const bytes = [];
|
|
const negative = number < big.zero;
|
|
while (number !== (negative ? -big.one : big.zero)) {
|
|
const byte = Number(number & big.ff);
|
|
bytes.push(byte);
|
|
number = number >> big.eight;
|
|
}
|
|
return bytes.reverse();
|
|
}
|
|
exports.bigintToBytes = bigintToBytes;
|
|
function bytesToBigint(bytes) {
|
|
const negative = bytes[0] >= 128;
|
|
let number = big.zero;
|
|
if (negative) {
|
|
bytes[0] -= 128;
|
|
}
|
|
for (const b of bytes) {
|
|
number = (number << big.eight) + BigInt(b);
|
|
}
|
|
if (negative) {
|
|
// Invert the bits
|
|
const bits = (BigInt(bytes.length) * big.eight) - big.one;
|
|
number = -(big.two ** bits) + number;
|
|
}
|
|
return number;
|
|
}
|
|
exports.bytesToBigint = 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;
|
|
}
|
|
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 (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 (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 {
|
|
throw new Error(`Cannot convert character ${str.charAt(i)} (code ${code}) to utf-8`);
|
|
}
|
|
}
|
|
else {
|
|
arr.push(code < 128 ? code : 63); // 63 = ?
|
|
}
|
|
}
|
|
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) {
|
|
buffer = Buffer.from(buffer.buffer, buffer.byteOffset, buffer.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
|
|
buffer = new Uint8Array(buffer['buffer'], buffer.byteOffset, buffer.byteLength);
|
|
}
|
|
if (buffer instanceof Buffer || buffer instanceof Array || buffer instanceof Uint8Array) {
|
|
let str = '';
|
|
for (let i = 0; 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;
|
|
}
|
|
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 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));
|
|
}
|
|
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 });
|
|
}
|
|
}
|
|
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 < 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;
|
|
|
|
}).call(this)}).call(this,require("buffer").Buffer)
|
|
},{"./partial-array":15,"./path-reference":17,"./process":18,"buffer":27}],27:[function(require,module,exports){
|
|
|
|
},{}],28:[function(require,module,exports){
|
|
module.exports = [ "\x00","\x01","\x02","\x03","\x04","\x05","\x06","\x07","\x08","\x09","\x0a","\x0b","\x0c","\x0d","\x0e","\x0f","\x10","\x11","\x12","\x13","\x14","\x15","\x16","\x17","\x18","\x19","\x1a","\x1b","\x1c","\x1d","\x1e","\x1f"," ","!","\"","#","$","%","&","'","(",")","*","+",",","-",".","/","0","1","2","3","4","5","6","7","8","9",":",";","<","=",">","?","@","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","[","\\","]","^","_","`","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","{","|","}","~","\x7f","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","",""," ","!","C/","PS","$?","Y=","|","SS","\"","(c)","a","<<","!","","(r)","-","deg","+-","2","3","'","u","P","*",",","1","o",">>","1/4","1/2","3/4","?","A","A","A","A","A","A","AE","C","E","E","E","E","I","I","I","I","D","N","O","O","O","O","O","x","O","U","U","U","U","U","Th","ss","a","a","a","a","a","a","ae","c","e","e","e","e","i","i","i","i","d","n","o","o","o","o","o","/","o","u","u","u","u","y","th","y" ];
|
|
|
|
},{}],29:[function(require,module,exports){
|
|
module.exports = [ "A","a","A","a","A","a","C","c","C","c","C","c","C","c","D","d","D","d","E","e","E","e","E","e","E","e","E","e","G","g","G","g","G","g","G","g","H","h","H","h","I","i","I","i","I","i","I","i","I","i","IJ","","J","j","K","k","k","L","l","L","l","L","l","L","l","L","l","N","n","N","n","N","n","'n","ng","NG","O","o","O","o","O","o","OE","oe","R","r","R","r","R","r","S","s","S","s","S","s","S","s","T","t","T","t","T","t","U","u","U","u","U","u","U","u","U","u","U","u","W","w","Y","y","Y","Z","z","Z","z","Z","z","s","b","B","B","b","6","6","O","C","c","D","D","D","d","d","3","@","E","F","f","G","G","hv","I","I","K","k","l","l","W","N","n","O","O","o","OI","oi","P","p","YR","2","2","SH","sh","t","T","t","T","U","u","Y","V","Y","y","Z","z","ZH","ZH","zh","zh","2","5","5","ts","w","|","||","|=","!","DZ","Dz","dz","LJ","Lj","lj","NJ","Nj","nj","A","a","I","i","O","o","U","u","U","u","U","u","U","u","U","u","@","A","a","A","a","AE","ae","G","g","G","g","K","k","O","o","O","o","ZH","zh","j","DZ","D","dz","G","g","HV","W","N","n","A","a","AE","ae","O","o" ];
|
|
|
|
},{}],30:[function(require,module,exports){
|
|
module.exports = [ "A","a","A","a","E","e","E","e","I","i","I","i","O","o","O","o","R","r","R","r","U","u","U","u","S","s","T","t","Y","y","H","h","[?]","[?]","OU","ou","Z","z","A","a","E","e","O","o","O","o","O","o","O","o","Y","y","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","a","a","a","b","o","c","d","d","e","@","@","e","e","e","e","j","g","g","g","g","u","Y","h","h","i","i","I","l","l","l","lZ","W","W","m","n","n","n","o","OE","O","F","R","R","R","R","r","r","R","R","R","s","S","j","S","S","t","t","U","U","v","^","W","Y","Y","z","z","Z","Z","?","?","?","C","@","B","E","G","H","j","k","L","q","?","?","dz","dZ","dz","ts","tS","tC","fN","ls","lz","WW","]]","[?]","[?]","k","h","j","r","r","r","r","w","y","'","\"","`","'","`","`","'","?","?","<",">","^","V","^","V","'","-","/","\\",",","_","\\","/",":",".","`","'","^","V","+","-","V",".","@",",","~","\"","R","X","G","l","s","x","?","","","","","","","","V","=","\"","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]" ];
|
|
|
|
},{}],31:[function(require,module,exports){
|
|
module.exports = [ "","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","","","","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","'",",","[?]","[?]","[?]","[?]","","[?]","[?]","[?]","?","[?]","[?]","[?]","[?]","[?]","","","A",";","E","E","I","[?]","O","[?]","U","O","I","A","B","G","D","E","Z","E","Th","I","K","L","M","N","Ks","O","P","R","[?]","S","T","U","Ph","Kh","Ps","O","I","U","a","e","e","i","u","a","b","g","d","e","z","e","th","i","k","l","m","n","x","o","p","r","s","s","t","u","ph","kh","ps","o","i","u","o","u","o","[?]","b","th","U","U","U","ph","p","&","[?]","[?]","St","st","W","w","Q","q","Sp","sp","Sh","sh","F","f","Kh","kh","H","h","G","g","CH","ch","Ti","ti","k","r","c","j","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]" ];
|
|
|
|
},{}],32:[function(require,module,exports){
|
|
module.exports = [ "Ie","Io","Dj","Gj","Ie","Dz","I","Yi","J","Lj","Nj","Tsh","Kj","I","U","Dzh","A","B","V","G","D","Ie","Zh","Z","I","I","K","L","M","N","O","P","R","S","T","U","F","Kh","Ts","Ch","Sh","Shch","","Y","'","E","Iu","Ia","a","b","v","gh","d","ie","zh","z","i","i","k","l","m","n","o","p","r","s","t","u","f","kh","ts","ch","sh","shch","","y","'","e","iu","ia","ie","io","dj","gj","ie","dz","i","yi","j","lj","nj","tsh","kj","i","u","dzh","O","o","E","e","Ie","ie","E","e","Ie","ie","O","o","Io","io","Ks","ks","Ps","ps","F","f","Y","y","Y","y","u","u","O","o","O","o","Ot","ot","Q","q","*1000*","","","","","[?]","*100.000*","*1.000.000*","[?]","[?]","\"","\"","R'","r'","G'","g'","G'","g'","G'","g'","Zh'","zh'","Z'","z'","K'","k'","K'","k'","K'","k'","K'","k'","N'","n'","Ng","ng","P'","p'","Kh","kh","S'","s'","T'","t'","U","u","U'","u'","Kh'","kh'","Tts","tts","Ch'","ch'","Ch'","ch'","H","h","Ch","ch","Ch'","ch'","`","Zh","zh","K'","k'","[?]","[?]","N'","n'","[?]","[?]","Ch","ch","[?]","[?]","[?]","a","a","A","a","Ae","ae","Ie","ie","@","@","@","@","Zh","zh","Z","z","Dz","dz","I","i","I","i","O","o","O","o","O","o","E","e","U","u","U","u","U","u","Ch","ch","[?]","[?]","Y","y","[?]","[?]","[?]","[?]","[?]" ];
|
|
|
|
},{}],33:[function(require,module,exports){
|
|
module.exports = [ "[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","A","B","G","D","E","Z","E","E","T`","Zh","I","L","Kh","Ts","K","H","Dz","Gh","Ch","M","Y","N","Sh","O","Ch`","P","J","Rh","S","V","T","R","Ts`","W","P`","K`","O","F","[?]","[?]","<","'","/","!",",","?",".","[?]","a","b","g","d","e","z","e","e","t`","zh","i","l","kh","ts","k","h","dz","gh","ch","m","y","n","sh","o","ch`","p","j","rh","s","v","t","r","ts`","w","p`","k`","o","f","ew","[?]",".","-","[?]","[?]","[?]","[?]","[?]","[?]","","","","","","","","","","","","","","","","","","[?]","","","","","","","","","","","","","","@","e","a","o","i","e","e","a","a","o","[?]","u","'","","","","","","",":","","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","","b","g","d","h","v","z","kh","t","y","k","k","l","m","m","n","n","s","`","p","p","ts","ts","q","r","sh","t","[?]","[?]","[?]","[?]","[?]","V","oy","i","'","\"","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]" ];
|
|
|
|
},{}],34:[function(require,module,exports){
|
|
module.exports = [ "[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]",",","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]",";","[?]","[?]","[?]","?","[?]","","a","'","w'","","y'","","b","@","t","th","j","H","kh","d","dh","r","z","s","sh","S","D","T","Z","aa","G","[?]","[?]","[?]","[?]","[?]","","f","q","k","l","m","n","h","w","~","y","an","un","in","a","u","i","W","","","'","'","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","0","1","2","3","4","5","6","7","8","9","%",".",",","*","[?]","[?]","","'","'","'","","'","'w","'u","'y","tt","tth","b","t","T","p","th","bh","'h","H","ny","dy","H","ch","cch","dd","D","D","Dt","dh","ddh","d","D","D","rr","R","R","R","R","R","R","j","R","S","S","S","S","S","T","GH","F","F","F","v","f","ph","Q","Q","kh","k","K","K","ng","K","g","G","N","G","G","G","L","L","L","L","N","N","N","N","N","h","Ch","hy","h","H","@","W","oe","oe","u","yu","yu","W","v","y","Y","Y","W","","","y","y'",".","ae","","","","","","","","@","#","","","","","","","","","","","^","","","","","[?]","[?]","0","1","2","3","4","5","6","7","8","9","Sh","D","Gh","&","+m" ];
|
|
|
|
},{}],35:[function(require,module,exports){
|
|
module.exports = [ "//","/",",","!","!","-",",",",",";","?","~","{","}","*","[?]","","'","","b","g","g","d","d","h","w","z","H","t","t","y","yh","k","l","m","n","s","s","`","p","p","S","q","r","sh","t","[?]","[?]","[?]","a","a","a","A","A","A","e","e","e","E","i","i","u","u","u","o","","`","'","","","X","Q","@","@","|","+","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","h","sh","n","r","b","L","k","'","v","m","f","dh","th","l","g","ny","s","d","z","t","y","p","j","ch","tt","hh","kh","th","z","sh","s","d","t","z","`","gh","q","w","a","aa","i","ee","u","oo","e","ey","o","oa","","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]" ];
|
|
|
|
},{}],36:[function(require,module,exports){
|
|
module.exports = [ "[?]","N","N","H","[?]","a","aa","i","ii","u","uu","R","L","eN","e","e","ai","oN","o","o","au","k","kh","g","gh","ng","c","ch","j","jh","ny","tt","tth","dd","ddh","nn","t","th","d","dh","n","nnn","p","ph","b","bh","m","y","r","rr","l","l","lll","v","sh","ss","s","h","[?]","[?]","'","'","aa","i","ii","u","uu","R","RR","eN","e","e","ai","oN","o","o","au","","[?]","[?]","AUM","'","'","`","'","[?]","[?]","[?]","q","khh","ghh","z","dddh","rh","f","yy","RR","LL","L","LL"," / "," // ","0","1","2","3","4","5","6","7","8","9",".","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","N","N","H","[?]","a","aa","i","ii","u","uu","R","RR","[?]","[?]","e","ai","[?]","[?]","o","au","k","kh","g","gh","ng","c","ch","j","jh","ny","tt","tth","dd","ddh","nn","t","th","d","dh","n","[?]","p","ph","b","bh","m","y","r","[?]","l","[?]","[?]","[?]","sh","ss","s","h","[?]","[?]","'","[?]","aa","i","ii","u","uu","R","RR","[?]","[?]","e","ai","[?]","[?]","o","au","","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","+","[?]","[?]","[?]","[?]","rr","rh","[?]","yy","RR","LL","L","LL","[?]","[?]","0","1","2","3","4","5","6","7","8","9","r'","r`","Rs","Rs","1/","2/","3/","4/"," 1 - 1/","/16","","[?]","[?]","[?]","[?]" ];
|
|
|
|
},{}],37:[function(require,module,exports){
|
|
module.exports = [ "[?]","[?]","N","[?]","[?]","a","aa","i","ii","u","uu","[?]","[?]","[?]","[?]","ee","ai","[?]","[?]","oo","au","k","kh","g","gh","ng","c","ch","j","jh","ny","tt","tth","dd","ddh","nn","t","th","d","dh","n","[?]","p","ph","b","bb","m","y","r","[?]","l","ll","[?]","v","sh","[?]","s","h","[?]","[?]","'","[?]","aa","i","ii","u","uu","[?]","[?]","[?]","[?]","ee","ai","[?]","[?]","oo","au","","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","khh","ghh","z","rr","[?]","f","[?]","[?]","[?]","[?]","[?]","[?]","[?]","0","1","2","3","4","5","6","7","8","9","N","H","","","G.E.O.","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","N","N","H","[?]","a","aa","i","ii","u","uu","R","[?]","eN","[?]","e","ai","oN","[?]","o","au","k","kh","g","gh","ng","c","ch","j","jh","ny","tt","tth","dd","ddh","nn","t","th","d","dh","n","[?]","p","ph","b","bh","m","ya","r","[?]","l","ll","[?]","v","sh","ss","s","h","[?]","[?]","'","'","aa","i","ii","u","uu","R","RR","eN","[?]","e","ai","oN","[?]","o","au","","[?]","[?]","AUM","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","RR","[?]","[?]","[?]","[?]","[?]","0","1","2","3","4","5","6","7","8","9","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]" ];
|
|
|
|
},{}],38:[function(require,module,exports){
|
|
module.exports = [ "[?]","N","N","H","[?]","a","aa","i","ii","u","uu","R","L","[?]","[?]","e","ai","[?]","[?]","o","au","k","kh","g","gh","ng","c","ch","j","jh","ny","tt","tth","dd","ddh","nn","t","th","d","dh","n","[?]","p","ph","b","bh","m","y","r","[?]","l","ll","[?]","","sh","ss","s","h","[?]","[?]","'","'","aa","i","ii","u","uu","R","[?]","[?]","[?]","e","ai","[?]","[?]","o","au","","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","+","+","[?]","[?]","[?]","[?]","rr","rh","[?]","yy","RR","LL","[?]","[?]","[?]","[?]","0","1","2","3","4","5","6","7","8","9","","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","N","H","[?]","a","aa","i","ii","u","uu","[?]","[?]","[?]","e","ee","ai","[?]","o","oo","au","k","[?]","[?]","[?]","ng","c","[?]","j","[?]","ny","tt","[?]","[?]","[?]","nn","t","[?]","[?]","[?]","n","nnn","p","[?]","[?]","[?]","m","y","r","rr","l","ll","lll","v","[?]","ss","s","h","[?]","[?]","[?]","[?]","aa","i","ii","u","uu","[?]","[?]","[?]","e","ee","ai","[?]","o","oo","au","","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","+","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","0","1","2","3","4","5","6","7","8","9","+10+","+100+","+1000+","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]" ];
|
|
|
|
},{}],39:[function(require,module,exports){
|
|
module.exports = [ "[?]","N","N","H","[?]","a","aa","i","ii","u","uu","R","L","[?]","e","ee","ai","[?]","o","oo","au","k","kh","g","gh","ng","c","ch","j","jh","ny","tt","tth","dd","ddh","nn","t","th","d","dh","n","[?]","p","ph","b","bh","m","y","r","rr","l","ll","[?]","v","sh","ss","s","h","[?]","[?]","[?]","[?]","aa","i","ii","u","uu","R","RR","[?]","e","ee","ai","[?]","o","oo","au","","[?]","[?]","[?]","[?]","[?]","[?]","[?]","+","+","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","RR","LL","[?]","[?]","[?]","[?]","0","1","2","3","4","5","6","7","8","9","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","N","H","[?]","a","aa","i","ii","u","uu","R","L","[?]","e","ee","ai","[?]","o","oo","au","k","kh","g","gh","ng","c","ch","j","jh","ny","tt","tth","dd","ddh","nn","t","th","d","dh","n","[?]","p","ph","b","bh","m","y","r","rr","l","ll","[?]","v","sh","ss","s","h","[?]","[?]","[?]","[?]","aa","i","ii","u","uu","R","RR","[?]","e","ee","ai","[?]","o","oo","au","","[?]","[?]","[?]","[?]","[?]","[?]","[?]","+","+","[?]","[?]","[?]","[?]","[?]","[?]","[?]","lll","[?]","RR","LL","[?]","[?]","[?]","[?]","0","1","2","3","4","5","6","7","8","9","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]" ];
|
|
|
|
},{}],40:[function(require,module,exports){
|
|
module.exports = [ "[?]","[?]","N","H","[?]","a","aa","i","ii","u","uu","R","L","[?]","e","ee","ai","[?]","o","oo","au","k","kh","g","gh","ng","c","ch","j","jh","ny","tt","tth","dd","ddh","nn","t","th","d","dh","n","[?]","p","ph","b","bh","m","y","r","rr","l","ll","lll","v","sh","ss","s","h","[?]","[?]","[?]","[?]","aa","i","ii","u","uu","R","[?]","[?]","e","ee","ai","","o","oo","au","","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","+","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","RR","LL","[?]","[?]","[?]","[?]","0","1","2","3","4","5","6","7","8","9","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","N","H","[?]","a","aa","ae","aae","i","ii","u","uu","R","RR","L","LL","e","ee","ai","o","oo","au","[?]","[?]","[?]","k","kh","g","gh","ng","nng","c","ch","j","jh","ny","jny","nyj","tt","tth","dd","ddh","nn","nndd","t","th","d","dh","n","[?]","nd","p","ph","b","bh","m","mb","y","r","[?]","l","[?]","[?]","v","sh","ss","s","h","ll","f","[?]","[?]","[?]","","[?]","[?]","[?]","[?]","aa","ae","aae","i","ii","u","[?]","uu","[?]","R","e","ee","ai","o","oo","au","L","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","RR","LL"," . ","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]" ];
|
|
|
|
},{}],41:[function(require,module,exports){
|
|
module.exports = [ "[?]","k","kh","kh","kh","kh","kh","ng","cch","ch","ch","ch","ch","y","d","t","th","th","th","n","d","t","th","th","th","n","b","p","ph","f","ph","f","ph","m","y","r","R","l","L","w","s","s","s","h","l","`","h","~","a","a","aa","am","i","ii","ue","uue","u","uu","'","[?]","[?]","[?]","[?]","Bh.","e","ae","o","ai","ai","ao","+","","","","","","","M",""," * ","0","1","2","3","4","5","6","7","8","9"," // "," /// ","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","k","kh","[?]","kh","[?]","[?]","ng","ch","[?]","s","[?]","[?]","ny","[?]","[?]","[?]","[?]","[?]","[?]","d","h","th","th","[?]","n","b","p","ph","f","ph","f","[?]","m","y","r","[?]","l","[?]","w","[?]","[?]","s","h","[?]","`","","~","a","","aa","am","i","ii","y","yy","u","uu","[?]","o","l","ny","[?]","[?]","e","ei","o","ay","ai","[?]","+","[?]","","","","","","M","[?]","[?]","0","1","2","3","4","5","6","7","8","9","[?]","[?]","hn","hm","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]" ];
|
|
|
|
},{}],42:[function(require,module,exports){
|
|
module.exports = [ "AUM","","","","","","",""," // "," * ","","-"," / "," / "," // "," -/ "," +/ "," X/ "," /XX/ "," /X/ ",", ","","","","","","","","","","","","0","1","2","3","4","5","6","7","8","9",".5","1.5","2.5","3.5","4.5","5.5","6.5","7.5","8.5","-.5","+","*","^","_","","~","[?]","]","[[","]]","","","k","kh","g","gh","ng","c","ch","j","[?]","ny","tt","tth","dd","ddh","nn","t","th","d","dh","n","p","ph","b","bh","m","ts","tsh","dz","dzh","w","zh","z","'","y","r","l","sh","ssh","s","h","a","kss","r","[?]","[?]","[?]","[?]","[?]","[?]","aa","i","ii","u","uu","R","RR","L","LL","e","ee","o","oo","M","H","i","ii","","","","","","","","","","","[?]","[?]","[?]","[?]","k","kh","g","gh","ng","c","ch","j","[?]","ny","tt","tth","dd","ddh","nn","t","th","d","dh","n","p","ph","b","bh","m","ts","tsh","dz","dzh","w","zh","z","'","y","r","l","sh","ss","s","h","a","kss","w","y","r","[?]","X"," :X: "," /O/ "," /o/ "," \\o\\ "," (O) ","","","","","","","","","","[?]","[?]","","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]" ];
|
|
|
|
},{}],43:[function(require,module,exports){
|
|
module.exports = [ "k","kh","g","gh","ng","c","ch","j","jh","ny","nny","tt","tth","dd","ddh","nn","tt","th","d","dh","n","p","ph","b","bh","m","y","r","l","w","s","h","ll","a","[?]","i","ii","u","uu","e","[?]","o","au","[?]","aa","i","ii","u","uu","e","ai","[?]","[?]","[?]","N","'",":","","[?]","[?]","[?]","[?]","[?]","[?]","0","1","2","3","4","5","6","7","8","9"," / "," // ","n*","r*","l*","e*","sh","ss","R","RR","L","LL","R","RR","L","LL","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","A","B","G","D","E","V","Z","T`","I","K","L","M","N","O","P","Zh","R","S","T","U","P`","K`","G'","Q","Sh","Ch`","C`","Z'","C","Ch","X","J","H","E","Y","W","Xh","OE","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","a","b","g","d","e","v","z","t`","i","k","l","m","n","o","p","zh","r","s","t","u","p`","k`","g'","q","sh","ch`","c`","z'","c","ch","x","j","h","e","y","w","xh","oe","f","[?]","[?]","[?]","[?]"," // ","[?]","[?]","[?]" ];
|
|
|
|
},{}],44:[function(require,module,exports){
|
|
module.exports = [ "g","gg","n","d","dd","r","m","b","bb","s","ss","","j","jj","c","k","t","p","h","ng","nn","nd","nb","dg","rn","rr","rh","rN","mb","mN","bg","bn","","bs","bsg","bst","bsb","bss","bsj","bj","bc","bt","bp","bN","bbN","sg","sn","sd","sr","sm","sb","sbg","sss","s","sj","sc","sk","st","sp","sh","","","","","Z","g","d","m","b","s","Z","","j","c","t","p","N","j","","","","","ck","ch","","","pb","pN","hh","Q","[?]","[?]","[?]","[?]","[?]","","","a","ae","ya","yae","eo","e","yeo","ye","o","wa","wae","oe","yo","u","weo","we","wi","yu","eu","yi","i","a-o","a-u","ya-o","ya-yo","eo-o","eo-u","eo-eu","yeo-o","yeo-u","o-eo","o-e","o-ye","o-o","o-u","yo-ya","yo-yae","yo-yeo","yo-o","yo-i","u-a","u-ae","u-eo-eu","u-ye","u-u","yu-a","yu-eo","yu-e","yu-yeo","yu-ye","yu-u","yu-i","eu-u","eu-eu","yi-u","i-a","i-ya","i-o","i-u","i-eu","i-U","U","U-eo","U-u","U-i","UU","[?]","[?]","[?]","[?]","[?]","g","gg","gs","n","nj","nh","d","l","lg","lm","lb","ls","lt","lp","lh","m","b","bs","s","ss","ng","j","c","k","t","p","h","gl","gsg","ng","nd","ns","nZ","nt","dg","tl","lgs","ln","ld","lth","ll","lmg","lms","lbs","lbh","rNp","lss","lZ","lk","lQ","mg","ml","mb","ms","mss","mZ","mc","mh","mN","bl","bp","ph","pN","sg","sd","sl","sb","Z","g","ss","","kh","N","Ns","NZ","pb","pN","hn","hl","hm","hb","Q","[?]","[?]","[?]","[?]","[?]" ];
|
|
|
|
},{}],45:[function(require,module,exports){
|
|
module.exports = [ "ha","hu","hi","haa","hee","he","ho","[?]","la","lu","li","laa","lee","le","lo","lwa","hha","hhu","hhi","hhaa","hhee","hhe","hho","hhwa","ma","mu","mi","maa","mee","me","mo","mwa","sza","szu","szi","szaa","szee","sze","szo","szwa","ra","ru","ri","raa","ree","re","ro","rwa","sa","su","si","saa","see","se","so","swa","sha","shu","shi","shaa","shee","she","sho","shwa","qa","qu","qi","qaa","qee","qe","qo","[?]","qwa","[?]","qwi","qwaa","qwee","qwe","[?]","[?]","qha","qhu","qhi","qhaa","qhee","qhe","qho","[?]","qhwa","[?]","qhwi","qhwaa","qhwee","qhwe","[?]","[?]","ba","bu","bi","baa","bee","be","bo","bwa","va","vu","vi","vaa","vee","ve","vo","vwa","ta","tu","ti","taa","tee","te","to","twa","ca","cu","ci","caa","cee","ce","co","cwa","xa","xu","xi","xaa","xee","xe","xo","[?]","xwa","[?]","xwi","xwaa","xwee","xwe","[?]","[?]","na","nu","ni","naa","nee","ne","no","nwa","nya","nyu","nyi","nyaa","nyee","nye","nyo","nywa","'a","'u","[?]","'aa","'ee","'e","'o","'wa","ka","ku","ki","kaa","kee","ke","ko","[?]","kwa","[?]","kwi","kwaa","kwee","kwe","[?]","[?]","kxa","kxu","kxi","kxaa","kxee","kxe","kxo","[?]","kxwa","[?]","kxwi","kxwaa","kxwee","kxwe","[?]","[?]","wa","wu","wi","waa","wee","we","wo","[?]","`a","`u","`i","`aa","`ee","`e","`o","[?]","za","zu","zi","zaa","zee","ze","zo","zwa","zha","zhu","zhi","zhaa","zhee","zhe","zho","zhwa","ya","yu","yi","yaa","yee","ye","yo","[?]","da","du","di","daa","dee","de","do","dwa","dda","ddu","ddi","ddaa","ddee","dde","ddo","ddwa" ];
|
|
|
|
},{}],46:[function(require,module,exports){
|
|
module.exports = [ "ja","ju","ji","jaa","jee","je","jo","jwa","ga","gu","gi","gaa","gee","ge","go","[?]","gwa","[?]","gwi","gwaa","gwee","gwe","[?]","[?]","gga","ggu","ggi","ggaa","ggee","gge","ggo","[?]","tha","thu","thi","thaa","thee","the","tho","thwa","cha","chu","chi","chaa","chee","che","cho","chwa","pha","phu","phi","phaa","phee","phe","pho","phwa","tsa","tsu","tsi","tsaa","tsee","tse","tso","tswa","tza","tzu","tzi","tzaa","tzee","tze","tzo","[?]","fa","fu","fi","faa","fee","fe","fo","fwa","pa","pu","pi","paa","pee","pe","po","pwa","rya","mya","fya","[?]","[?]","[?]","[?]","[?]","[?]"," ",".",",",";",":",":: ","?","//","1","2","3","4","5","6","7","8","9","10+","20+","30+","40+","50+","60+","70+","80+","90+","100+","10,000+","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","a","e","i","o","u","v","ga","ka","ge","gi","go","gu","gv","ha","he","hi","ho","hu","hv","la","le","li","lo","lu","lv","ma","me","mi","mo","mu","na","hna","nah","ne","ni","no","nu","nv","qua","que","qui","quo","quu","quv","sa","s","se","si","so","su","sv","da","ta","de","te","di","ti","do","du","dv","dla","tla","tle","tli","tlo","tlu","tlv","tsa","tse","tsi","tso","tsu","tsv","wa","we","wi","wo","wu","wv","ya","ye","yi","yo","yu","yv","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]" ];
|
|
|
|
},{}],47:[function(require,module,exports){
|
|
module.exports = [ "[?]","e","aai","i","ii","o","oo","oo","ee","i","a","aa","we","we","wi","wi","wii","wii","wo","wo","woo","woo","woo","wa","wa","waa","waa","waa","ai","w","'","t","k","sh","s","n","w","n","[?]","w","c","?","l","en","in","on","an","pe","paai","pi","pii","po","poo","poo","hee","hi","pa","paa","pwe","pwe","pwi","pwi","pwii","pwii","pwo","pwo","pwoo","pwoo","pwa","pwa","pwaa","pwaa","pwaa","p","p","h","te","taai","ti","tii","to","too","too","dee","di","ta","taa","twe","twe","twi","twi","twii","twii","two","two","twoo","twoo","twa","twa","twaa","twaa","twaa","t","tte","tti","tto","tta","ke","kaai","ki","kii","ko","koo","koo","ka","kaa","kwe","kwe","kwi","kwi","kwii","kwii","kwo","kwo","kwoo","kwoo","kwa","kwa","kwaa","kwaa","kwaa","k","kw","keh","kih","koh","kah","ce","caai","ci","cii","co","coo","coo","ca","caa","cwe","cwe","cwi","cwi","cwii","cwii","cwo","cwo","cwoo","cwoo","cwa","cwa","cwaa","cwaa","cwaa","c","th","me","maai","mi","mii","mo","moo","moo","ma","maa","mwe","mwe","mwi","mwi","mwii","mwii","mwo","mwo","mwoo","mwoo","mwa","mwa","mwaa","mwaa","mwaa","m","m","mh","m","m","ne","naai","ni","nii","no","noo","noo","na","naa","nwe","nwe","nwa","nwa","nwaa","nwaa","nwaa","n","ng","nh","le","laai","li","lii","lo","loo","loo","la","laa","lwe","lwe","lwi","lwi","lwii","lwii","lwo","lwo","lwoo","lwoo","lwa","lwa","lwaa","lwaa","l","l","l","se","saai","si","sii","so","soo","soo","sa","saa","swe","swe","swi","swi","swii","swii","swo","swo","swoo","swoo" ];
|
|
|
|
},{}],48:[function(require,module,exports){
|
|
module.exports = [ "swa","swa","swaa","swaa","swaa","s","s","sw","s","sk","skw","sW","spwa","stwa","skwa","scwa","she","shi","shii","sho","shoo","sha","shaa","shwe","shwe","shwi","shwi","shwii","shwii","shwo","shwo","shwoo","shwoo","shwa","shwa","shwaa","shwaa","sh","ye","yaai","yi","yii","yo","yoo","yoo","ya","yaa","ywe","ywe","ywi","ywi","ywii","ywii","ywo","ywo","ywoo","ywoo","ywa","ywa","ywaa","ywaa","ywaa","y","y","y","yi","re","re","le","raai","ri","rii","ro","roo","lo","ra","raa","la","rwaa","rwaa","r","r","r","fe","faai","fi","fii","fo","foo","fa","faa","fwaa","fwaa","f","the","the","thi","thi","thii","thii","tho","thoo","tha","thaa","thwaa","thwaa","th","tthe","tthi","ttho","ttha","tth","tye","tyi","tyo","tya","he","hi","hii","ho","hoo","ha","haa","h","h","hk","qaai","qi","qii","qo","qoo","qa","qaa","q","tlhe","tlhi","tlho","tlha","re","ri","ro","ra","ngaai","ngi","ngii","ngo","ngoo","nga","ngaa","ng","nng","she","shi","sho","sha","the","thi","tho","tha","th","lhi","lhii","lho","lhoo","lha","lhaa","lh","the","thi","thii","tho","thoo","tha","thaa","th","b","e","i","o","a","we","wi","wo","wa","ne","ni","no","na","ke","ki","ko","ka","he","hi","ho","ha","ghu","gho","ghe","ghee","ghi","gha","ru","ro","re","ree","ri","ra","wu","wo","we","wee","wi","wa","hwu","hwo","hwe","hwee","hwi","hwa","thu","tho","the","thee","thi","tha","ttu","tto","tte","ttee","tti","tta","pu","po","pe","pee","pi","pa","p","gu","go","ge","gee","gi","ga","khu","kho","khe","khee","khi","kha","kku","kko","kke","kkee","kki" ];
|
|
|
|
},{}],49:[function(require,module,exports){
|
|
module.exports = [ "kka","kk","nu","no","ne","nee","ni","na","mu","mo","me","mee","mi","ma","yu","yo","ye","yee","yi","ya","ju","ju","jo","je","jee","ji","ji","ja","jju","jjo","jje","jjee","jji","jja","lu","lo","le","lee","li","la","dlu","dlo","dle","dlee","dli","dla","lhu","lho","lhe","lhee","lhi","lha","tlhu","tlho","tlhe","tlhee","tlhi","tlha","tlu","tlo","tle","tlee","tli","tla","zu","zo","ze","zee","zi","za","z","z","dzu","dzo","dze","dzee","dzi","dza","su","so","se","see","si","sa","shu","sho","she","shee","shi","sha","sh","tsu","tso","tse","tsee","tsi","tsa","chu","cho","che","chee","chi","cha","ttsu","ttso","ttse","ttsee","ttsi","ttsa","X",".","qai","ngai","nngi","nngii","nngo","nngoo","nnga","nngaa","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]"," ","b","l","f","s","n","h","d","t","c","q","m","g","ng","z","r","a","o","u","e","i","ch","th","ph","p","x","p","<",">","[?]","[?]","[?]","f","v","u","yr","y","w","th","th","a","o","ac","ae","o","o","o","oe","on","r","k","c","k","g","ng","g","g","w","h","h","h","h","n","n","n","i","e","j","g","ae","a","eo","p","z","s","s","s","c","z","t","t","d","b","b","p","p","e","m","m","m","l","l","ng","ng","d","o","ear","ior","qu","qu","qu","s","yr","yr","yr","q","x",".",":","+","17","18","19","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]" ];
|
|
|
|
},{}],50:[function(require,module,exports){
|
|
module.exports = [ "[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","k","kh","g","gh","ng","c","ch","j","jh","ny","t","tth","d","ddh","nn","t","th","d","dh","n","p","ph","b","bh","m","y","r","l","v","sh","ss","s","h","l","q","a","aa","i","ii","u","uk","uu","uuv","ry","ryy","ly","lyy","e","ai","oo","oo","au","a","aa","aa","i","ii","y","yy","u","uu","ua","oe","ya","ie","e","ae","ai","oo","au","M","H","a`","","","","r","","!","","","","","","."," // ",":","+","++"," * "," /// ","KR","'","[?]","[?]","[?]","0","1","2","3","4","5","6","7","8","9","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]" ];
|
|
|
|
},{}],51:[function(require,module,exports){
|
|
module.exports = [ " @ "," ... ",", ",". ",": "," // ","","-",", ",". ","","","","","","[?]","0","1","2","3","4","5","6","7","8","9","[?]","[?]","[?]","[?]","[?]","[?]","a","e","i","o","u","O","U","ee","n","ng","b","p","q","g","m","l","s","sh","t","d","ch","j","y","r","w","f","k","kha","ts","z","h","zr","lh","zh","ch","-","e","i","o","u","O","U","ng","b","p","q","g","m","t","d","ch","j","ts","y","w","k","g","h","jy","ny","dz","e","i","iy","U","u","ng","k","g","h","p","sh","t","d","j","f","g","h","ts","z","r","ch","zh","i","k","r","f","zh","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","H","X","W","M"," 3 "," 333 ","a","i","k","ng","c","tt","tth","dd","nn","t","d","p","ph","ss","zh","z","a","t","zh","gh","ng","c","jh","tta","ddh","t","dh","ss","cy","zh","z","u","y","bh","'","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]" ];
|
|
|
|
},{}],52:[function(require,module,exports){
|
|
module.exports = [ "A","a","B","b","B","b","B","b","C","c","D","d","D","d","D","d","D","d","D","d","E","e","E","e","E","e","E","e","E","e","F","f","G","g","H","h","H","h","H","h","H","h","H","h","I","i","I","i","K","k","K","k","K","k","L","l","L","l","L","l","L","l","M","m","M","m","M","m","N","n","N","n","N","n","N","n","O","o","O","o","O","o","O","o","P","p","P","p","R","r","R","r","R","r","R","r","S","s","S","s","S","s","S","s","S","s","T","t","T","t","T","t","T","t","U","u","U","u","U","u","U","u","U","u","V","v","V","v","W","w","W","w","W","w","W","w","W","w","X","x","X","x","Y","y","Z","z","Z","z","Z","z","h","t","w","y","a","S","[?]","[?]","[?]","[?]","A","a","A","a","A","a","A","a","A","a","A","a","A","a","A","a","A","a","A","a","A","a","A","a","E","e","E","e","E","e","E","e","E","e","E","e","E","e","E","e","I","i","I","i","O","o","O","o","O","o","O","o","O","o","O","o","O","o","O","o","O","o","O","o","O","o","O","o","U","u","U","u","U","u","U","u","U","u","U","u","U","u","Y","y","Y","y","Y","y","Y","y","[?]","[?]","[?]","[?]","[?]" ];
|
|
|
|
},{}],53:[function(require,module,exports){
|
|
module.exports = [ "a","a","a","a","a","a","a","a","A","A","A","A","A","A","A","A","e","e","e","e","e","e","[?]","[?]","E","E","E","E","E","E","[?]","[?]","e","e","e","e","e","e","e","e","E","E","E","E","E","E","E","E","i","i","i","i","i","i","i","i","I","I","I","I","I","I","I","I","o","o","o","o","o","o","[?]","[?]","O","O","O","O","O","O","[?]","[?]","u","u","u","u","u","u","u","u","[?]","U","[?]","U","[?]","U","[?]","U","o","o","o","o","o","o","o","o","O","O","O","O","O","O","O","O","a","a","e","e","e","e","i","i","o","o","u","u","o","o","[?]","[?]","a","a","a","a","a","a","a","a","A","A","A","A","A","A","A","A","e","e","e","e","e","e","e","e","E","E","E","E","E","E","E","E","o","o","o","o","o","o","o","o","O","O","O","O","O","O","O","O","a","a","a","a","a","[?]","a","a","A","A","A","A","A","'","i","'","~","\"~","e","e","e","[?]","e","e","E","E","E","E","E","'`","''","'~","i","i","i","i","[?]","[?]","i","i","I","I","I","I","[?]","`'","`'","`~","u","u","u","u","R","R","u","u","U","U","U","U","R","\"`","\"'","`","[?]","[?]","o","o","o","[?]","o","o","O","O","O","O","O","'","`" ];
|
|
|
|
},{}],54:[function(require,module,exports){
|
|
module.exports = [ " "," "," "," "," "," "," "," "," "," "," "," ","","","","","-","-","-","-","--","--","||","_","'","'",",","'","\"","\"",",,","\"","+","++","*","*>",".","..","...",".","\n","\n\n","","","","",""," ","%0","%00","'","''","'''","`","``","```","^","<",">","*","!!","!?","-","_","-","^","***","--","/","-[","]-","[?]","?!","!?","7","PP","(]","[)","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","","","","","","","0","","","","4","5","6","7","8","9","+","-","=","(",")","n","0","1","2","3","4","5","6","7","8","9","+","-","=","(",")","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","ECU","CL","Cr","FF","L","mil","N","Pts","Rs","W","NS","D","EU","K","T","Dr","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","","","","","","","","","","","","","","","","","","","","","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]" ];
|
|
|
|
},{}],55:[function(require,module,exports){
|
|
module.exports = [ "","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]"," 1/3 "," 2/3 "," 1/5 "," 2/5 "," 3/5 "," 4/5 "," 1/6 "," 5/6 "," 1/8 "," 3/8 "," 5/8 "," 7/8 "," 1/","I","II","III","IV","V","VI","VII","VIII","IX","X","XI","XII","L","C","D","M","i","ii","iii","iv","v","vi","vii","viii","ix","x","xi","xii","l","c","d","m","(D","D)","((|))",")","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","-","|","-","|","-","|","\\","/","\\","/","-","-","~","~","-","|","-","|","-","-","-","|","-","|","|","-","-","-","-","-","-","|","|","|","|","|","|","|","^","V","\\","=","V","^","-","-","|","|","-","-","|","|","=","|","=","=","|","=","|","=","=","=","=","=","=","|","=","|","=","|","\\","/","\\","/","=","=","~","~","|","|","-","|","-","|","-","-","-","|","-","|","|","|","|","|","|","|","-","\\","\\","|","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]" ];
|
|
|
|
},{}],56:[function(require,module,exports){
|
|
module.exports = [ "[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]" ];
|
|
|
|
},{}],57:[function(require,module,exports){
|
|
arguments[4][56][0].apply(exports,arguments)
|
|
},{"dup":56}],58:[function(require,module,exports){
|
|
module.exports = [ "","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","","","","","","","","","","","","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]" ];
|
|
|
|
},{}],59:[function(require,module,exports){
|
|
module.exports = [ "-","-","|","|","-","-","|","|","-","-","|","|","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","-","-","|","|","-","|","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","+","/","\\","X","-","|","-","|","-","|","-","|","-","|","-","|","#","#","#","#","#","#","#","#","#","#","#","#","#","#","#","#","#","#","#","#","-","|","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","#","#","#","#","#","#","#","#","#","#","#","#","#","#","#","#","#","#","^","^","^","^",">",">",">",">",">",">","V","V","V","V","<","<","<","<","<","<","*","*","*","*","*","*","*","*","*","*","*","*","*","*","*","*","*","*","*","*","*","*","*","*","*","*","*","*","*","*","*","*","*","#","#","#","#","#","^","^","^","O","#","#","#","#","#","#","#","#","[?]","[?]","[?]","[?]","[?]","[?]","[?]" ];
|
|
|
|
},{}],60:[function(require,module,exports){
|
|
module.exports = [ "","","","","","","","","","","","","","","","","","","","","[?]","[?]","[?]","[?]","[?]","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]" ];
|
|
|
|
},{}],61:[function(require,module,exports){
|
|
module.exports = [ "[?]","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","[?]","[?]","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","[?]","","","","","","","","","","","","","","","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]" ];
|
|
|
|
},{}],62:[function(require,module,exports){
|
|
module.exports = [ " ","a","1","b","'","k","2","l","@","c","i","f","/","m","s","p","\"","e","3","h","9","o","6","r","^","d","j","g",">","n","t","q",",","*","5","<","-","u","8","v",".","%","[","$","+","x","!","&",";",":","4","\\","0","z","7","(","_","?","w","]","#","y",")","=","[d7]","[d17]","[d27]","[d127]","[d37]","[d137]","[d237]","[d1237]","[d47]","[d147]","[d247]","[d1247]","[d347]","[d1347]","[d2347]","[d12347]","[d57]","[d157]","[d257]","[d1257]","[d357]","[d1357]","[d2357]","[d12357]","[d457]","[d1457]","[d2457]","[d12457]","[d3457]","[d13457]","[d23457]","[d123457]","[d67]","[d167]","[d267]","[d1267]","[d367]","[d1367]","[d2367]","[d12367]","[d467]","[d1467]","[d2467]","[d12467]","[d3467]","[d13467]","[d23467]","[d123467]","[d567]","[d1567]","[d2567]","[d12567]","[d3567]","[d13567]","[d23567]","[d123567]","[d4567]","[d14567]","[d24567]","[d124567]","[d34567]","[d134567]","[d234567]","[d1234567]","[d8]","[d18]","[d28]","[d128]","[d38]","[d138]","[d238]","[d1238]","[d48]","[d148]","[d248]","[d1248]","[d348]","[d1348]","[d2348]","[d12348]","[d58]","[d158]","[d258]","[d1258]","[d358]","[d1358]","[d2358]","[d12358]","[d458]","[d1458]","[d2458]","[d12458]","[d3458]","[d13458]","[d23458]","[d123458]","[d68]","[d168]","[d268]","[d1268]","[d368]","[d1368]","[d2368]","[d12368]","[d468]","[d1468]","[d2468]","[d12468]","[d3468]","[d13468]","[d23468]","[d123468]","[d568]","[d1568]","[d2568]","[d12568]","[d3568]","[d13568]","[d23568]","[d123568]","[d4568]","[d14568]","[d24568]","[d124568]","[d34568]","[d134568]","[d234568]","[d1234568]","[d78]","[d178]","[d278]","[d1278]","[d378]","[d1378]","[d2378]","[d12378]","[d478]","[d1478]","[d2478]","[d12478]","[d3478]","[d13478]","[d23478]","[d123478]","[d578]","[d1578]","[d2578]","[d12578]","[d3578]","[d13578]","[d23578]","[d123578]","[d4578]","[d14578]","[d24578]","[d124578]","[d34578]","[d134578]","[d234578]","[d1234578]","[d678]","[d1678]","[d2678]","[d12678]","[d3678]","[d13678]","[d23678]","[d123678]","[d4678]","[d14678]","[d24678]","[d124678]","[d34678]","[d134678]","[d234678]","[d1234678]","[d5678]","[d15678]","[d25678]","[d125678]","[d35678]","[d135678]","[d235678]","[d1235678]","[d45678]","[d145678]","[d245678]","[d1245678]","[d345678]","[d1345678]","[d2345678]","[d12345678]" ];
|
|
|
|
},{}],63:[function(require,module,exports){
|
|
module.exports = [ "[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?]","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]" ];
|
|
|
|
},{}],64:[function(require,module,exports){
|
|
module.exports = [ "[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?]","[?]","[?]" ];
|
|
|
|
},{}],65:[function(require,module,exports){
|
|
module.exports = [ " ",", ",". ","\"","[JIS]","\"","/","0","<","> ","<<",">> ","[","] ","{","} ","[(",")] ","@","X ","[","] ","[[","]] ","((",")) ","[[","]] ","~ ","``","''",",,","@","1","2","3","4","5","6","7","8","9","","","","","","","~","+","+","+","+","","@"," // ","+10+","+20+","+30+","[?]","[?]","[?]","","","[?]","a","a","i","i","u","u","e","e","o","o","ka","ga","ki","gi","ku","gu","ke","ge","ko","go","sa","za","si","zi","su","zu","se","ze","so","zo","ta","da","ti","di","tu","tu","du","te","de","to","do","na","ni","nu","ne","no","ha","ba","pa","hi","bi","pi","hu","bu","pu","he","be","pe","ho","bo","po","ma","mi","mu","me","mo","ya","ya","yu","yu","yo","yo","ra","ri","ru","re","ro","wa","wa","wi","we","wo","n","vu","[?]","[?]","[?]","[?]","","","","","\"","\"","[?]","[?]","a","a","i","i","u","u","e","e","o","o","ka","ga","ki","gi","ku","gu","ke","ge","ko","go","sa","za","si","zi","su","zu","se","ze","so","zo","ta","da","ti","di","tu","tu","du","te","de","to","do","na","ni","nu","ne","no","ha","ba","pa","hi","bi","pi","hu","bu","pu","he","be","pe","ho","bo","po","ma","mi","mu","me","mo","ya","ya","yu","yu","yo","yo","ra","ri","ru","re","ro","wa","wa","wi","we","wo","n","vu","ka","ke","va","vi","ve","vo","","","\"","\"" ];
|
|
|
|
},{}],66:[function(require,module,exports){
|
|
module.exports = [ "[?]","[?]","[?]","[?]","[?]","B","P","M","F","D","T","N","L","G","K","H","J","Q","X","ZH","CH","SH","R","Z","C","S","A","O","E","EH","AI","EI","AU","OU","AN","EN","ANG","ENG","ER","I","U","IU","V","NG","GN","[?]","[?]","[?]","[?]","g","gg","gs","n","nj","nh","d","dd","r","lg","lm","lb","ls","lt","lp","rh","m","b","bb","bs","s","ss","","j","jj","c","k","t","p","h","a","ae","ya","yae","eo","e","yeo","ye","o","wa","wae","oe","yo","u","weo","we","wi","yu","eu","yi","i","","nn","nd","ns","nZ","lgs","ld","lbs","lZ","lQ","mb","ms","mZ","mN","bg","","bsg","bst","bj","bt","bN","bbN","sg","sn","sd","sb","sj","Z","","N","Ns","NZ","pN","hh","Q","yo-ya","yo-yae","yo-i","yu-yeo","yu-ye","yu-i","U","U-i","[?]","","","","","","","","","","","","","","","","","BU","ZI","JI","GU","EE","ENN","OO","ONN","IR","ANN","INN","UNN","IM","NGG","AINN","AUNN","AM","OM","ONG","INNN","P","T","K","H","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]" ];
|
|
|
|
},{}],67:[function(require,module,exports){
|
|
module.exports = [ "(g)","(n)","(d)","(r)","(m)","(b)","(s)","()","(j)","(c)","(k)","(t)","(p)","(h)","(ga)","(na)","(da)","(ra)","(ma)","(ba)","(sa)","(a)","(ja)","(ca)","(ka)","(ta)","(pa)","(ha)","(ju)","[?]","[?]","[?]","(1) ","(2) ","(3) ","(4) ","(5) ","(6) ","(7) ","(8) ","(9) ","(10) ","(Yue) ","(Huo) ","(Shui) ","(Mu) ","(Jin) ","(Tu) ","(Ri) ","(Zhu) ","(You) ","(She) ","(Ming) ","(Te) ","(Cai) ","(Zhu) ","(Lao) ","(Dai) ","(Hu) ","(Xue) ","(Jian) ","(Qi) ","(Zi) ","(Xie) ","(Ji) ","(Xiu) ","<<",">>","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","(g)","(n)","(d)","(r)","(m)","(b)","(s)","()","(j)","(c)","(k)","(t)","(p)","(h)","(ga)","(na)","(da)","(ra)","(ma)","(ba)","(sa)","(a)","(ja)","(ca)","(ka)","(ta)","(pa)","(ha)","[?]","[?]","[?]","KIS ","(1) ","(2) ","(3) ","(4) ","(5) ","(6) ","(7) ","(8) ","(9) ","(10) ","(Yue) ","(Huo) ","(Shui) ","(Mu) ","(Jin) ","(Tu) ","(Ri) ","(Zhu) ","(You) ","(She) ","(Ming) ","(Te) ","(Cai) ","(Zhu) ","(Lao) ","(Mi) ","(Nan) ","(Nu) ","(Shi) ","(You) ","(Yin) ","(Zhu) ","(Xiang) ","(Xiu) ","(Xie) ","(Zheng) ","(Shang) ","(Zhong) ","(Xia) ","(Zuo) ","(You) ","(Yi) ","(Zong) ","(Xue) ","(Jian) ","(Qi) ","(Zi) ","(Xie) ","(Ye) ","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","1M","2M","3M","4M","5M","6M","7M","8M","9M","10M","11M","12M","[?]","[?]","[?]","[?]","a","i","u","u","o","ka","ki","ku","ke","ko","sa","si","su","se","so","ta","ti","tu","te","to","na","ni","nu","ne","no","ha","hi","hu","he","ho","ma","mi","mu","me","mo","ya","yu","yo","ra","ri","ru","re","ro","wa","wi","we","wo" ];
|
|
|
|
},{}],68:[function(require,module,exports){
|
|
module.exports = [ "apartment","alpha","ampere","are","inning","inch","won","escudo","acre","ounce","ohm","kai-ri","carat","calorie","gallon","gamma","giga","guinea","curie","guilder","kilo","kilogram","kilometer","kilowatt","gram","gram ton","cruzeiro","krone","case","koruna","co-op","cycle","centime","shilling","centi","cent","dozen","desi","dollar","ton","nano","knot","heights","percent","parts","barrel","piaster","picul","pico","building","farad","feet","bushel","franc","hectare","peso","pfennig","hertz","pence","page","beta","point","volt","hon","pound","hall","horn","micro","mile","mach","mark","mansion","micron","milli","millibar","mega","megaton","meter","yard","yard","yuan","liter","lira","rupee","ruble","rem","roentgen","watt","0h","1h","2h","3h","4h","5h","6h","7h","8h","9h","10h","11h","12h","13h","14h","15h","16h","17h","18h","19h","20h","21h","22h","23h","24h","HPA","da","AU","bar","oV","pc","[?]","[?]","[?]","[?]","Heisei","Syouwa","Taisyou","Meiji","Inc.","pA","nA","microamp","mA","kA","kB","MB","GB","cal","kcal","pF","nF","microFarad","microgram","mg","kg","Hz","kHz","MHz","GHz","THz","microliter","ml","dl","kl","fm","nm","micrometer","mm","cm","km","mm^2","cm^2","m^2","km^2","mm^4","cm^3","m^3","km^3","m/s","m/s^2","Pa","kPa","MPa","GPa","rad","rad/s","rad/s^2","ps","ns","microsecond","ms","pV","nV","microvolt","mV","kV","MV","pW","nW","microwatt","mW","kW","MW","kOhm","MOhm","a.m.","Bq","cc","cd","C/kg","Co.","dB","Gy","ha","HP","in","K.K.","KM","kt","lm","ln","log","lx","mb","mil","mol","pH","p.m.","PPM","PR","sr","Sv","Wb","[?]","[?]","1d","2d","3d","4d","5d","6d","7d","8d","9d","10d","11d","12d","13d","14d","15d","16d","17d","18d","19d","20d","21d","22d","23d","24d","25d","26d","27d","28d","29d","30d","31d" ];
|
|
|
|
},{}],69:[function(require,module,exports){
|
|
module.exports = [ "[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?] ","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]" ];
|
|
|
|
},{}],70:[function(require,module,exports){
|
|
module.exports = [ "Yi ","Ding ","Kao ","Qi ","Shang ","Xia ","[?] ","Mo ","Zhang ","San ","Shang ","Xia ","Ji ","Bu ","Yu ","Mian ","Gai ","Chou ","Chou ","Zhuan ","Qie ","Pi ","Shi ","Shi ","Qiu ","Bing ","Ye ","Cong ","Dong ","Si ","Cheng ","Diu ","Qiu ","Liang ","Diu ","You ","Liang ","Yan ","Bing ","Sang ","Gun ","Jiu ","Ge ","Ya ","Qiang ","Zhong ","Ji ","Jie ","Feng ","Guan ","Chuan ","Chan ","Lin ","Zhuo ","Zhu ","Ha ","Wan ","Dan ","Wei ","Zhu ","Jing ","Li ","Ju ","Pie ","Fu ","Yi ","Yi ","Nai ","Shime ","Jiu ","Jiu ","Zhe ","Yao ","Yi ","[?] ","Zhi ","Wu ","Zha ","Hu ","Fa ","Le ","Zhong ","Ping ","Pang ","Qiao ","Hu ","Guai ","Cheng ","Cheng ","Yi ","Yin ","[?] ","Mie ","Jiu ","Qi ","Ye ","Xi ","Xiang ","Gai ","Diu ","Hal ","[?] ","Shu ","Twul ","Shi ","Ji ","Nang ","Jia ","Kel ","Shi ","[?] ","Ol ","Mai ","Luan ","Cal ","Ru ","Xue ","Yan ","Fu ","Sha ","Na ","Gan ","Sol ","El ","Cwul ","[?] ","Gan ","Chi ","Gui ","Gan ","Luan ","Lin ","Yi ","Jue ","Liao ","Ma ","Yu ","Zheng ","Shi ","Shi ","Er ","Chu ","Yu ","Yu ","Yu ","Yun ","Hu ","Qi ","Wu ","Jing ","Si ","Sui ","Gen ","Gen ","Ya ","Xie ","Ya ","Qi ","Ya ","Ji ","Tou ","Wang ","Kang ","Ta ","Jiao ","Hai ","Yi ","Chan ","Heng ","Mu ","[?] ","Xiang ","Jing ","Ting ","Liang ","Xiang ","Jing ","Ye ","Qin ","Bo ","You ","Xie ","Dan ","Lian ","Duo ","Wei ","Ren ","Ren ","Ji ","La ","Wang ","Yi ","Shi ","Ren ","Le ","Ding ","Ze ","Jin ","Pu ","Chou ","Ba ","Zhang ","Jin ","Jie ","Bing ","Reng ","Cong ","Fo ","San ","Lun ","Sya ","Cang ","Zi ","Shi ","Ta ","Zhang ","Fu ","Xian ","Xian ","Tuo ","Hong ","Tong ","Ren ","Qian ","Gan ","Yi ","Di ","Dai ","Ling ","Yi ","Chao ","Chang ","Sa ","[?] ","Yi ","Mu ","Men ","Ren ","Jia ","Chao ","Yang ","Qian ","Zhong ","Pi ","Wan ","Wu ","Jian ","Jie ","Yao ","Feng ","Cang ","Ren ","Wang ","Fen ","Di ","Fang " ];
|
|
|
|
},{}],71:[function(require,module,exports){
|
|
module.exports = [ "Zhong ","Qi ","Pei ","Yu ","Diao ","Dun ","Wen ","Yi ","Xin ","Kang ","Yi ","Ji ","Ai ","Wu ","Ji ","Fu ","Fa ","Xiu ","Jin ","Bei ","Dan ","Fu ","Tang ","Zhong ","You ","Huo ","Hui ","Yu ","Cui ","Chuan ","San ","Wei ","Chuan ","Che ","Ya ","Xian ","Shang ","Chang ","Lun ","Cang ","Xun ","Xin ","Wei ","Zhu ","[?] ","Xuan ","Nu ","Bo ","Gu ","Ni ","Ni ","Xie ","Ban ","Xu ","Ling ","Zhou ","Shen ","Qu ","Si ","Beng ","Si ","Jia ","Pi ","Yi ","Si ","Ai ","Zheng ","Dian ","Han ","Mai ","Dan ","Zhu ","Bu ","Qu ","Bi ","Shao ","Ci ","Wei ","Di ","Zhu ","Zuo ","You ","Yang ","Ti ","Zhan ","He ","Bi ","Tuo ","She ","Yu ","Yi ","Fo ","Zuo ","Kou ","Ning ","Tong ","Ni ","Xuan ","Qu ","Yong ","Wa ","Qian ","[?] ","Ka ","[?] ","Pei ","Huai ","He ","Lao ","Xiang ","Ge ","Yang ","Bai ","Fa ","Ming ","Jia ","Er ","Bing ","Ji ","Hen ","Huo ","Gui ","Quan ","Tiao ","Jiao ","Ci ","Yi ","Shi ","Xing ","Shen ","Tuo ","Kan ","Zhi ","Gai ","Lai ","Yi ","Chi ","Kua ","Guang ","Li ","Yin ","Shi ","Mi ","Zhu ","Xu ","You ","An ","Lu ","Mou ","Er ","Lun ","Tong ","Cha ","Chi ","Xun ","Gong ","Zhou ","Yi ","Ru ","Jian ","Xia ","Jia ","Zai ","Lu ","Ko ","Jiao ","Zhen ","Ce ","Qiao ","Kuai ","Chai ","Ning ","Nong ","Jin ","Wu ","Hou ","Jiong ","Cheng ","Zhen ","Zuo ","Chou ","Qin ","Lu ","Ju ","Shu ","Ting ","Shen ","Tuo ","Bo ","Nan ","Hao ","Bian ","Tui ","Yu ","Xi ","Cu ","E ","Qiu ","Xu ","Kuang ","Ku ","Wu ","Jun ","Yi ","Fu ","Lang ","Zu ","Qiao ","Li ","Yong ","Hun ","Jing ","Xian ","San ","Pai ","Su ","Fu ","Xi ","Li ","Fu ","Ping ","Bao ","Yu ","Si ","Xia ","Xin ","Xiu ","Yu ","Ti ","Che ","Chou ","[?] ","Yan ","Lia ","Li ","Lai ","[?] ","Jian ","Xiu ","Fu ","He ","Ju ","Xiao ","Pai ","Jian ","Biao ","Chu ","Fei ","Feng ","Ya ","An ","Bei ","Yu ","Xin ","Bi ","Jian " ];
|
|
|
|
},{}],72:[function(require,module,exports){
|
|
module.exports = [ "Chang ","Chi ","Bing ","Zan ","Yao ","Cui ","Lia ","Wan ","Lai ","Cang ","Zong ","Ge ","Guan ","Bei ","Tian ","Shu ","Shu ","Men ","Dao ","Tan ","Jue ","Chui ","Xing ","Peng ","Tang ","Hou ","Yi ","Qi ","Ti ","Gan ","Jing ","Jie ","Sui ","Chang ","Jie ","Fang ","Zhi ","Kong ","Juan ","Zong ","Ju ","Qian ","Ni ","Lun ","Zhuo ","Wei ","Luo ","Song ","Leng ","Hun ","Dong ","Zi ","Ben ","Wu ","Ju ","Nai ","Cai ","Jian ","Zhai ","Ye ","Zhi ","Sha ","Qing ","[?] ","Ying ","Cheng ","Jian ","Yan ","Nuan ","Zhong ","Chun ","Jia ","Jie ","Wei ","Yu ","Bing ","Ruo ","Ti ","Wei ","Pian ","Yan ","Feng ","Tang ","Wo ","E ","Xie ","Che ","Sheng ","Kan ","Di ","Zuo ","Cha ","Ting ","Bei ","Ye ","Huang ","Yao ","Zhan ","Chou ","Yan ","You ","Jian ","Xu ","Zha ","Ci ","Fu ","Bi ","Zhi ","Zong ","Mian ","Ji ","Yi ","Xie ","Xun ","Si ","Duan ","Ce ","Zhen ","Ou ","Tou ","Tou ","Bei ","Za ","Lu ","Jie ","Wei ","Fen ","Chang ","Gui ","Sou ","Zhi ","Su ","Xia ","Fu ","Yuan ","Rong ","Li ","Ru ","Yun ","Gou ","Ma ","Bang ","Dian ","Tang ","Hao ","Jie ","Xi ","Shan ","Qian ","Jue ","Cang ","Chu ","San ","Bei ","Xiao ","Yong ","Yao ","Tan ","Suo ","Yang ","Fa ","Bing ","Jia ","Dai ","Zai ","Tang ","[?] ","Bin ","Chu ","Nuo ","Can ","Lei ","Cui ","Yong ","Zao ","Zong ","Peng ","Song ","Ao ","Chuan ","Yu ","Zhai ","Cou ","Shang ","Qiang ","Jing ","Chi ","Sha ","Han ","Zhang ","Qing ","Yan ","Di ","Xi ","Lu ","Bei ","Piao ","Jin ","Lian ","Lu ","Man ","Qian ","Xian ","Tan ","Ying ","Dong ","Zhuan ","Xiang ","Shan ","Qiao ","Jiong ","Tui ","Zun ","Pu ","Xi ","Lao ","Chang ","Guang ","Liao ","Qi ","Deng ","Chan ","Wei ","Ji ","Fan ","Hui ","Chuan ","Jian ","Dan ","Jiao ","Jiu ","Seng ","Fen ","Xian ","Jue ","E ","Jiao ","Jian ","Tong ","Lin ","Bo ","Gu ","[?] ","Su ","Xian ","Jiang ","Min ","Ye ","Jin ","Jia ","Qiao ","Pi ","Feng ","Zhou ","Ai ","Sai " ];
|
|
|
|
},{}],73:[function(require,module,exports){
|
|
module.exports = [ "Yi ","Jun ","Nong ","Chan ","Yi ","Dang ","Jing ","Xuan ","Kuai ","Jian ","Chu ","Dan ","Jiao ","Sha ","Zai ","[?] ","Bin ","An ","Ru ","Tai ","Chou ","Chai ","Lan ","Ni ","Jin ","Qian ","Meng ","Wu ","Ning ","Qiong ","Ni ","Chang ","Lie ","Lei ","Lu ","Kuang ","Bao ","Du ","Biao ","Zan ","Zhi ","Si ","You ","Hao ","Chen ","Chen ","Li ","Teng ","Wei ","Long ","Chu ","Chan ","Rang ","Shu ","Hui ","Li ","Luo ","Zan ","Nuo ","Tang ","Yan ","Lei ","Nang ","Er ","Wu ","Yun ","Zan ","Yuan ","Xiong ","Chong ","Zhao ","Xiong ","Xian ","Guang ","Dui ","Ke ","Dui ","Mian ","Tu ","Chang ","Er ","Dui ","Er ","Xin ","Tu ","Si ","Yan ","Yan ","Shi ","Shi ","Dang ","Qian ","Dou ","Fen ","Mao ","Shen ","Dou ","Bai ","Jing ","Li ","Huang ","Ru ","Wang ","Nei ","Quan ","Liang ","Yu ","Ba ","Gong ","Liu ","Xi ","[?] ","Lan ","Gong ","Tian ","Guan ","Xing ","Bing ","Qi ","Ju ","Dian ","Zi ","Ppwun ","Yang ","Jian ","Shou ","Ji ","Yi ","Ji ","Chan ","Jiong ","Mao ","Ran ","Nei ","Yuan ","Mao ","Gang ","Ran ","Ce ","Jiong ","Ce ","Zai ","Gua ","Jiong ","Mao ","Zhou ","Mou ","Gou ","Xu ","Mian ","Mi ","Rong ","Yin ","Xie ","Kan ","Jun ","Nong ","Yi ","Mi ","Shi ","Guan ","Meng ","Zhong ","Ju ","Yuan ","Ming ","Kou ","Lam ","Fu ","Xie ","Mi ","Bing ","Dong ","Tai ","Gang ","Feng ","Bing ","Hu ","Chong ","Jue ","Hu ","Kuang ","Ye ","Leng ","Pan ","Fu ","Min ","Dong ","Xian ","Lie ","Xia ","Jian ","Jing ","Shu ","Mei ","Tu ","Qi ","Gu ","Zhun ","Song ","Jing ","Liang ","Qing ","Diao ","Ling ","Dong ","Gan ","Jian ","Yin ","Cou ","Yi ","Li ","Cang ","Ming ","Zhuen ","Cui ","Si ","Duo ","Jin ","Lin ","Lin ","Ning ","Xi ","Du ","Ji ","Fan ","Fan ","Fan ","Feng ","Ju ","Chu ","Tako ","Feng ","Mok ","Ci ","Fu ","Feng ","Ping ","Feng ","Kai ","Huang ","Kai ","Gan ","Deng ","Ping ","Qu ","Xiong ","Kuai ","Tu ","Ao ","Chu ","Ji ","Dang ","Han ","Han ","Zao " ];
|
|
|
|
},{}],74:[function(require,module,exports){
|
|
module.exports = [ "Dao ","Diao ","Dao ","Ren ","Ren ","Chuang ","Fen ","Qie ","Yi ","Ji ","Kan ","Qian ","Cun ","Chu ","Wen ","Ji ","Dan ","Xing ","Hua ","Wan ","Jue ","Li ","Yue ","Lie ","Liu ","Ze ","Gang ","Chuang ","Fu ","Chu ","Qu ","Ju ","Shan ","Min ","Ling ","Zhong ","Pan ","Bie ","Jie ","Jie ","Bao ","Li ","Shan ","Bie ","Chan ","Jing ","Gua ","Gen ","Dao ","Chuang ","Kui ","Ku ","Duo ","Er ","Zhi ","Shua ","Quan ","Cha ","Ci ","Ke ","Jie ","Gui ","Ci ","Gui ","Kai ","Duo ","Ji ","Ti ","Jing ","Lou ","Gen ","Ze ","Yuan ","Cuo ","Xue ","Ke ","La ","Qian ","Cha ","Chuang ","Gua ","Jian ","Cuo ","Li ","Ti ","Fei ","Pou ","Chan ","Qi ","Chuang ","Zi ","Gang ","Wan ","Bo ","Ji ","Duo ","Qing ","Yan ","Zhuo ","Jian ","Ji ","Bo ","Yan ","Ju ","Huo ","Sheng ","Jian ","Duo ","Duan ","Wu ","Gua ","Fu ","Sheng ","Jian ","Ge ","Zha ","Kai ","Chuang ","Juan ","Chan ","Tuan ","Lu ","Li ","Fou ","Shan ","Piao ","Kou ","Jiao ","Gua ","Qiao ","Jue ","Hua ","Zha ","Zhuo ","Lian ","Ju ","Pi ","Liu ","Gui ","Jiao ","Gui ","Jian ","Jian ","Tang ","Huo ","Ji ","Jian ","Yi ","Jian ","Zhi ","Chan ","Cuan ","Mo ","Li ","Zhu ","Li ","Ya ","Quan ","Ban ","Gong ","Jia ","Wu ","Mai ","Lie ","Jin ","Keng ","Xie ","Zhi ","Dong ","Zhu ","Nu ","Jie ","Qu ","Shao ","Yi ","Zhu ","Miao ","Li ","Jing ","Lao ","Lao ","Juan ","Kou ","Yang ","Wa ","Xiao ","Mou ","Kuang ","Jie ","Lie ","He ","Shi ","Ke ","Jing ","Hao ","Bo ","Min ","Chi ","Lang ","Yong ","Yong ","Mian ","Ke ","Xun ","Juan ","Qing ","Lu ","Pou ","Meng ","Lai ","Le ","Kai ","Mian ","Dong ","Xu ","Xu ","Kan ","Wu ","Yi ","Xun ","Weng ","Sheng ","Lao ","Mu ","Lu ","Piao ","Shi ","Ji ","Qin ","Qiang ","Jiao ","Quan ","Yang ","Yi ","Jue ","Fan ","Juan ","Tong ","Ju ","Dan ","Xie ","Mai ","Xun ","Xun ","Lu ","Li ","Che ","Rang ","Quan ","Bao ","Shao ","Yun ","Jiu ","Bao ","Gou ","Wu " ];
|
|
|
|
},{}],75:[function(require,module,exports){
|
|
module.exports = [ "Yun ","Mwun ","Nay ","Gai ","Gai ","Bao ","Cong ","[?] ","Xiong ","Peng ","Ju ","Tao ","Ge ","Pu ","An ","Pao ","Fu ","Gong ","Da ","Jiu ","Qiong ","Bi ","Hua ","Bei ","Nao ","Chi ","Fang ","Jiu ","Yi ","Za ","Jiang ","Kang ","Jiang ","Kuang ","Hu ","Xia ","Qu ","Bian ","Gui ","Qie ","Zang ","Kuang ","Fei ","Hu ","Tou ","Gui ","Gui ","Hui ","Dan ","Gui ","Lian ","Lian ","Suan ","Du ","Jiu ","Qu ","Xi ","Pi ","Qu ","Yi ","Qia ","Yan ","Bian ","Ni ","Qu ","Shi ","Xin ","Qian ","Nian ","Sa ","Zu ","Sheng ","Wu ","Hui ","Ban ","Shi ","Xi ","Wan ","Hua ","Xie ","Wan ","Bei ","Zu ","Zhuo ","Xie ","Dan ","Mai ","Nan ","Dan ","Ji ","Bo ","Shuai ","Bu ","Kuang ","Bian ","Bu ","Zhan ","Qia ","Lu ","You ","Lu ","Xi ","Gua ","Wo ","Xie ","Jie ","Jie ","Wei ","Ang ","Qiong ","Zhi ","Mao ","Yin ","Wei ","Shao ","Ji ","Que ","Luan ","Shi ","Juan ","Xie ","Xu ","Jin ","Que ","Wu ","Ji ","E ","Qing ","Xi ","[?] ","Han ","Zhan ","E ","Ting ","Li ","Zhe ","Han ","Li ","Ya ","Ya ","Yan ","She ","Zhi ","Zha ","Pang ","[?] ","He ","Ya ","Zhi ","Ce ","Pang ","Ti ","Li ","She ","Hou ","Ting ","Zui ","Cuo ","Fei ","Yuan ","Ce ","Yuan ","Xiang ","Yan ","Li ","Jue ","Sha ","Dian ","Chu ","Jiu ","Qin ","Ao ","Gui ","Yan ","Si ","Li ","Chang ","Lan ","Li ","Yan ","Yan ","Yuan ","Si ","Gong ","Lin ","Qiu ","Qu ","Qu ","Uk ","Lei ","Du ","Xian ","Zhuan ","San ","Can ","Can ","Can ","Can ","Ai ","Dai ","You ","Cha ","Ji ","You ","Shuang ","Fan ","Shou ","Guai ","Ba ","Fa ","Ruo ","Shi ","Shu ","Zhuo ","Qu ","Shou ","Bian ","Xu ","Jia ","Pan ","Sou ","Gao ","Wei ","Sou ","Die ","Rui ","Cong ","Kou ","Gu ","Ju ","Ling ","Gua ","Tao ","Kou ","Zhi ","Jiao ","Zhao ","Ba ","Ding ","Ke ","Tai ","Chi ","Shi ","You ","Qiu ","Po ","Xie ","Hao ","Si ","Tan ","Chi ","Le ","Diao ","Ji ","[?] ","Hong " ];
|
|
|
|
},{}],76:[function(require,module,exports){
|
|
module.exports = [ "Mie ","Xu ","Mang ","Chi ","Ge ","Xuan ","Yao ","Zi ","He ","Ji ","Diao ","Cun ","Tong ","Ming ","Hou ","Li ","Tu ","Xiang ","Zha ","Xia ","Ye ","Lu ","A ","Ma ","Ou ","Xue ","Yi ","Jun ","Chou ","Lin ","Tun ","Yin ","Fei ","Bi ","Qin ","Qin ","Jie ","Bu ","Fou ","Ba ","Dun ","Fen ","E ","Han ","Ting ","Hang ","Shun ","Qi ","Hong ","Zhi ","Shen ","Wu ","Wu ","Chao ","Ne ","Xue ","Xi ","Chui ","Dou ","Wen ","Hou ","Ou ","Wu ","Gao ","Ya ","Jun ","Lu ","E ","Ge ","Mei ","Ai ","Qi ","Cheng ","Wu ","Gao ","Fu ","Jiao ","Hong ","Chi ","Sheng ","Ne ","Tun ","Fu ","Yi ","Dai ","Ou ","Li ","Bai ","Yuan ","Kuai ","[?] ","Qiang ","Wu ","E ","Shi ","Quan ","Pen ","Wen ","Ni ","M ","Ling ","Ran ","You ","Di ","Zhou ","Shi ","Zhou ","Tie ","Xi ","Yi ","Qi ","Ping ","Zi ","Gu ","Zi ","Wei ","Xu ","He ","Nao ","Xia ","Pei ","Yi ","Xiao ","Shen ","Hu ","Ming ","Da ","Qu ","Ju ","Gem ","Za ","Tuo ","Duo ","Pou ","Pao ","Bi ","Fu ","Yang ","He ","Zha ","He ","Hai ","Jiu ","Yong ","Fu ","Que ","Zhou ","Wa ","Ka ","Gu ","Ka ","Zuo ","Bu ","Long ","Dong ","Ning ","Tha ","Si ","Xian ","Huo ","Qi ","Er ","E ","Guang ","Zha ","Xi ","Yi ","Lie ","Zi ","Mie ","Mi ","Zhi ","Yao ","Ji ","Zhou ","Ge ","Shuai ","Zan ","Xiao ","Ke ","Hui ","Kua ","Huai ","Tao ","Xian ","E ","Xuan ","Xiu ","Wai ","Yan ","Lao ","Yi ","Ai ","Pin ","Shen ","Tong ","Hong ","Xiong ","Chi ","Wa ","Ha ","Zai ","Yu ","Di ","Pai ","Xiang ","Ai ","Hen ","Kuang ","Ya ","Da ","Xiao ","Bi ","Yue ","[?] ","Hua ","Sasou ","Kuai ","Duo ","[?] ","Ji ","Nong ","Mou ","Yo ","Hao ","Yuan ","Long ","Pou ","Mang ","Ge ","E ","Chi ","Shao ","Li ","Na ","Zu ","He ","Ku ","Xiao ","Xian ","Lao ","Bo ","Zhe ","Zha ","Liang ","Ba ","Mie ","Le ","Sui ","Fou ","Bu ","Han ","Heng ","Geng ","Shuo ","Ge " ];
|
|
|
|
},{}],77:[function(require,module,exports){
|
|
module.exports = [ "You ","Yan ","Gu ","Gu ","Bai ","Han ","Suo ","Chun ","Yi ","Ai ","Jia ","Tu ","Xian ","Huan ","Li ","Xi ","Tang ","Zuo ","Qiu ","Che ","Wu ","Zao ","Ya ","Dou ","Qi ","Di ","Qin ","Ma ","Mal ","Hong ","Dou ","Kes ","Lao ","Liang ","Suo ","Zao ","Huan ","Lang ","Sha ","Ji ","Zuo ","Wo ","Feng ","Yin ","Hu ","Qi ","Shou ","Wei ","Shua ","Chang ","Er ","Li ","Qiang ","An ","Jie ","Yo ","Nian ","Yu ","Tian ","Lai ","Sha ","Xi ","Tuo ","Hu ","Ai ","Zhou ","Nou ","Ken ","Zhuo ","Zhuo ","Shang ","Di ","Heng ","Lan ","A ","Xiao ","Xiang ","Tun ","Wu ","Wen ","Cui ","Sha ","Hu ","Qi ","Qi ","Tao ","Dan ","Dan ","Ye ","Zi ","Bi ","Cui ","Chuo ","He ","Ya ","Qi ","Zhe ","Pei ","Liang ","Xian ","Pi ","Sha ","La ","Ze ","Qing ","Gua ","Pa ","Zhe ","Se ","Zhuan ","Nie ","Guo ","Luo ","Yan ","Di ","Quan ","Tan ","Bo ","Ding ","Lang ","Xiao ","[?] ","Tang ","Chi ","Ti ","An ","Jiu ","Dan ","Ke ","Yong ","Wei ","Nan ","Shan ","Yu ","Zhe ","La ","Jie ","Hou ","Han ","Die ","Zhou ","Chai ","Wai ","Re ","Yu ","Yin ","Zan ","Yao ","Wo ","Mian ","Hu ","Yun ","Chuan ","Hui ","Huan ","Huan ","Xi ","He ","Ji ","Kui ","Zhong ","Wei ","Sha ","Xu ","Huang ","Du ","Nie ","Xuan ","Liang ","Yu ","Sang ","Chi ","Qiao ","Yan ","Dan ","Pen ","Can ","Li ","Yo ","Zha ","Wei ","Miao ","Ying ","Pen ","Phos ","Kui ","Xi ","Yu ","Jie ","Lou ","Ku ","Sao ","Huo ","Ti ","Yao ","He ","A ","Xiu ","Qiang ","Se ","Yong ","Su ","Hong ","Xie ","Yi ","Suo ","Ma ","Cha ","Hai ","Ke ","Ta ","Sang ","Tian ","Ru ","Sou ","Wa ","Ji ","Pang ","Wu ","Xian ","Shi ","Ge ","Zi ","Jie ","Luo ","Weng ","Wa ","Si ","Chi ","Hao ","Suo ","Jia ","Hai ","Suo ","Qin ","Nie ","He ","Cis ","Sai ","Ng ","Ge ","Na ","Dia ","Ai ","[?] ","Tong ","Bi ","Ao ","Ao ","Lian ","Cui ","Zhe ","Mo ","Sou ","Sou ","Tan " ];
|
|
|
|
},{}],78:[function(require,module,exports){
|
|
module.exports = [ "Di ","Qi ","Jiao ","Chong ","Jiao ","Kai ","Tan ","San ","Cao ","Jia ","Ai ","Xiao ","Piao ","Lou ","Ga ","Gu ","Xiao ","Hu ","Hui ","Guo ","Ou ","Xian ","Ze ","Chang ","Xu ","Po ","De ","Ma ","Ma ","Hu ","Lei ","Du ","Ga ","Tang ","Ye ","Beng ","Ying ","Saai ","Jiao ","Mi ","Xiao ","Hua ","Mai ","Ran ","Zuo ","Peng ","Lao ","Xiao ","Ji ","Zhu ","Chao ","Kui ","Zui ","Xiao ","Si ","Hao ","Fu ","Liao ","Qiao ","Xi ","Xiu ","Tan ","Tan ","Mo ","Xun ","E ","Zun ","Fan ","Chi ","Hui ","Zan ","Chuang ","Cu ","Dan ","Yu ","Tun ","Cheng ","Jiao ","Ye ","Xi ","Qi ","Hao ","Lian ","Xu ","Deng ","Hui ","Yin ","Pu ","Jue ","Qin ","Xun ","Nie ","Lu ","Si ","Yan ","Ying ","Da ","Dan ","Yu ","Zhou ","Jin ","Nong ","Yue ","Hui ","Qi ","E ","Zao ","Yi ","Shi ","Jiao ","Yuan ","Ai ","Yong ","Jue ","Kuai ","Yu ","Pen ","Dao ","Ge ","Xin ","Dun ","Dang ","Sin ","Sai ","Pi ","Pi ","Yin ","Zui ","Ning ","Di ","Lan ","Ta ","Huo ","Ru ","Hao ","Xia ","Ya ","Duo ","Xi ","Chou ","Ji ","Jin ","Hao ","Ti ","Chang ","[?] ","[?] ","Ca ","Ti ","Lu ","Hui ","Bo ","You ","Nie ","Yin ","Hu ","Mo ","Huang ","Zhe ","Li ","Liu ","Haai ","Nang ","Xiao ","Mo ","Yan ","Li ","Lu ","Long ","Fu ","Dan ","Chen ","Pin ","Pi ","Xiang ","Huo ","Mo ","Xi ","Duo ","Ku ","Yan ","Chan ","Ying ","Rang ","Dian ","La ","Ta ","Xiao ","Jiao ","Chuo ","Huan ","Huo ","Zhuan ","Nie ","Xiao ","Ca ","Li ","Chan ","Chai ","Li ","Yi ","Luo ","Nang ","Zan ","Su ","Xi ","So ","Jian ","Za ","Zhu ","Lan ","Nie ","Nang ","[?] ","[?] ","Wei ","Hui ","Yin ","Qiu ","Si ","Nin ","Jian ","Hui ","Xin ","Yin ","Nan ","Tuan ","Tuan ","Dun ","Kang ","Yuan ","Jiong ","Pian ","Yun ","Cong ","Hu ","Hui ","Yuan ","You ","Guo ","Kun ","Cong ","Wei ","Tu ","Wei ","Lun ","Guo ","Qun ","Ri ","Ling ","Gu ","Guo ","Tai ","Guo ","Tu ","You " ];
|
|
|
|
},{}],79:[function(require,module,exports){
|
|
module.exports = [ "Guo ","Yin ","Hun ","Pu ","Yu ","Han ","Yuan ","Lun ","Quan ","Yu ","Qing ","Guo ","Chuan ","Wei ","Yuan ","Quan ","Ku ","Fu ","Yuan ","Yuan ","E ","Tu ","Tu ","Tu ","Tuan ","Lue ","Hui ","Yi ","Yuan ","Luan ","Luan ","Tu ","Ya ","Tu ","Ting ","Sheng ","Pu ","Lu ","Iri ","Ya ","Zai ","Wei ","Ge ","Yu ","Wu ","Gui ","Pi ","Yi ","Di ","Qian ","Qian ","Zhen ","Zhuo ","Dang ","Qia ","Akutsu ","Yama ","Kuang ","Chang ","Qi ","Nie ","Mo ","Ji ","Jia ","Zhi ","Zhi ","Ban ","Xun ","Tou ","Qin ","Fen ","Jun ","Keng ","Tun ","Fang ","Fen ","Ben ","Tan ","Kan ","Pi ","Zuo ","Keng ","Bi ","Xing ","Di ","Jing ","Ji ","Kuai ","Di ","Jing ","Jian ","Tan ","Li ","Ba ","Wu ","Fen ","Zhui ","Po ","Pan ","Tang ","Kun ","Qu ","Tan ","Zhi ","Tuo ","Gan ","Ping ","Dian ","Gua ","Ni ","Tai ","Pi ","Jiong ","Yang ","Fo ","Ao ","Liu ","Qiu ","Mu ","Ke ","Gou ","Xue ","Ba ","Chi ","Che ","Ling ","Zhu ","Fu ","Hu ","Zhi ","Chui ","La ","Long ","Long ","Lu ","Ao ","Tay ","Pao ","[?] ","Xing ","Dong ","Ji ","Ke ","Lu ","Ci ","Chi ","Lei ","Gai ","Yin ","Hou ","Dui ","Zhao ","Fu ","Guang ","Yao ","Duo ","Duo ","Gui ","Cha ","Yang ","Yin ","Fa ","Gou ","Yuan ","Die ","Xie ","Ken ","Jiong ","Shou ","E ","Ha ","Dian ","Hong ","Wu ","Kua ","[?] ","Tao ","Dang ","Kai ","Gake ","Nao ","An ","Xing ","Xian ","Huan ","Bang ","Pei ","Ba ","Yi ","Yin ","Han ","Xu ","Chui ","Cen ","Geng ","Ai ","Peng ","Fang ","Que ","Yong ","Xun ","Jia ","Di ","Mai ","Lang ","Xuan ","Cheng ","Yan ","Jin ","Zhe ","Lei ","Lie ","Bu ","Cheng ","Gomi ","Bu ","Shi ","Xun ","Guo ","Jiong ","Ye ","Nian ","Di ","Yu ","Bu ","Ya ","Juan ","Sui ","Pi ","Cheng ","Wan ","Ju ","Lun ","Zheng ","Kong ","Chong ","Dong ","Dai ","Tan ","An ","Cai ","Shu ","Beng ","Kan ","Zhi ","Duo ","Yi ","Zhi ","Yi ","Pei ","Ji ","Zhun ","Qi ","Sao ","Ju ","Ni " ];
|
|
|
|
},{}],80:[function(require,module,exports){
|
|
module.exports = [ "Ku ","Ke ","Tang ","Kun ","Ni ","Jian ","Dui ","Jin ","Gang ","Yu ","E ","Peng ","Gu ","Tu ","Leng ","[?] ","Ya ","Qian ","[?] ","An ","[?] ","Duo ","Nao ","Tu ","Cheng ","Yin ","Hun ","Bi ","Lian ","Guo ","Die ","Zhuan ","Hou ","Bao ","Bao ","Yu ","Di ","Mao ","Jie ","Ruan ","E ","Geng ","Kan ","Zong ","Yu ","Huang ","E ","Yao ","Yan ","Bao ","Ji ","Mei ","Chang ","Du ","Tuo ","Yin ","Feng ","Zhong ","Jie ","Zhen ","Feng ","Gang ","Chuan ","Jian ","Pyeng ","Toride ","Xiang ","Huang ","Leng ","Duan ","[?] ","Xuan ","Ji ","Ji ","Kuai ","Ying ","Ta ","Cheng ","Yong ","Kai ","Su ","Su ","Shi ","Mi ","Ta ","Weng ","Cheng ","Tu ","Tang ","Que ","Zhong ","Li ","Peng ","Bang ","Sai ","Zang ","Dui ","Tian ","Wu ","Cheng ","Xun ","Ge ","Zhen ","Ai ","Gong ","Yan ","Kan ","Tian ","Yuan ","Wen ","Xie ","Liu ","Ama ","Lang ","Chang ","Peng ","Beng ","Chen ","Cu ","Lu ","Ou ","Qian ","Mei ","Mo ","Zhuan ","Shuang ","Shu ","Lou ","Chi ","Man ","Biao ","Jing ","Qi ","Shu ","Di ","Zhang ","Kan ","Yong ","Dian ","Chen ","Zhi ","Xi ","Guo ","Qiang ","Jin ","Di ","Shang ","Mu ","Cui ","Yan ","Ta ","Zeng ","Qi ","Qiang ","Liang ","[?] ","Zhui ","Qiao ","Zeng ","Xu ","Shan ","Shan ","Ba ","Pu ","Kuai ","Dong ","Fan ","Que ","Mo ","Dun ","Dun ","Dun ","Di ","Sheng ","Duo ","Duo ","Tan ","Deng ","Wu ","Fen ","Huang ","Tan ","Da ","Ye ","Sho ","Mama ","Yu ","Qiang ","Ji ","Qiao ","Ken ","Yi ","Pi ","Bi ","Dian ","Jiang ","Ye ","Yong ","Bo ","Tan ","Lan ","Ju ","Huai ","Dang ","Rang ","Qian ","Xun ","Lan ","Xi ","He ","Ai ","Ya ","Dao ","Hao ","Ruan ","Mama ","Lei ","Kuang ","Lu ","Yan ","Tan ","Wei ","Huai ","Long ","Long ","Rui ","Li ","Lin ","Rang ","Ten ","Xun ","Yan ","Lei ","Ba ","[?] ","Shi ","Ren ","[?] ","Zhuang ","Zhuang ","Sheng ","Yi ","Mai ","Ke ","Zhu ","Zhuang ","Hu ","Hu ","Kun ","Yi ","Hu ","Xu ","Kun ","Shou ","Mang ","Zun " ];
|
|
|
|
},{}],81:[function(require,module,exports){
|
|
module.exports = [ "Shou ","Yi ","Zhi ","Gu ","Chu ","Jiang ","Feng ","Bei ","Cay ","Bian ","Sui ","Qun ","Ling ","Fu ","Zuo ","Xia ","Xiong ","[?] ","Nao ","Xia ","Kui ","Xi ","Wai ","Yuan ","Mao ","Su ","Duo ","Duo ","Ye ","Qing ","Uys ","Gou ","Gou ","Qi ","Meng ","Meng ","Yin ","Huo ","Chen ","Da ","Ze ","Tian ","Tai ","Fu ","Guai ","Yao ","Yang ","Hang ","Gao ","Shi ","Ben ","Tai ","Tou ","Yan ","Bi ","Yi ","Kua ","Jia ","Duo ","Kwu ","Kuang ","Yun ","Jia ","Pa ","En ","Lian ","Huan ","Di ","Yan ","Pao ","Quan ","Qi ","Nai ","Feng ","Xie ","Fen ","Dian ","[?] ","Kui ","Zou ","Huan ","Qi ","Kai ","Zha ","Ben ","Yi ","Jiang ","Tao ","Zang ","Ben ","Xi ","Xiang ","Fei ","Diao ","Xun ","Keng ","Dian ","Ao ","She ","Weng ","Pan ","Ao ","Wu ","Ao ","Jiang ","Lian ","Duo ","Yun ","Jiang ","Shi ","Fen ","Huo ","Bi ","Lian ","Duo ","Nu ","Nu ","Ding ","Nai ","Qian ","Jian ","Ta ","Jiu ","Nan ","Cha ","Hao ","Xian ","Fan ","Ji ","Shuo ","Ru ","Fei ","Wang ","Hong ","Zhuang ","Fu ","Ma ","Dan ","Ren ","Fu ","Jing ","Yan ","Xie ","Wen ","Zhong ","Pa ","Du ","Ji ","Keng ","Zhong ","Yao ","Jin ","Yun ","Miao ","Pei ","Shi ","Yue ","Zhuang ","Niu ","Yan ","Na ","Xin ","Fen ","Bi ","Yu ","Tuo ","Feng ","Yuan ","Fang ","Wu ","Yu ","Gui ","Du ","Ba ","Ni ","Zhou ","Zhuo ","Zhao ","Da ","Nai ","Yuan ","Tou ","Xuan ","Zhi ","E ","Mei ","Mo ","Qi ","Bi ","Shen ","Qie ","E ","He ","Xu ","Fa ","Zheng ","Min ","Ban ","Mu ","Fu ","Ling ","Zi ","Zi ","Shi ","Ran ","Shan ","Yang ","Man ","Jie ","Gu ","Si ","Xing ","Wei ","Zi ","Ju ","Shan ","Pin ","Ren ","Yao ","Tong ","Jiang ","Shu ","Ji ","Gai ","Shang ","Kuo ","Juan ","Jiao ","Gou ","Mu ","Jian ","Jian ","Yi ","Nian ","Zhi ","Ji ","Ji ","Xian ","Heng ","Guang ","Jun ","Kua ","Yan ","Ming ","Lie ","Pei ","Yan ","You ","Yan ","Cha ","Shen ","Yin ","Chi ","Gui ","Quan ","Zi " ];
|
|
|
|
},{}],82:[function(require,module,exports){
|
|
module.exports = [ "Song ","Wei ","Hong ","Wa ","Lou ","Ya ","Rao ","Jiao ","Luan ","Ping ","Xian ","Shao ","Li ","Cheng ","Xiao ","Mang ","Fu ","Suo ","Wu ","Wei ","Ke ","Lai ","Chuo ","Ding ","Niang ","Xing ","Nan ","Yu ","Nuo ","Pei ","Nei ","Juan ","Shen ","Zhi ","Han ","Di ","Zhuang ","E ","Pin ","Tui ","Han ","Mian ","Wu ","Yan ","Wu ","Xi ","Yan ","Yu ","Si ","Yu ","Wa ","[?] ","Xian ","Ju ","Qu ","Shui ","Qi ","Xian ","Zhui ","Dong ","Chang ","Lu ","Ai ","E ","E ","Lou ","Mian ","Cong ","Pou ","Ju ","Po ","Cai ","Ding ","Wan ","Biao ","Xiao ","Shu ","Qi ","Hui ","Fu ","E ","Wo ","Tan ","Fei ","Wei ","Jie ","Tian ","Ni ","Quan ","Jing ","Hun ","Jing ","Qian ","Dian ","Xing ","Hu ","Wa ","Lai ","Bi ","Yin ","Chou ","Chuo ","Fu ","Jing ","Lun ","Yan ","Lan ","Kun ","Yin ","Ya ","Ju ","Li ","Dian ","Xian ","Hwa ","Hua ","Ying ","Chan ","Shen ","Ting ","Dang ","Yao ","Wu ","Nan ","Ruo ","Jia ","Tou ","Xu ","Yu ","Wei ","Ti ","Rou ","Mei ","Dan ","Ruan ","Qin ","Hui ","Wu ","Qian ","Chun ","Mao ","Fu ","Jie ","Duan ","Xi ","Zhong ","Mei ","Huang ","Mian ","An ","Ying ","Xuan ","Jie ","Wei ","Mei ","Yuan ","Zhen ","Qiu ","Ti ","Xie ","Tuo ","Lian ","Mao ","Ran ","Si ","Pian ","Wei ","Wa ","Jiu ","Hu ","Ao ","[?] ","Bou ","Xu ","Tou ","Gui ","Zou ","Yao ","Pi ","Xi ","Yuan ","Ying ","Rong ","Ru ","Chi ","Liu ","Mei ","Pan ","Ao ","Ma ","Gou ","Kui ","Qin ","Jia ","Sao ","Zhen ","Yuan ","Cha ","Yong ","Ming ","Ying ","Ji ","Su ","Niao ","Xian ","Tao ","Pang ","Lang ","Nao ","Bao ","Ai ","Pi ","Pin ","Yi ","Piao ","Yu ","Lei ","Xuan ","Man ","Yi ","Zhang ","Kang ","Yong ","Ni ","Li ","Di ","Gui ","Yan ","Jin ","Zhuan ","Chang ","Ce ","Han ","Nen ","Lao ","Mo ","Zhe ","Hu ","Hu ","Ao ","Nen ","Qiang ","Ma ","Pie ","Gu ","Wu ","Jiao ","Tuo ","Zhan ","Mao ","Xian ","Xian ","Mo ","Liao ","Lian ","Hua " ];
|
|
|
|
},{}],83:[function(require,module,exports){
|
|
module.exports = [ "Gui ","Deng ","Zhi ","Xu ","Yi ","Hua ","Xi ","Hui ","Rao ","Xi ","Yan ","Chan ","Jiao ","Mei ","Fan ","Fan ","Xian ","Yi ","Wei ","Jiao ","Fu ","Shi ","Bi ","Shan ","Sui ","Qiang ","Lian ","Huan ","Xin ","Niao ","Dong ","Yi ","Can ","Ai ","Niang ","Neng ","Ma ","Tiao ","Chou ","Jin ","Ci ","Yu ","Pin ","Yong ","Xu ","Nai ","Yan ","Tai ","Ying ","Can ","Niao ","Wo ","Ying ","Mian ","Kaka ","Ma ","Shen ","Xing ","Ni ","Du ","Liu ","Yuan ","Lan ","Yan ","Shuang ","Ling ","Jiao ","Niang ","Lan ","Xian ","Ying ","Shuang ","Shuai ","Quan ","Mi ","Li ","Luan ","Yan ","Zhu ","Lan ","Zi ","Jie ","Jue ","Jue ","Kong ","Yun ","Zi ","Zi ","Cun ","Sun ","Fu ","Bei ","Zi ","Xiao ","Xin ","Meng ","Si ","Tai ","Bao ","Ji ","Gu ","Nu ","Xue ","[?] ","Zhuan ","Hai ","Luan ","Sun ","Huai ","Mie ","Cong ","Qian ","Shu ","Chan ","Ya ","Zi ","Ni ","Fu ","Zi ","Li ","Xue ","Bo ","Ru ","Lai ","Nie ","Nie ","Ying ","Luan ","Mian ","Zhu ","Rong ","Ta ","Gui ","Zhai ","Qiong ","Yu ","Shou ","An ","Tu ","Song ","Wan ","Rou ","Yao ","Hong ","Yi ","Jing ","Zhun ","Mi ","Zhu ","Dang ","Hong ","Zong ","Guan ","Zhou ","Ding ","Wan ","Yi ","Bao ","Shi ","Shi ","Chong ","Shen ","Ke ","Xuan ","Shi ","You ","Huan ","Yi ","Tiao ","Shi ","Xian ","Gong ","Cheng ","Qun ","Gong ","Xiao ","Zai ","Zha ","Bao ","Hai ","Yan ","Xiao ","Jia ","Shen ","Chen ","Rong ","Huang ","Mi ","Kou ","Kuan ","Bin ","Su ","Cai ","Zan ","Ji ","Yuan ","Ji ","Yin ","Mi ","Kou ","Qing ","Que ","Zhen ","Jian ","Fu ","Ning ","Bing ","Huan ","Mei ","Qin ","Han ","Yu ","Shi ","Ning ","Qin ","Ning ","Zhi ","Yu ","Bao ","Kuan ","Ning ","Qin ","Mo ","Cha ","Ju ","Gua ","Qin ","Hu ","Wu ","Liao ","Shi ","Zhu ","Zhai ","Shen ","Wei ","Xie ","Kuan ","Hui ","Liao ","Jun ","Huan ","Yi ","Yi ","Bao ","Qin ","Chong ","Bao ","Feng ","Cun ","Dui ","Si ","Xun ","Dao ","Lu ","Dui ","Shou " ];
|
|
|
|
},{}],84:[function(require,module,exports){
|
|
module.exports = [ "Po ","Feng ","Zhuan ","Fu ","She ","Ke ","Jiang ","Jiang ","Zhuan ","Wei ","Zun ","Xun ","Shu ","Dui ","Dao ","Xiao ","Ji ","Shao ","Er ","Er ","Er ","Ga ","Jian ","Shu ","Chen ","Shang ","Shang ","Mo ","Ga ","Chang ","Liao ","Xian ","Xian ","[?] ","Wang ","Wang ","You ","Liao ","Liao ","Yao ","Mang ","Wang ","Wang ","Wang ","Ga ","Yao ","Duo ","Kui ","Zhong ","Jiu ","Gan ","Gu ","Gan ","Tui ","Gan ","Gan ","Shi ","Yin ","Chi ","Kao ","Ni ","Jin ","Wei ","Niao ","Ju ","Pi ","Ceng ","Xi ","Bi ","Ju ","Jie ","Tian ","Qu ","Ti ","Jie ","Wu ","Diao ","Shi ","Shi ","Ping ","Ji ","Xie ","Chen ","Xi ","Ni ","Zhan ","Xi ","[?] ","Man ","E ","Lou ","Ping ","Ti ","Fei ","Shu ","Xie ","Tu ","Lu ","Lu ","Xi ","Ceng ","Lu ","Ju ","Xie ","Ju ","Jue ","Liao ","Jue ","Shu ","Xi ","Che ","Tun ","Ni ","Shan ","[?] ","Xian ","Li ","Xue ","Nata ","[?] ","Long ","Yi ","Qi ","Ren ","Wu ","Han ","Shen ","Yu ","Chu ","Sui ","Qi ","[?] ","Yue ","Ban ","Yao ","Ang ","Ya ","Wu ","Jie ","E ","Ji ","Qian ","Fen ","Yuan ","Qi ","Cen ","Qian ","Qi ","Cha ","Jie ","Qu ","Gang ","Xian ","Ao ","Lan ","Dao ","Ba ","Zuo ","Zuo ","Yang ","Ju ","Gang ","Ke ","Gou ","Xue ","Bei ","Li ","Tiao ","Ju ","Yan ","Fu ","Xiu ","Jia ","Ling ","Tuo ","Pei ","You ","Dai ","Kuang ","Yue ","Qu ","Hu ","Po ","Min ","An ","Tiao ","Ling ","Chi ","Yuri ","Dong ","Cem ","Kui ","Xiu ","Mao ","Tong ","Xue ","Yi ","Kura ","He ","Ke ","Luo ","E ","Fu ","Xun ","Die ","Lu ","An ","Er ","Gai ","Quan ","Tong ","Yi ","Mu ","Shi ","An ","Wei ","Hu ","Zhi ","Mi ","Li ","Ji ","Tong ","Wei ","You ","Sang ","Xia ","Li ","Yao ","Jiao ","Zheng ","Luan ","Jiao ","E ","E ","Yu ","Ye ","Bu ","Qiao ","Qun ","Feng ","Feng ","Nao ","Li ","You ","Xian ","Hong ","Dao ","Shen ","Cheng ","Tu ","Geng ","Jun ","Hao ","Xia ","Yin ","Yu " ];
|
|
|
|
},{}],85:[function(require,module,exports){
|
|
module.exports = [ "Lang ","Kan ","Lao ","Lai ","Xian ","Que ","Kong ","Chong ","Chong ","Ta ","Lin ","Hua ","Ju ","Lai ","Qi ","Min ","Kun ","Kun ","Zu ","Gu ","Cui ","Ya ","Ya ","Gang ","Lun ","Lun ","Leng ","Jue ","Duo ","Zheng ","Guo ","Yin ","Dong ","Han ","Zheng ","Wei ","Yao ","Pi ","Yan ","Song ","Jie ","Beng ","Zu ","Jue ","Dong ","Zhan ","Gu ","Yin ","[?] ","Ze ","Huang ","Yu ","Wei ","Yang ","Feng ","Qiu ","Dun ","Ti ","Yi ","Zhi ","Shi ","Zai ","Yao ","E ","Zhu ","Kan ","Lu ","Yan ","Mei ","Gan ","Ji ","Ji ","Huan ","Ting ","Sheng ","Mei ","Qian ","Wu ","Yu ","Zong ","Lan ","Jue ","Yan ","Yan ","Wei ","Zong ","Cha ","Sui ","Rong ","Yamashina ","Qin ","Yu ","Kewashii ","Lou ","Tu ","Dui ","Xi ","Weng ","Cang ","Dang ","Hong ","Jie ","Ai ","Liu ","Wu ","Song ","Qiao ","Zi ","Wei ","Beng ","Dian ","Cuo ","Qian ","Yong ","Nie ","Cuo ","Ji ","[?] ","Tao ","Song ","Zong ","Jiang ","Liao ","Kang ","Chan ","Die ","Cen ","Ding ","Tu ","Lou ","Zhang ","Zhan ","Zhan ","Ao ","Cao ","Qu ","Qiang ","Zui ","Zui ","Dao ","Dao ","Xi ","Yu ","Bo ","Long ","Xiang ","Ceng ","Bo ","Qin ","Jiao ","Yan ","Lao ","Zhan ","Lin ","Liao ","Liao ","Jin ","Deng ","Duo ","Zun ","Jiao ","Gui ","Yao ","Qiao ","Yao ","Jue ","Zhan ","Yi ","Xue ","Nao ","Ye ","Ye ","Yi ","E ","Xian ","Ji ","Xie ","Ke ","Xi ","Di ","Ao ","Zui ","[?] ","Ni ","Rong ","Dao ","Ling ","Za ","Yu ","Yue ","Yin ","[?] ","Jie ","Li ","Sui ","Long ","Long ","Dian ","Ying ","Xi ","Ju ","Chan ","Ying ","Kui ","Yan ","Wei ","Nao ","Quan ","Chao ","Cuan ","Luan ","Dian ","Dian ","[?] ","Yan ","Yan ","Yan ","Nao ","Yan ","Chuan ","Gui ","Chuan ","Zhou ","Huang ","Jing ","Xun ","Chao ","Chao ","Lie ","Gong ","Zuo ","Qiao ","Ju ","Gong ","Kek ","Wu ","Pwu ","Pwu ","Chai ","Qiu ","Qiu ","Ji ","Yi ","Si ","Ba ","Zhi ","Zhao ","Xiang ","Yi ","Jin ","Xun ","Juan ","Phas ","Xun ","Jin ","Fu " ];
|
|
|
|
},{}],86:[function(require,module,exports){
|
|
module.exports = [ "Za ","Bi ","Shi ","Bu ","Ding ","Shuai ","Fan ","Nie ","Shi ","Fen ","Pa ","Zhi ","Xi ","Hu ","Dan ","Wei ","Zhang ","Tang ","Dai ","Ma ","Pei ","Pa ","Tie ","Fu ","Lian ","Zhi ","Zhou ","Bo ","Zhi ","Di ","Mo ","Yi ","Yi ","Ping ","Qia ","Juan ","Ru ","Shuai ","Dai ","Zheng ","Shui ","Qiao ","Zhen ","Shi ","Qun ","Xi ","Bang ","Dai ","Gui ","Chou ","Ping ","Zhang ","Sha ","Wan ","Dai ","Wei ","Chang ","Sha ","Qi ","Ze ","Guo ","Mao ","Du ","Hou ","Zheng ","Xu ","Mi ","Wei ","Wo ","Fu ","Yi ","Bang ","Ping ","Tazuna ","Gong ","Pan ","Huang ","Dao ","Mi ","Jia ","Teng ","Hui ","Zhong ","Shan ","Man ","Mu ","Biao ","Guo ","Ze ","Mu ","Bang ","Zhang ","Jiong ","Chan ","Fu ","Zhi ","Hu ","Fan ","Chuang ","Bi ","Hei ","[?] ","Mi ","Qiao ","Chan ","Fen ","Meng ","Bang ","Chou ","Mie ","Chu ","Jie ","Xian ","Lan ","Gan ","Ping ","Nian ","Qian ","Bing ","Bing ","Xing ","Gan ","Yao ","Huan ","You ","You ","Ji ","Yan ","Pi ","Ting ","Ze ","Guang ","Zhuang ","Mo ","Qing ","Bi ","Qin ","Dun ","Chuang ","Gui ","Ya ","Bai ","Jie ","Xu ","Lu ","Wu ","[?] ","Ku ","Ying ","Di ","Pao ","Dian ","Ya ","Miao ","Geng ","Ci ","Fu ","Tong ","Pang ","Fei ","Xiang ","Yi ","Zhi ","Tiao ","Zhi ","Xiu ","Du ","Zuo ","Xiao ","Tu ","Gui ","Ku ","Pang ","Ting ","You ","Bu ","Ding ","Cheng ","Lai ","Bei ","Ji ","An ","Shu ","Kang ","Yong ","Tuo ","Song ","Shu ","Qing ","Yu ","Yu ","Miao ","Sou ","Ce ","Xiang ","Fei ","Jiu ","He ","Hui ","Liu ","Sha ","Lian ","Lang ","Sou ","Jian ","Pou ","Qing ","Jiu ","Jiu ","Qin ","Ao ","Kuo ","Lou ","Yin ","Liao ","Dai ","Lu ","Yi ","Chu ","Chan ","Tu ","Si ","Xin ","Miao ","Chang ","Wu ","Fei ","Guang ","Koc ","Kuai ","Bi ","Qiang ","Xie ","Lin ","Lin ","Liao ","Lu ","[?] ","Ying ","Xian ","Ting ","Yong ","Li ","Ting ","Yin ","Xun ","Yan ","Ting ","Di ","Po ","Jian ","Hui ","Nai ","Hui ","Gong ","Nian " ];
|
|
|
|
},{}],87:[function(require,module,exports){
|
|
module.exports = [ "Kai ","Bian ","Yi ","Qi ","Nong ","Fen ","Ju ","Yan ","Yi ","Zang ","Bi ","Yi ","Yi ","Er ","San ","Shi ","Er ","Shi ","Shi ","Gong ","Diao ","Yin ","Hu ","Fu ","Hong ","Wu ","Tui ","Chi ","Jiang ","Ba ","Shen ","Di ","Zhang ","Jue ","Tao ","Fu ","Di ","Mi ","Xian ","Hu ","Chao ","Nu ","Jing ","Zhen ","Yi ","Mi ","Quan ","Wan ","Shao ","Ruo ","Xuan ","Jing ","Dun ","Zhang ","Jiang ","Qiang ","Peng ","Dan ","Qiang ","Bi ","Bi ","She ","Dan ","Jian ","Gou ","Sei ","Fa ","Bi ","Kou ","Nagi ","Bie ","Xiao ","Dan ","Kuo ","Qiang ","Hong ","Mi ","Kuo ","Wan ","Jue ","Ji ","Ji ","Gui ","Dang ","Lu ","Lu ","Tuan ","Hui ","Zhi ","Hui ","Hui ","Yi ","Yi ","Yi ","Yi ","Huo ","Huo ","Shan ","Xing ","Wen ","Tong ","Yan ","Yan ","Yu ","Chi ","Cai ","Biao ","Diao ","Bin ","Peng ","Yong ","Piao ","Zhang ","Ying ","Chi ","Chi ","Zhuo ","Tuo ","Ji ","Pang ","Zhong ","Yi ","Wang ","Che ","Bi ","Chi ","Ling ","Fu ","Wang ","Zheng ","Cu ","Wang ","Jing ","Dai ","Xi ","Xun ","Hen ","Yang ","Huai ","Lu ","Hou ","Wa ","Cheng ","Zhi ","Xu ","Jing ","Tu ","Cong ","[?] ","Lai ","Cong ","De ","Pai ","Xi ","[?] ","Qi ","Chang ","Zhi ","Cong ","Zhou ","Lai ","Yu ","Xie ","Jie ","Jian ","Chi ","Jia ","Bian ","Huang ","Fu ","Xun ","Wei ","Pang ","Yao ","Wei ","Xi ","Zheng ","Piao ","Chi ","De ","Zheng ","Zheng ","Bie ","De ","Chong ","Che ","Jiao ","Wei ","Jiao ","Hui ","Mei ","Long ","Xiang ","Bao ","Qu ","Xin ","Shu ","Bi ","Yi ","Le ","Ren ","Dao ","Ding ","Gai ","Ji ","Ren ","Ren ","Chan ","Tan ","Te ","Te ","Gan ","Qi ","Shi ","Cun ","Zhi ","Wang ","Mang ","Xi ","Fan ","Ying ","Tian ","Min ","Min ","Zhong ","Chong ","Wu ","Ji ","Wu ","Xi ","Ye ","You ","Wan ","Cong ","Zhong ","Kuai ","Yu ","Bian ","Zhi ","Qi ","Cui ","Chen ","Tai ","Tun ","Qian ","Nian ","Hun ","Xiong ","Niu ","Wang ","Xian ","Xin ","Kang ","Hu ","Kai ","Fen " ];
|
|
|
|
},{}],88:[function(require,module,exports){
|
|
module.exports = [ "Huai ","Tai ","Song ","Wu ","Ou ","Chang ","Chuang ","Ju ","Yi ","Bao ","Chao ","Min ","Pei ","Zuo ","Zen ","Yang ","Kou ","Ban ","Nu ","Nao ","Zheng ","Pa ","Bu ","Tie ","Gu ","Hu ","Ju ","Da ","Lian ","Si ","Chou ","Di ","Dai ","Yi ","Tu ","You ","Fu ","Ji ","Peng ","Xing ","Yuan ","Ni ","Guai ","Fu ","Xi ","Bi ","You ","Qie ","Xuan ","Cong ","Bing ","Huang ","Xu ","Chu ","Pi ","Xi ","Xi ","Tan ","Koraeru ","Zong ","Dui ","[?] ","Ki ","Yi ","Chi ","Ren ","Xun ","Shi ","Xi ","Lao ","Heng ","Kuang ","Mu ","Zhi ","Xie ","Lian ","Tiao ","Huang ","Die ","Hao ","Kong ","Gui ","Heng ","Xi ","Xiao ","Shu ","S ","Kua ","Qiu ","Yang ","Hui ","Hui ","Chi ","Jia ","Yi ","Xiong ","Guai ","Lin ","Hui ","Zi ","Xu ","Chi ","Xiang ","Nu ","Hen ","En ","Ke ","Tong ","Tian ","Gong ","Quan ","Xi ","Qia ","Yue ","Peng ","Ken ","De ","Hui ","E ","Kyuu ","Tong ","Yan ","Kai ","Ce ","Nao ","Yun ","Mang ","Yong ","Yong ","Yuan ","Pi ","Kun ","Qiao ","Yue ","Yu ","Yu ","Jie ","Xi ","Zhe ","Lin ","Ti ","Han ","Hao ","Qie ","Ti ","Bu ","Yi ","Qian ","Hui ","Xi ","Bei ","Man ","Yi ","Heng ","Song ","Quan ","Cheng ","Hui ","Wu ","Wu ","You ","Li ","Liang ","Huan ","Cong ","Yi ","Yue ","Li ","Nin ","Nao ","E ","Que ","Xuan ","Qian ","Wu ","Min ","Cong ","Fei ","Bei ","Duo ","Cui ","Chang ","Men ","Li ","Ji ","Guan ","Guan ","Xing ","Dao ","Qi ","Kong ","Tian ","Lun ","Xi ","Kan ","Kun ","Ni ","Qing ","Chou ","Dun ","Guo ","Chan ","Liang ","Wan ","Yuan ","Jin ","Ji ","Lin ","Yu ","Huo ","He ","Quan ","Tan ","Ti ","Ti ","Nie ","Wang ","Chuo ","Bu ","Hun ","Xi ","Tang ","Xin ","Wei ","Hui ","E ","Rui ","Zong ","Jian ","Yong ","Dian ","Ju ","Can ","Cheng ","De ","Bei ","Qie ","Can ","Dan ","Guan ","Duo ","Nao ","Yun ","Xiang ","Zhui ","Die ","Huang ","Chun ","Qiong ","Re ","Xing ","Ce ","Bian ","Hun ","Zong ","Ti " ];
|
|
|
|
},{}],89:[function(require,module,exports){
|
|
module.exports = [ "Qiao ","Chou ","Bei ","Xuan ","Wei ","Ge ","Qian ","Wei ","Yu ","Yu ","Bi ","Xuan ","Huan ","Min ","Bi ","Yi ","Mian ","Yong ","Kai ","Dang ","Yin ","E ","Chen ","Mou ","Ke ","Ke ","Yu ","Ai ","Qie ","Yan ","Nuo ","Gan ","Yun ","Zong ","Sai ","Leng ","Fen ","[?] ","Kui ","Kui ","Que ","Gong ","Yun ","Su ","Su ","Qi ","Yao ","Song ","Huang ","Ji ","Gu ","Ju ","Chuang ","Ni ","Xie ","Kai ","Zheng ","Yong ","Cao ","Sun ","Shen ","Bo ","Kai ","Yuan ","Xie ","Hun ","Yong ","Yang ","Li ","Sao ","Tao ","Yin ","Ci ","Xu ","Qian ","Tai ","Huang ","Yun ","Shen ","Ming ","[?] ","She ","Cong ","Piao ","Mo ","Mu ","Guo ","Chi ","Can ","Can ","Can ","Cui ","Min ","Te ","Zhang ","Tong ","Ao ","Shuang ","Man ","Guan ","Que ","Zao ","Jiu ","Hui ","Kai ","Lian ","Ou ","Song ","Jin ","Yin ","Lu ","Shang ","Wei ","Tuan ","Man ","Qian ","She ","Yong ","Qing ","Kang ","Di ","Zhi ","Lou ","Juan ","Qi ","Qi ","Yu ","Ping ","Liao ","Cong ","You ","Chong ","Zhi ","Tong ","Cheng ","Qi ","Qu ","Peng ","Bei ","Bie ","Chun ","Jiao ","Zeng ","Chi ","Lian ","Ping ","Kui ","Hui ","Qiao ","Cheng ","Yin ","Yin ","Xi ","Xi ","Dan ","Tan ","Duo ","Dui ","Dui ","Su ","Jue ","Ce ","Xiao ","Fan ","Fen ","Lao ","Lao ","Chong ","Han ","Qi ","Xian ","Min ","Jing ","Liao ","Wu ","Can ","Jue ","Cu ","Xian ","Tan ","Sheng ","Pi ","Yi ","Chu ","Xian ","Nao ","Dan ","Tan ","Jing ","Song ","Han ","Jiao ","Wai ","Huan ","Dong ","Qin ","Qin ","Qu ","Cao ","Ken ","Xie ","Ying ","Ao ","Mao ","Yi ","Lin ","Se ","Jun ","Huai ","Men ","Lan ","Ai ","Lin ","Yan ","Gua ","Xia ","Chi ","Yu ","Yin ","Dai ","Meng ","Ai ","Meng ","Dui ","Qi ","Mo ","Lan ","Men ","Chou ","Zhi ","Nuo ","Nuo ","Yan ","Yang ","Bo ","Zhi ","Kuang ","Kuang ","You ","Fu ","Liu ","Mie ","Cheng ","[?] ","Chan ","Meng ","Lan ","Huai ","Xuan ","Rang ","Chan ","Ji ","Ju ","Huan ","She ","Yi " ];
|
|
|
|
},{}],90:[function(require,module,exports){
|
|
module.exports = [ "Lian ","Nan ","Mi ","Tang ","Jue ","Gang ","Gang ","Gang ","Ge ","Yue ","Wu ","Jian ","Xu ","Shu ","Rong ","Xi ","Cheng ","Wo ","Jie ","Ge ","Jian ","Qiang ","Huo ","Qiang ","Zhan ","Dong ","Qi ","Jia ","Die ","Zei ","Jia ","Ji ","Shi ","Kan ","Ji ","Kui ","Gai ","Deng ","Zhan ","Chuang ","Ge ","Jian ","Jie ","Yu ","Jian ","Yan ","Lu ","Xi ","Zhan ","Xi ","Xi ","Chuo ","Dai ","Qu ","Hu ","Hu ","Hu ","E ","Shi ","Li ","Mao ","Hu ","Li ","Fang ","Suo ","Bian ","Dian ","Jiong ","Shang ","Yi ","Yi ","Shan ","Hu ","Fei ","Yan ","Shou ","T ","Cai ","Zha ","Qiu ","Le ","Bu ","Ba ","Da ","Reng ","Fu ","Hameru ","Zai ","Tuo ","Zhang ","Diao ","Kang ","Yu ","Ku ","Han ","Shen ","Cha ","Yi ","Gu ","Kou ","Wu ","Tuo ","Qian ","Zhi ","Ren ","Kuo ","Men ","Sao ","Yang ","Niu ","Ban ","Che ","Rao ","Xi ","Qian ","Ban ","Jia ","Yu ","Fu ","Ao ","Xi ","Pi ","Zhi ","Zi ","E ","Dun ","Zhao ","Cheng ","Ji ","Yan ","Kuang ","Bian ","Chao ","Ju ","Wen ","Hu ","Yue ","Jue ","Ba ","Qin ","Zhen ","Zheng ","Yun ","Wan ","Nu ","Yi ","Shu ","Zhua ","Pou ","Tou ","Dou ","Kang ","Zhe ","Pou ","Fu ","Pao ","Ba ","Ao ","Ze ","Tuan ","Kou ","Lun ","Qiang ","[?] ","Hu ","Bao ","Bing ","Zhi ","Peng ","Tan ","Pu ","Pi ","Tai ","Yao ","Zhen ","Zha ","Yang ","Bao ","He ","Ni ","Yi ","Di ","Chi ","Pi ","Za ","Mo ","Mo ","Shen ","Ya ","Chou ","Qu ","Min ","Chu ","Jia ","Fu ","Zhan ","Zhu ","Dan ","Chai ","Mu ","Nian ","La ","Fu ","Pao ","Ban ","Pai ","Ling ","Na ","Guai ","Qian ","Ju ","Tuo ","Ba ","Tuo ","Tuo ","Ao ","Ju ","Zhuo ","Pan ","Zhao ","Bai ","Bai ","Di ","Ni ","Ju ","Kuo ","Long ","Jian ","[?] ","Yong ","Lan ","Ning ","Bo ","Ze ","Qian ","Hen ","Gua ","Shi ","Jie ","Zheng ","Nin ","Gong ","Gong ","Quan ","Shuan ","Cun ","Zan ","Kao ","Chi ","Xie ","Ce ","Hui ","Pin ","Zhuai ","Shi ","Na " ];
|
|
|
|
},{}],91:[function(require,module,exports){
|
|
module.exports = [ "Bo ","Chi ","Gua ","Zhi ","Kuo ","Duo ","Duo ","Zhi ","Qie ","An ","Nong ","Zhen ","Ge ","Jiao ","Ku ","Dong ","Ru ","Tiao ","Lie ","Zha ","Lu ","Die ","Wa ","Jue ","Mushiru ","Ju ","Zhi ","Luan ","Ya ","Zhua ","Ta ","Xie ","Nao ","Dang ","Jiao ","Zheng ","Ji ","Hui ","Xun ","Ku ","Ai ","Tuo ","Nuo ","Cuo ","Bo ","Geng ","Ti ","Zhen ","Cheng ","Suo ","Suo ","Keng ","Mei ","Long ","Ju ","Peng ","Jian ","Yi ","Ting ","Shan ","Nuo ","Wan ","Xie ","Cha ","Feng ","Jiao ","Wu ","Jun ","Jiu ","Tong ","Kun ","Huo ","Tu ","Zhuo ","Pou ","Le ","Ba ","Han ","Shao ","Nie ","Juan ","Ze ","Song ","Ye ","Jue ","Bu ","Huan ","Bu ","Zun ","Yi ","Zhai ","Lu ","Sou ","Tuo ","Lao ","Sun ","Bang ","Jian ","Huan ","Dao ","[?] ","Wan ","Qin ","Peng ","She ","Lie ","Min ","Men ","Fu ","Bai ","Ju ","Dao ","Wo ","Ai ","Juan ","Yue ","Zong ","Chen ","Chui ","Jie ","Tu ","Ben ","Na ","Nian ","Nuo ","Zu ","Wo ","Xi ","Xian ","Cheng ","Dian ","Sao ","Lun ","Qing ","Gang ","Duo ","Shou ","Diao ","Pou ","Di ","Zhang ","Gun ","Ji ","Tao ","Qia ","Qi ","Pai ","Shu ","Qian ","Ling ","Yi ","Ya ","Jue ","Zheng ","Liang ","Gua ","Yi ","Huo ","Shan ","Zheng ","Lue ","Cai ","Tan ","Che ","Bing ","Jie ","Ti ","Kong ","Tui ","Yan ","Cuo ","Zou ","Ju ","Tian ","Qian ","Ken ","Bai ","Shou ","Jie ","Lu ","Guo ","Haba ","[?] ","Zhi ","Dan ","Mang ","Xian ","Sao ","Guan ","Peng ","Yuan ","Nuo ","Jian ","Zhen ","Jiu ","Jian ","Yu ","Yan ","Kui ","Nan ","Hong ","Rou ","Pi ","Wei ","Sai ","Zou ","Xuan ","Miao ","Ti ","Nie ","Cha ","Shi ","Zong ","Zhen ","Yi ","Shun ","Heng ","Bian ","Yang ","Huan ","Yan ","Zuan ","An ","Xu ","Ya ","Wo ","Ke ","Chuai ","Ji ","Ti ","La ","La ","Cheng ","Kai ","Jiu ","Jiu ","Tu ","Jie ","Hui ","Geng ","Chong ","Shuo ","She ","Xie ","Yuan ","Qian ","Ye ","Cha ","Zha ","Bei ","Yao ","[?] ","[?] ","Lan ","Wen ","Qin " ];
|
|
|
|
},{}],92:[function(require,module,exports){
|
|
module.exports = [ "Chan ","Ge ","Lou ","Zong ","Geng ","Jiao ","Gou ","Qin ","Yong ","Que ","Chou ","Chi ","Zhan ","Sun ","Sun ","Bo ","Chu ","Rong ","Beng ","Cuo ","Sao ","Ke ","Yao ","Dao ","Zhi ","Nu ","Xie ","Jian ","Sou ","Qiu ","Gao ","Xian ","Shuo ","Sang ","Jin ","Mie ","E ","Chui ","Nuo ","Shan ","Ta ","Jie ","Tang ","Pan ","Ban ","Da ","Li ","Tao ","Hu ","Zhi ","Wa ","Xia ","Qian ","Wen ","Qiang ","Tian ","Zhen ","E ","Xi ","Nuo ","Quan ","Cha ","Zha ","Ge ","Wu ","En ","She ","Kang ","She ","Shu ","Bai ","Yao ","Bin ","Sou ","Tan ","Sa ","Chan ","Suo ","Liao ","Chong ","Chuang ","Guo ","Bing ","Feng ","Shuai ","Di ","Qi ","Sou ","Zhai ","Lian ","Tang ","Chi ","Guan ","Lu ","Luo ","Lou ","Zong ","Gai ","Hu ","Zha ","Chuang ","Tang ","Hua ","Cui ","Nai ","Mo ","Jiang ","Gui ","Ying ","Zhi ","Ao ","Zhi ","Nie ","Man ","Shan ","Kou ","Shu ","Suo ","Tuan ","Jiao ","Mo ","Mo ","Zhe ","Xian ","Keng ","Piao ","Jiang ","Yin ","Gou ","Qian ","Lue ","Ji ","Ying ","Jue ","Pie ","Pie ","Lao ","Dun ","Xian ","Ruan ","Kui ","Zan ","Yi ","Xun ","Cheng ","Cheng ","Sa ","Nao ","Heng ","Si ","Qian ","Huang ","Da ","Zun ","Nian ","Lin ","Zheng ","Hui ","Zhuang ","Jiao ","Ji ","Cao ","Dan ","Dan ","Che ","Bo ","Che ","Jue ","Xiao ","Liao ","Ben ","Fu ","Qiao ","Bo ","Cuo ","Zhuo ","Zhuan ","Tuo ","Pu ","Qin ","Dun ","Nian ","[?] ","Xie ","Lu ","Jiao ","Cuan ","Ta ","Han ","Qiao ","Zhua ","Jian ","Gan ","Yong ","Lei ","Kuo ","Lu ","Shan ","Zhuo ","Ze ","Pu ","Chuo ","Ji ","Dang ","Suo ","Cao ","Qing ","Jing ","Huan ","Jie ","Qin ","Kuai ","Dan ","Xi ","Ge ","Pi ","Bo ","Ao ","Ju ","Ye ","[?] ","Mang ","Sou ","Mi ","Ji ","Tai ","Zhuo ","Dao ","Xing ","Lan ","Ca ","Ju ","Ye ","Ru ","Ye ","Ye ","Ni ","Hu ","Ji ","Bin ","Ning ","Ge ","Zhi ","Jie ","Kuo ","Mo ","Jian ","Xie ","Lie ","Tan ","Bai ","Sou ","Lu ","Lue ","Rao ","Zhi " ];
|
|
|
|
},{}],93:[function(require,module,exports){
|
|
module.exports = [ "Pan ","Yang ","Lei ","Sa ","Shu ","Zan ","Nian ","Xian ","Jun ","Huo ","Li ","La ","Han ","Ying ","Lu ","Long ","Qian ","Qian ","Zan ","Qian ","Lan ","San ","Ying ","Mei ","Rang ","Chan ","[?] ","Cuan ","Xi ","She ","Luo ","Jun ","Mi ","Li ","Zan ","Luan ","Tan ","Zuan ","Li ","Dian ","Wa ","Dang ","Jiao ","Jue ","Lan ","Li ","Nang ","Zhi ","Gui ","Gui ","Qi ","Xin ","Pu ","Sui ","Shou ","Kao ","You ","Gai ","Yi ","Gong ","Gan ","Ban ","Fang ","Zheng ","Bo ","Dian ","Kou ","Min ","Wu ","Gu ","He ","Ce ","Xiao ","Mi ","Chu ","Ge ","Di ","Xu ","Jiao ","Min ","Chen ","Jiu ","Zhen ","Duo ","Yu ","Chi ","Ao ","Bai ","Xu ","Jiao ","Duo ","Lian ","Nie ","Bi ","Chang ","Dian ","Duo ","Yi ","Gan ","San ","Ke ","Yan ","Dun ","Qi ","Dou ","Xiao ","Duo ","Jiao ","Jing ","Yang ","Xia ","Min ","Shu ","Ai ","Qiao ","Ai ","Zheng ","Di ","Zhen ","Fu ","Shu ","Liao ","Qu ","Xiong ","Xi ","Jiao ","Sen ","Jiao ","Zhuo ","Yi ","Lian ","Bi ","Li ","Xiao ","Xiao ","Wen ","Xue ","Qi ","Qi ","Zhai ","Bin ","Jue ","Zhai ","[?] ","Fei ","Ban ","Ban ","Lan ","Yu ","Lan ","Wei ","Dou ","Sheng ","Liao ","Jia ","Hu ","Xie ","Jia ","Yu ","Zhen ","Jiao ","Wo ","Tou ","Chu ","Jin ","Chi ","Yin ","Fu ","Qiang ","Zhan ","Qu ","Zhuo ","Zhan ","Duan ","Zhuo ","Si ","Xin ","Zhuo ","Zhuo ","Qin ","Lin ","Zhuo ","Chu ","Duan ","Zhu ","Fang ","Xie ","Hang ","Yu ","Shi ","Pei ","You ","Mye ","Pang ","Qi ","Zhan ","Mao ","Lu ","Pei ","Pi ","Liu ","Fu ","Fang ","Xuan ","Jing ","Jing ","Ni ","Zu ","Zhao ","Yi ","Liu ","Shao ","Jian ","Es ","Yi ","Qi ","Zhi ","Fan ","Piao ","Fan ","Zhan ","Guai ","Sui ","Yu ","Wu ","Ji ","Ji ","Ji ","Huo ","Ri ","Dan ","Jiu ","Zhi ","Zao ","Xie ","Tiao ","Xun ","Xu ","Xu ","Xu ","Gan ","Han ","Tai ","Di ","Xu ","Chan ","Shi ","Kuang ","Yang ","Shi ","Wang ","Min ","Min ","Tun ","Chun ","Wu " ];
|
|
|
|
},{}],94:[function(require,module,exports){
|
|
module.exports = [ "Yun ","Bei ","Ang ","Ze ","Ban ","Jie ","Kun ","Sheng ","Hu ","Fang ","Hao ","Gui ","Chang ","Xuan ","Ming ","Hun ","Fen ","Qin ","Hu ","Yi ","Xi ","Xin ","Yan ","Ze ","Fang ","Tan ","Shen ","Ju ","Yang ","Zan ","Bing ","Xing ","Ying ","Xuan ","Pei ","Zhen ","Ling ","Chun ","Hao ","Mei ","Zuo ","Mo ","Bian ","Xu ","Hun ","Zhao ","Zong ","Shi ","Shi ","Yu ","Fei ","Die ","Mao ","Ni ","Chang ","Wen ","Dong ","Ai ","Bing ","Ang ","Zhou ","Long ","Xian ","Kuang ","Tiao ","Chao ","Shi ","Huang ","Huang ","Xuan ","Kui ","Xu ","Jiao ","Jin ","Zhi ","Jin ","Shang ","Tong ","Hong ","Yan ","Gai ","Xiang ","Shai ","Xiao ","Ye ","Yun ","Hui ","Han ","Han ","Jun ","Wan ","Xian ","Kun ","Zhou ","Xi ","Cheng ","Sheng ","Bu ","Zhe ","Zhe ","Wu ","Han ","Hui ","Hao ","Chen ","Wan ","Tian ","Zhuo ","Zui ","Zhou ","Pu ","Jing ","Xi ","Shan ","Yi ","Xi ","Qing ","Qi ","Jing ","Gui ","Zhen ","Yi ","Zhi ","An ","Wan ","Lin ","Liang ","Chang ","Wang ","Xiao ","Zan ","Hi ","Xuan ","Xuan ","Yi ","Xia ","Yun ","Hui ","Fu ","Min ","Kui ","He ","Ying ","Du ","Wei ","Shu ","Qing ","Mao ","Nan ","Jian ","Nuan ","An ","Yang ","Chun ","Yao ","Suo ","Jin ","Ming ","Jiao ","Kai ","Gao ","Weng ","Chang ","Qi ","Hao ","Yan ","Li ","Ai ","Ji ","Gui ","Men ","Zan ","Xie ","Hao ","Mu ","Mo ","Cong ","Ni ","Zhang ","Hui ","Bao ","Han ","Xuan ","Chuan ","Liao ","Xian ","Dan ","Jing ","Pie ","Lin ","Tun ","Xi ","Yi ","Ji ","Huang ","Tai ","Ye ","Ye ","Li ","Tan ","Tong ","Xiao ","Fei ","Qin ","Zhao ","Hao ","Yi ","Xiang ","Xing ","Sen ","Jiao ","Bao ","Jing ","Yian ","Ai ","Ye ","Ru ","Shu ","Meng ","Xun ","Yao ","Pu ","Li ","Chen ","Kuang ","Die ","[?] ","Yan ","Huo ","Lu ","Xi ","Rong ","Long ","Nang ","Luo ","Luan ","Shai ","Tang ","Yan ","Chu ","Yue ","Yue ","Qu ","Yi ","Geng ","Ye ","Hu ","He ","Shu ","Cao ","Cao ","Noboru ","Man ","Ceng ","Ceng ","Ti " ];
|
|
|
|
},{}],95:[function(require,module,exports){
|
|
module.exports = [ "Zui ","Can ","Xu ","Hui ","Yin ","Qie ","Fen ","Pi ","Yue ","You ","Ruan ","Peng ","Ban ","Fu ","Ling ","Fei ","Qu ","[?] ","Nu ","Tiao ","Shuo ","Zhen ","Lang ","Lang ","Juan ","Ming ","Huang ","Wang ","Tun ","Zhao ","Ji ","Qi ","Ying ","Zong ","Wang ","Tong ","Lang ","[?] ","Meng ","Long ","Mu ","Deng ","Wei ","Mo ","Ben ","Zha ","Zhu ","Zhu ","[?] ","Zhu ","Ren ","Ba ","Po ","Duo ","Duo ","Dao ","Li ","Qiu ","Ji ","Jiu ","Bi ","Xiu ","Ting ","Ci ","Sha ","Eburi ","Za ","Quan ","Qian ","Yu ","Gan ","Wu ","Cha ","Shan ","Xun ","Fan ","Wu ","Zi ","Li ","Xing ","Cai ","Cun ","Ren ","Shao ","Tuo ","Di ","Zhang ","Mang ","Chi ","Yi ","Gu ","Gong ","Du ","Yi ","Qi ","Shu ","Gang ","Tiao ","Moku ","Soma ","Tochi ","Lai ","Sugi ","Mang ","Yang ","Ma ","Miao ","Si ","Yuan ","Hang ","Fei ","Bei ","Jie ","Dong ","Gao ","Yao ","Xian ","Chu ","Qun ","Pa ","Shu ","Hua ","Xin ","Chou ","Zhu ","Chou ","Song ","Ban ","Song ","Ji ","Yue ","Jin ","Gou ","Ji ","Mao ","Pi ","Bi ","Wang ","Ang ","Fang ","Fen ","Yi ","Fu ","Nan ","Xi ","Hu ","Ya ","Dou ","Xun ","Zhen ","Yao ","Lin ","Rui ","E ","Mei ","Zhao ","Guo ","Zhi ","Cong ","Yun ","Waku ","Dou ","Shu ","Zao ","[?] ","Li ","Haze ","Jian ","Cheng ","Matsu ","Qiang ","Feng ","Nan ","Xiao ","Xian ","Ku ","Ping ","Yi ","Xi ","Zhi ","Guai ","Xiao ","Jia ","Jia ","Gou ","Fu ","Mo ","Yi ","Ye ","Ye ","Shi ","Nie ","Bi ","Duo ","Yi ","Ling ","Bing ","Ni ","La ","He ","Pan ","Fan ","Zhong ","Dai ","Ci ","Yang ","Fu ","Bo ","Mou ","Gan ","Qi ","Ran ","Rou ","Mao ","Zhao ","Song ","Zhe ","Xia ","You ","Shen ","Ju ","Tuo ","Zuo ","Nan ","Ning ","Yong ","Di ","Zhi ","Zha ","Cha ","Dan ","Gu ","Pu ","Jiu ","Ao ","Fu ","Jian ","Bo ","Duo ","Ke ","Nai ","Zhu ","Bi ","Liu ","Chai ","Zha ","Si ","Zhu ","Pei ","Shi ","Guai ","Cha ","Yao ","Jue ","Jiu ","Shi " ];
|
|
|
|
},{}],96:[function(require,module,exports){
|
|
module.exports = [ "Zhi ","Liu ","Mei ","Hoy ","Rong ","Zha ","[?] ","Biao ","Zhan ","Jie ","Long ","Dong ","Lu ","Sayng ","Li ","Lan ","Yong ","Shu ","Xun ","Shuan ","Qi ","Zhen ","Qi ","Li ","Yi ","Xiang ","Zhen ","Li ","Su ","Gua ","Kan ","Bing ","Ren ","Xiao ","Bo ","Ren ","Bing ","Zi ","Chou ","Yi ","Jie ","Xu ","Zhu ","Jian ","Zui ","Er ","Er ","You ","Fa ","Gong ","Kao ","Lao ","Zhan ","Li ","Yin ","Yang ","He ","Gen ","Zhi ","Chi ","Ge ","Zai ","Luan ","Fu ","Jie ","Hang ","Gui ","Tao ","Guang ","Wei ","Kuang ","Ru ","An ","An ","Juan ","Yi ","Zhuo ","Ku ","Zhi ","Qiong ","Tong ","Sang ","Sang ","Huan ","Jie ","Jiu ","Xue ","Duo ","Zhui ","Yu ","Zan ","Kasei ","Ying ","Masu ","[?] ","Zhan ","Ya ","Nao ","Zhen ","Dang ","Qi ","Qiao ","Hua ","Kuai ","Jiang ","Zhuang ","Xun ","Suo ","Sha ","Zhen ","Bei ","Ting ","Gua ","Jing ","Bo ","Ben ","Fu ","Rui ","Tong ","Jue ","Xi ","Lang ","Liu ","Feng ","Qi ","Wen ","Jun ","Gan ","Cu ","Liang ","Qiu ","Ting ","You ","Mei ","Bang ","Long ","Peng ","Zhuang ","Di ","Xuan ","Tu ","Zao ","Ao ","Gu ","Bi ","Di ","Han ","Zi ","Zhi ","Ren ","Bei ","Geng ","Jian ","Huan ","Wan ","Nuo ","Jia ","Tiao ","Ji ","Xiao ","Lu ","Huan ","Shao ","Cen ","Fen ","Song ","Meng ","Wu ","Li ","Li ","Dou ","Cen ","Ying ","Suo ","Ju ","Ti ","Jie ","Kun ","Zhuo ","Shu ","Chan ","Fan ","Wei ","Jing ","Li ","Bing ","Fumoto ","Shikimi ","Tao ","Zhi ","Lai ","Lian ","Jian ","Zhuo ","Ling ","Li ","Qi ","Bing ","Zhun ","Cong ","Qian ","Mian ","Qi ","Qi ","Cai ","Gun ","Chan ","Te ","Fei ","Pai ","Bang ","Pou ","Hun ","Zong ","Cheng ","Zao ","Ji ","Li ","Peng ","Yu ","Yu ","Gu ","Hun ","Dong ","Tang ","Gang ","Wang ","Di ","Xi ","Fan ","Cheng ","Zhan ","Qi ","Yuan ","Yan ","Yu ","Quan ","Yi ","Sen ","Ren ","Chui ","Leng ","Qi ","Zhuo ","Fu ","Ke ","Lai ","Zou ","Zou ","Zhuo ","Guan ","Fen ","Fen ","Chen ","Qiong ","Nie " ];
|
|
|
|
},{}],97:[function(require,module,exports){
|
|
module.exports = [ "Wan ","Guo ","Lu ","Hao ","Jie ","Yi ","Chou ","Ju ","Ju ","Cheng ","Zuo ","Liang ","Qiang ","Zhi ","Zhui ","Ya ","Ju ","Bei ","Jiao ","Zhuo ","Zi ","Bin ","Peng ","Ding ","Chu ","Chang ","Kunugi ","Momiji ","Jian ","Gui ","Xi ","Du ","Qian ","Kunugi ","Soko ","Shide ","Luo ","Zhi ","Ken ","Myeng ","Tafu ","[?] ","Peng ","Zhan ","[?] ","Tuo ","Sen ","Duo ","Ye ","Fou ","Wei ","Wei ","Duan ","Jia ","Zong ","Jian ","Yi ","Shen ","Xi ","Yan ","Yan ","Chuan ","Zhan ","Chun ","Yu ","He ","Zha ","Wo ","Pian ","Bi ","Yao ","Huo ","Xu ","Ruo ","Yang ","La ","Yan ","Ben ","Hun ","Kui ","Jie ","Kui ","Si ","Feng ","Xie ","Tuo ","Zhi ","Jian ","Mu ","Mao ","Chu ","Hu ","Hu ","Lian ","Leng ","Ting ","Nan ","Yu ","You ","Mei ","Song ","Xuan ","Xuan ","Ying ","Zhen ","Pian ","Ye ","Ji ","Jie ","Ye ","Chu ","Shun ","Yu ","Cou ","Wei ","Mei ","Di ","Ji ","Jie ","Kai ","Qiu ","Ying ","Rou ","Heng ","Lou ","Le ","Hazou ","Katsura ","Pin ","Muro ","Gai ","Tan ","Lan ","Yun ","Yu ","Chen ","Lu ","Ju ","Sakaki ","[?] ","Pi ","Xie ","Jia ","Yi ","Zhan ","Fu ","Nai ","Mi ","Lang ","Rong ","Gu ","Jian ","Ju ","Ta ","Yao ","Zhen ","Bang ","Sha ","Yuan ","Zi ","Ming ","Su ","Jia ","Yao ","Jie ","Huang ","Gan ","Fei ","Zha ","Qian ","Ma ","Sun ","Yuan ","Xie ","Rong ","Shi ","Zhi ","Cui ","Yun ","Ting ","Liu ","Rong ","Tang ","Que ","Zhai ","Si ","Sheng ","Ta ","Ke ","Xi ","Gu ","Qi ","Kao ","Gao ","Sun ","Pan ","Tao ","Ge ","Xun ","Dian ","Nou ","Ji ","Shuo ","Gou ","Chui ","Qiang ","Cha ","Qian ","Huai ","Mei ","Xu ","Gang ","Gao ","Zhuo ","Tuo ","Hashi ","Yang ","Dian ","Jia ","Jian ","Zui ","Kashi ","Ori ","Bin ","Zhu ","[?] ","Xi ","Qi ","Lian ","Hui ","Yong ","Qian ","Guo ","Gai ","Gai ","Tuan ","Hua ","Cu ","Sen ","Cui ","Beng ","You ","Hu ","Jiang ","Hu ","Huan ","Kui ","Yi ","Nie ","Gao ","Kang ","Gui ","Gui ","Cao ","Man ","Jin " ];
|
|
|
|
},{}],98:[function(require,module,exports){
|
|
module.exports = [ "Di ","Zhuang ","Le ","Lang ","Chen ","Cong ","Li ","Xiu ","Qing ","Shuang ","Fan ","Tong ","Guan ","Ji ","Suo ","Lei ","Lu ","Liang ","Mi ","Lou ","Chao ","Su ","Ke ","Shu ","Tang ","Biao ","Lu ","Jiu ","Shu ","Zha ","Shu ","Zhang ","Men ","Mo ","Niao ","Yang ","Tiao ","Peng ","Zhu ","Sha ","Xi ","Quan ","Heng ","Jian ","Cong ","[?] ","Hokuso ","Qiang ","Tara ","Ying ","Er ","Xin ","Zhi ","Qiao ","Zui ","Cong ","Pu ","Shu ","Hua ","Kui ","Zhen ","Zun ","Yue ","Zhan ","Xi ","Xun ","Dian ","Fa ","Gan ","Mo ","Wu ","Qiao ","Nao ","Lin ","Liu ","Qiao ","Xian ","Run ","Fan ","Zhan ","Tuo ","Lao ","Yun ","Shun ","Tui ","Cheng ","Tang ","Meng ","Ju ","Cheng ","Su ","Jue ","Jue ","Tan ","Hui ","Ji ","Nuo ","Xiang ","Tuo ","Ning ","Rui ","Zhu ","Chuang ","Zeng ","Fen ","Qiong ","Ran ","Heng ","Cen ","Gu ","Liu ","Lao ","Gao ","Chu ","Zusa ","Nude ","Ca ","San ","Ji ","Dou ","Shou ","Lu ","[?] ","[?] ","Yuan ","Ta ","Shu ","Jiang ","Tan ","Lin ","Nong ","Yin ","Xi ","Sui ","Shan ","Zui ","Xuan ","Cheng ","Gan ","Ju ","Zui ","Yi ","Qin ","Pu ","Yan ","Lei ","Feng ","Hui ","Dang ","Ji ","Sui ","Bo ","Bi ","Ding ","Chu ","Zhua ","Kuai ","Ji ","Jie ","Jia ","Qing ","Zhe ","Jian ","Qiang ","Dao ","Yi ","Biao ","Song ","She ","Lin ","Kunugi ","Cha ","Meng ","Yin ","Tao ","Tai ","Mian ","Qi ","Toan ","Bin ","Huo ","Ji ","Qian ","Mi ","Ning ","Yi ","Gao ","Jian ","Yin ","Er ","Qing ","Yan ","Qi ","Mi ","Zhao ","Gui ","Chun ","Ji ","Kui ","Po ","Deng ","Chu ","[?] ","Mian ","You ","Zhi ","Guang ","Qian ","Lei ","Lei ","Sa ","Lu ","Li ","Cuan ","Lu ","Mie ","Hui ","Ou ","Lu ","Jie ","Gao ","Du ","Yuan ","Li ","Fei ","Zhuo ","Sou ","Lian ","Tamo ","Chu ","[?] ","Zhu ","Lu ","Yan ","Li ","Zhu ","Chen ","Jie ","E ","Su ","Huai ","Nie ","Yu ","Long ","Lai ","[?] ","Xian ","Kwi ","Ju ","Xiao ","Ling ","Ying ","Jian ","Yin ","You ","Ying " ];
|
|
|
|
},{}],99:[function(require,module,exports){
|
|
module.exports = [ "Xiang ","Nong ","Bo ","Chan ","Lan ","Ju ","Shuang ","She ","Wei ","Cong ","Quan ","Qu ","Cang ","[?] ","Yu ","Luo ","Li ","Zan ","Luan ","Dang ","Jue ","Em ","Lan ","Lan ","Zhu ","Lei ","Li ","Ba ","Nang ","Yu ","Ling ","Tsuki ","Qian ","Ci ","Huan ","Xin ","Yu ","Yu ","Qian ","Ou ","Xu ","Chao ","Chu ","Chi ","Kai ","Yi ","Jue ","Xi ","Xu ","Xia ","Yu ","Kuai ","Lang ","Kuan ","Shuo ","Xi ","Ai ","Yi ","Qi ","Hu ","Chi ","Qin ","Kuan ","Kan ","Kuan ","Kan ","Chuan ","Sha ","Gua ","Yin ","Xin ","Xie ","Yu ","Qian ","Xiao ","Yi ","Ge ","Wu ","Tan ","Jin ","Ou ","Hu ","Ti ","Huan ","Xu ","Pen ","Xi ","Xiao ","Xu ","Xi ","Sen ","Lian ","Chu ","Yi ","Kan ","Yu ","Chuo ","Huan ","Zhi ","Zheng ","Ci ","Bu ","Wu ","Qi ","Bu ","Bu ","Wai ","Ju ","Qian ","Chi ","Se ","Chi ","Se ","Zhong ","Sui ","Sui ","Li ","Cuo ","Yu ","Li ","Gui ","Dai ","Dai ","Si ","Jian ","Zhe ","Mo ","Mo ","Yao ","Mo ","Cu ","Yang ","Tian ","Sheng ","Dai ","Shang ","Xu ","Xun ","Shu ","Can ","Jue ","Piao ","Qia ","Qiu ","Su ","Qing ","Yun ","Lian ","Yi ","Fou ","Zhi ","Ye ","Can ","Hun ","Dan ","Ji ","Ye ","Zhen ","Yun ","Wen ","Chou ","Bin ","Ti ","Jin ","Shang ","Yin ","Diao ","Cu ","Hui ","Cuan ","Yi ","Dan ","Du ","Jiang ","Lian ","Bin ","Du ","Tsukusu ","Jian ","Shu ","Ou ","Duan ","Zhu ","Yin ","Qing ","Yi ","Sha ","Que ","Ke ","Yao ","Jun ","Dian ","Hui ","Hui ","Gu ","Que ","Ji ","Yi ","Ou ","Hui ","Duan ","Yi ","Xiao ","Wu ","Guan ","Mu ","Mei ","Mei ","Ai ","Zuo ","Du ","Yu ","Bi ","Bi ","Bi ","Pi ","Pi ","Bi ","Chan ","Mao ","[?] ","[?] ","Pu ","Mushiru ","Jia ","Zhan ","Sai ","Mu ","Tuo ","Xun ","Er ","Rong ","Xian ","Ju ","Mu ","Hao ","Qiu ","Dou ","Mushiru ","Tan ","Pei ","Ju ","Duo ","Cui ","Bi ","San ","[?] ","Mao ","Sui ","Yu ","Yu ","Tuo ","He ","Jian ","Ta ","San " ];
|
|
|
|
},{}],100:[function(require,module,exports){
|
|
module.exports = [ "Lu ","Mu ","Li ","Tong ","Rong ","Chang ","Pu ","Luo ","Zhan ","Sao ","Zhan ","Meng ","Luo ","Qu ","Die ","Shi ","Di ","Min ","Jue ","Mang ","Qi ","Pie ","Nai ","Qi ","Dao ","Xian ","Chuan ","Fen ","Ri ","Nei ","[?] ","Fu ","Shen ","Dong ","Qing ","Qi ","Yin ","Xi ","Hai ","Yang ","An ","Ya ","Ke ","Qing ","Ya ","Dong ","Dan ","Lu ","Qing ","Yang ","Yun ","Yun ","Shui ","San ","Zheng ","Bing ","Yong ","Dang ","Shitamizu ","Le ","Ni ","Tun ","Fan ","Gui ","Ting ","Zhi ","Qiu ","Bin ","Ze ","Mian ","Cuan ","Hui ","Diao ","Yi ","Cha ","Zhuo ","Chuan ","Wan ","Fan ","Dai ","Xi ","Tuo ","Mang ","Qiu ","Qi ","Shan ","Pai ","Han ","Qian ","Wu ","Wu ","Xun ","Si ","Ru ","Gong ","Jiang ","Chi ","Wu ","Tsuchi ","[?] ","Tang ","Zhi ","Chi ","Qian ","Mi ","Yu ","Wang ","Qing ","Jing ","Rui ","Jun ","Hong ","Tai ","Quan ","Ji ","Bian ","Bian ","Gan ","Wen ","Zhong ","Fang ","Xiong ","Jue ","Hang ","Niou ","Qi ","Fen ","Xu ","Xu ","Qin ","Yi ","Wo ","Yun ","Yuan ","Hang ","Yan ","Chen ","Chen ","Dan ","You ","Dun ","Hu ","Huo ","Qie ","Mu ","Rou ","Mei ","Ta ","Mian ","Wu ","Chong ","Tian ","Bi ","Sha ","Zhi ","Pei ","Pan ","Zhui ","Za ","Gou ","Liu ","Mei ","Ze ","Feng ","Ou ","Li ","Lun ","Cang ","Feng ","Wei ","Hu ","Mo ","Mei ","Shu ","Ju ","Zan ","Tuo ","Tuo ","Tuo ","He ","Li ","Mi ","Yi ","Fa ","Fei ","You ","Tian ","Zhi ","Zhao ","Gu ","Zhan ","Yan ","Si ","Kuang ","Jiong ","Ju ","Xie ","Qiu ","Yi ","Jia ","Zhong ","Quan ","Bo ","Hui ","Mi ","Ben ","Zhuo ","Chu ","Le ","You ","Gu ","Hong ","Gan ","Fa ","Mao ","Si ","Hu ","Ping ","Ci ","Fan ","Chi ","Su ","Ning ","Cheng ","Ling ","Pao ","Bo ","Qi ","Si ","Ni ","Ju ","Yue ","Zhu ","Sheng ","Lei ","Xuan ","Xue ","Fu ","Pan ","Min ","Tai ","Yang ","Ji ","Yong ","Guan ","Beng ","Xue ","Long ","Lu ","[?] ","Bo ","Xie ","Po ","Ze ","Jing ","Yin " ];
|
|
|
|
},{}],101:[function(require,module,exports){
|
|
module.exports = [ "Zhou ","Ji ","Yi ","Hui ","Hui ","Zui ","Cheng ","Yin ","Wei ","Hou ","Jian ","Yang ","Lie ","Si ","Ji ","Er ","Xing ","Fu ","Sa ","Suo ","Zhi ","Yin ","Wu ","Xi ","Kao ","Zhu ","Jiang ","Luo ","[?] ","An ","Dong ","Yi ","Mou ","Lei ","Yi ","Mi ","Quan ","Jin ","Mo ","Wei ","Xiao ","Xie ","Hong ","Xu ","Shuo ","Kuang ","Tao ","Qie ","Ju ","Er ","Zhou ","Ru ","Ping ","Xun ","Xiong ","Zhi ","Guang ","Huan ","Ming ","Huo ","Wa ","Qia ","Pai ","Wu ","Qu ","Liu ","Yi ","Jia ","Jing ","Qian ","Jiang ","Jiao ","Cheng ","Shi ","Zhuo ","Ce ","Pal ","Kuai ","Ji ","Liu ","Chan ","Hun ","Hu ","Nong ","Xun ","Jin ","Lie ","Qiu ","Wei ","Zhe ","Jun ","Han ","Bang ","Mang ","Zhuo ","You ","Xi ","Bo ","Dou ","Wan ","Hong ","Yi ","Pu ","Ying ","Lan ","Hao ","Lang ","Han ","Li ","Geng ","Fu ","Wu ","Lian ","Chun ","Feng ","Yi ","Yu ","Tong ","Lao ","Hai ","Jin ","Jia ","Chong ","Weng ","Mei ","Sui ","Cheng ","Pei ","Xian ","Shen ","Tu ","Kun ","Pin ","Nie ","Han ","Jing ","Xiao ","She ","Nian ","Tu ","Yong ","Xiao ","Xian ","Ting ","E ","Su ","Tun ","Juan ","Cen ","Ti ","Li ","Shui ","Si ","Lei ","Shui ","Tao ","Du ","Lao ","Lai ","Lian ","Wei ","Wo ","Yun ","Huan ","Di ","[?] ","Run ","Jian ","Zhang ","Se ","Fu ","Guan ","Xing ","Shou ","Shuan ","Ya ","Chuo ","Zhang ","Ye ","Kong ","Wo ","Han ","Tuo ","Dong ","He ","Wo ","Ju ","Gan ","Liang ","Hun ","Ta ","Zhuo ","Dian ","Qie ","De ","Juan ","Zi ","Xi ","Yao ","Qi ","Gu ","Guo ","Han ","Lin ","Tang ","Zhou ","Peng ","Hao ","Chang ","Shu ","Qi ","Fang ","Chi ","Lu ","Nao ","Ju ","Tao ","Cong ","Lei ","Zhi ","Peng ","Fei ","Song ","Tian ","Pi ","Dan ","Yu ","Ni ","Yu ","Lu ","Gan ","Mi ","Jing ","Ling ","Lun ","Yin ","Cui ","Qu ","Huai ","Yu ","Nian ","Shen ","Piao ","Chun ","Wa ","Yuan ","Lai ","Hun ","Qing ","Yan ","Qian ","Tian ","Miao ","Zhi ","Yin ","Mi " ];
|
|
|
|
},{}],102:[function(require,module,exports){
|
|
module.exports = [ "Ben ","Yuan ","Wen ","Re ","Fei ","Qing ","Yuan ","Ke ","Ji ","She ","Yuan ","Shibui ","Lu ","Zi ","Du ","[?] ","Jian ","Min ","Pi ","Tani ","Yu ","Yuan ","Shen ","Shen ","Rou ","Huan ","Zhu ","Jian ","Nuan ","Yu ","Qiu ","Ting ","Qu ","Du ","Feng ","Zha ","Bo ","Wo ","Wo ","Di ","Wei ","Wen ","Ru ","Xie ","Ce ","Wei ","Ge ","Gang ","Yan ","Hong ","Xuan ","Mi ","Ke ","Mao ","Ying ","Yan ","You ","Hong ","Miao ","Xing ","Mei ","Zai ","Hun ","Nai ","Kui ","Shi ","E ","Pai ","Mei ","Lian ","Qi ","Qi ","Mei ","Tian ","Cou ","Wei ","Can ","Tuan ","Mian ","Hui ","Mo ","Xu ","Ji ","Pen ","Jian ","Jian ","Hu ","Feng ","Xiang ","Yi ","Yin ","Zhan ","Shi ","Jie ","Cheng ","Huang ","Tan ","Yu ","Bi ","Min ","Shi ","Tu ","Sheng ","Yong ","Qu ","Zhong ","Suei ","Jiu ","Jiao ","Qiou ","Yin ","Tang ","Long ","Huo ","Yuan ","Nan ","Ban ","You ","Quan ","Chui ","Liang ","Chan ","Yan ","Chun ","Nie ","Zi ","Wan ","Shi ","Man ","Ying ","Ratsu ","Kui ","[?] ","Jian ","Xu ","Lu ","Gui ","Gai ","[?] ","[?] ","Po ","Jin ","Gui ","Tang ","Yuan ","Suo ","Yuan ","Lian ","Yao ","Meng ","Zhun ","Sheng ","Ke ","Tai ","Da ","Wa ","Liu ","Gou ","Sao ","Ming ","Zha ","Shi ","Yi ","Lun ","Ma ","Pu ","Wei ","Li ","Cai ","Wu ","Xi ","Wen ","Qiang ","Ze ","Shi ","Su ","Yi ","Zhen ","Sou ","Yun ","Xiu ","Yin ","Rong ","Hun ","Su ","Su ","Ni ","Ta ","Shi ","Ru ","Wei ","Pan ","Chu ","Chu ","Pang ","Weng ","Cang ","Mie ","He ","Dian ","Hao ","Huang ","Xi ","Zi ","Di ","Zhi ","Ying ","Fu ","Jie ","Hua ","Ge ","Zi ","Tao ","Teng ","Sui ","Bi ","Jiao ","Hui ","Gun ","Yin ","Gao ","Long ","Zhi ","Yan ","She ","Man ","Ying ","Chun ","Lu ","Lan ","Luan ","[?] ","Bin ","Tan ","Yu ","Sou ","Hu ","Bi ","Biao ","Zhi ","Jiang ","Kou ","Shen ","Shang ","Di ","Mi ","Ao ","Lu ","Hu ","Hu ","You ","Chan ","Fan ","Yong ","Gun ","Man " ];
|
|
|
|
},{}],103:[function(require,module,exports){
|
|
module.exports = [ "Qing ","Yu ","Piao ","Ji ","Ya ","Jiao ","Qi ","Xi ","Ji ","Lu ","Lu ","Long ","Jin ","Guo ","Cong ","Lou ","Zhi ","Gai ","Qiang ","Li ","Yan ","Cao ","Jiao ","Cong ","Qun ","Tuan ","Ou ","Teng ","Ye ","Xi ","Mi ","Tang ","Mo ","Shang ","Han ","Lian ","Lan ","Wa ","Li ","Qian ","Feng ","Xuan ","Yi ","Man ","Zi ","Mang ","Kang ","Lei ","Peng ","Shu ","Zhang ","Zhang ","Chong ","Xu ","Huan ","Kuo ","Jian ","Yan ","Chuang ","Liao ","Cui ","Ti ","Yang ","Jiang ","Cong ","Ying ","Hong ","Xun ","Shu ","Guan ","Ying ","Xiao ","[?] ","[?] ","Xu ","Lian ","Zhi ","Wei ","Pi ","Jue ","Jiao ","Po ","Dang ","Hui ","Jie ","Wu ","Pa ","Ji ","Pan ","Gui ","Xiao ","Qian ","Qian ","Xi ","Lu ","Xi ","Xuan ","Dun ","Huang ","Min ","Run ","Su ","Liao ","Zhen ","Zhong ","Yi ","Di ","Wan ","Dan ","Tan ","Chao ","Xun ","Kui ","Yie ","Shao ","Tu ","Zhu ","San ","Hei ","Bi ","Shan ","Chan ","Chan ","Shu ","Tong ","Pu ","Lin ","Wei ","Se ","Se ","Cheng ","Jiong ","Cheng ","Hua ","Jiao ","Lao ","Che ","Gan ","Cun ","Heng ","Si ","Shu ","Peng ","Han ","Yun ","Liu ","Hong ","Fu ","Hao ","He ","Xian ","Jian ","Shan ","Xi ","Oki ","[?] ","Lan ","[?] ","Yu ","Lin ","Min ","Zao ","Dang ","Wan ","Ze ","Xie ","Yu ","Li ","Shi ","Xue ","Ling ","Man ","Zi ","Yong ","Kuai ","Can ","Lian ","Dian ","Ye ","Ao ","Huan ","Zhen ","Chan ","Man ","Dan ","Dan ","Yi ","Sui ","Pi ","Ju ","Ta ","Qin ","Ji ","Zhuo ","Lian ","Nong ","Guo ","Jin ","Fen ","Se ","Ji ","Sui ","Hui ","Chu ","Ta ","Song ","Ding ","[?] ","Zhu ","Lai ","Bin ","Lian ","Mi ","Shi ","Shu ","Mi ","Ning ","Ying ","Ying ","Meng ","Jin ","Qi ","Pi ","Ji ","Hao ","Ru ","Zui ","Wo ","Tao ","Yin ","Yin ","Dui ","Ci ","Huo ","Jing ","Lan ","Jun ","Ai ","Pu ","Zhuo ","Wei ","Bin ","Gu ","Qian ","Xing ","Hama ","Kuo ","Fei ","[?] ","Boku ","Jian ","Wei ","Luo ","Zan ","Lu ","Li " ];
|
|
|
|
},{}],104:[function(require,module,exports){
|
|
module.exports = [ "You ","Yang ","Lu ","Si ","Jie ","Ying ","Du ","Wang ","Hui ","Xie ","Pan ","Shen ","Biao ","Chan ","Mo ","Liu ","Jian ","Pu ","Se ","Cheng ","Gu ","Bin ","Huo ","Xian ","Lu ","Qin ","Han ","Ying ","Yong ","Li ","Jing ","Xiao ","Ying ","Sui ","Wei ","Xie ","Huai ","Hao ","Zhu ","Long ","Lai ","Dui ","Fan ","Hu ","Lai ","[?] ","[?] ","Ying ","Mi ","Ji ","Lian ","Jian ","Ying ","Fen ","Lin ","Yi ","Jian ","Yue ","Chan ","Dai ","Rang ","Jian ","Lan ","Fan ","Shuang ","Yuan ","Zhuo ","Feng ","She ","Lei ","Lan ","Cong ","Qu ","Yong ","Qian ","Fa ","Guan ","Que ","Yan ","Hao ","Hyeng ","Sa ","Zan ","Luan ","Yan ","Li ","Mi ","Shan ","Tan ","Dang ","Jiao ","Chan ","[?] ","Hao ","Ba ","Zhu ","Lan ","Lan ","Nang ","Wan ","Luan ","Xun ","Xian ","Yan ","Gan ","Yan ","Yu ","Huo ","Si ","Mie ","Guang ","Deng ","Hui ","Xiao ","Xiao ","Hu ","Hong ","Ling ","Zao ","Zhuan ","Jiu ","Zha ","Xie ","Chi ","Zhuo ","Zai ","Zai ","Can ","Yang ","Qi ","Zhong ","Fen ","Niu ","Jiong ","Wen ","Po ","Yi ","Lu ","Chui ","Pi ","Kai ","Pan ","Yan ","Kai ","Pang ","Mu ","Chao ","Liao ","Gui ","Kang ","Tun ","Guang ","Xin ","Zhi ","Guang ","Guang ","Wei ","Qiang ","[?] ","Da ","Xia ","Zheng ","Zhu ","Ke ","Zhao ","Fu ","Ba ","Duo ","Duo ","Ling ","Zhuo ","Xuan ","Ju ","Tan ","Pao ","Jiong ","Pao ","Tai ","Tai ","Bing ","Yang ","Tong ","Han ","Zhu ","Zha ","Dian ","Wei ","Shi ","Lian ","Chi ","Huang ","[?] ","Hu ","Shuo ","Lan ","Jing ","Jiao ","Xu ","Xing ","Quan ","Lie ","Huan ","Yang ","Xiao ","Xiu ","Xian ","Yin ","Wu ","Zhou ","Yao ","Shi ","Wei ","Tong ","Xue ","Zai ","Kai ","Hong ","Luo ","Xia ","Zhu ","Xuan ","Zheng ","Po ","Yan ","Hui ","Guang ","Zhe ","Hui ","Kao ","[?] ","Fan ","Shao ","Ye ","Hui ","[?] ","Tang ","Jin ","Re ","[?] ","Xi ","Fu ","Jiong ","Che ","Pu ","Jing ","Zhuo ","Ting ","Wan ","Hai ","Peng ","Lang ","Shan ","Hu ","Feng ","Chi ","Rong " ];
|
|
|
|
},{}],105:[function(require,module,exports){
|
|
module.exports = [ "Hu ","Xi ","Shu ","He ","Xun ","Ku ","Jue ","Xiao ","Xi ","Yan ","Han ","Zhuang ","Jun ","Di ","Xie ","Ji ","Wu ","[?] ","[?] ","Han ","Yan ","Huan ","Men ","Ju ","Chou ","Bei ","Fen ","Lin ","Kun ","Hun ","Tun ","Xi ","Cui ","Wu ","Hong ","Ju ","Fu ","Wo ","Jiao ","Cong ","Feng ","Ping ","Qiong ","Ruo ","Xi ","Qiong ","Xin ","Zhuo ","Yan ","Yan ","Yi ","Jue ","Yu ","Gang ","Ran ","Pi ","Gu ","[?] ","Sheng ","Chang ","Shao ","[?] ","[?] ","[?] ","[?] ","Chen ","He ","Kui ","Zhong ","Duan ","Xia ","Hui ","Feng ","Lian ","Xuan ","Xing ","Huang ","Jiao ","Jian ","Bi ","Ying ","Zhu ","Wei ","Tuan ","Tian ","Xi ","Nuan ","Nuan ","Chan ","Yan ","Jiong ","Jiong ","Yu ","Mei ","Sha ","Wei ","Ye ","Xin ","Qiong ","Rou ","Mei ","Huan ","Xu ","Zhao ","Wei ","Fan ","Qiu ","Sui ","Yang ","Lie ","Zhu ","Jie ","Gao ","Gua ","Bao ","Hu ","Yun ","Xia ","[?] ","[?] ","Bian ","Gou ","Tui ","Tang ","Chao ","Shan ","N ","Bo ","Huang ","Xie ","Xi ","Wu ","Xi ","Yun ","He ","He ","Xi ","Yun ","Xiong ","Nai ","Shan ","Qiong ","Yao ","Xun ","Mi ","Lian ","Ying ","Wen ","Rong ","Oozutsu ","[?] ","Qiang ","Liu ","Xi ","Bi ","Biao ","Zong ","Lu ","Jian ","Shou ","Yi ","Lou ","Feng ","Sui ","Yi ","Tong ","Jue ","Zong ","Yun ","Hu ","Yi ","Zhi ","Ao ","Wei ","Liao ","Han ","Ou ","Re ","Jiong ","Man ","[?] ","Shang ","Cuan ","Zeng ","Jian ","Xi ","Xi ","Xi ","Yi ","Xiao ","Chi ","Huang ","Chan ","Ye ","Qian ","Ran ","Yan ","Xian ","Qiao ","Zun ","Deng ","Dun ","Shen ","Jiao ","Fen ","Si ","Liao ","Yu ","Lin ","Tong ","Shao ","Fen ","Fan ","Yan ","Xun ","Lan ","Mei ","Tang ","Yi ","Jing ","Men ","[?] ","[?] ","Ying ","Yu ","Yi ","Xue ","Lan ","Tai ","Zao ","Can ","Sui ","Xi ","Que ","Cong ","Lian ","Hui ","Zhu ","Xie ","Ling ","Wei ","Yi ","Xie ","Zhao ","Hui ","Tatsu ","Nung ","Lan ","Ru ","Xian ","Kao ","Xun ","Jin ","Chou ","Chou ","Yao " ];
|
|
|
|
},{}],106:[function(require,module,exports){
|
|
module.exports = [ "He ","Lan ","Biao ","Rong ","Li ","Mo ","Bao ","Ruo ","Lu ","La ","Ao ","Xun ","Kuang ","Shuo ","[?] ","Li ","Lu ","Jue ","Liao ","Yan ","Xi ","Xie ","Long ","Ye ","[?] ","Rang ","Yue ","Lan ","Cong ","Jue ","Tong ","Guan ","[?] ","Che ","Mi ","Tang ","Lan ","Zhu ","[?] ","Ling ","Cuan ","Yu ","Zhua ","Tsumekanmuri ","Pa ","Zheng ","Pao ","Cheng ","Yuan ","Ai ","Wei ","[?] ","Jue ","Jue ","Fu ","Ye ","Ba ","Die ","Ye ","Yao ","Zu ","Shuang ","Er ","Qiang ","Chuang ","Ge ","Zang ","Die ","Qiang ","Yong ","Qiang ","Pian ","Ban ","Pan ","Shao ","Jian ","Pai ","Du ","Chuang ","Tou ","Zha ","Bian ","Die ","Bang ","Bo ","Chuang ","You ","[?] ","Du ","Ya ","Cheng ","Niu ","Ushihen ","Pin ","Jiu ","Mou ","Tuo ","Mu ","Lao ","Ren ","Mang ","Fang ","Mao ","Mu ","Gang ","Wu ","Yan ","Ge ","Bei ","Si ","Jian ","Gu ","You ","Ge ","Sheng ","Mu ","Di ","Qian ","Quan ","Quan ","Zi ","Te ","Xi ","Mang ","Keng ","Qian ","Wu ","Gu ","Xi ","Li ","Li ","Pou ","Ji ","Gang ","Zhi ","Ben ","Quan ","Run ","Du ","Ju ","Jia ","Jian ","Feng ","Pian ","Ke ","Ju ","Kao ","Chu ","Xi ","Bei ","Luo ","Jie ","Ma ","San ","Wei ","Li ","Dun ","Tong ","[?] ","Jiang ","Ikenie ","Li ","Du ","Lie ","Pi ","Piao ","Bao ","Xi ","Chou ","Wei ","Kui ","Chou ","Quan ","Fan ","Ba ","Fan ","Qiu ","Ji ","Cai ","Chuo ","An ","Jie ","Zhuang ","Guang ","Ma ","You ","Kang ","Bo ","Hou ","Ya ","Yin ","Huan ","Zhuang ","Yun ","Kuang ","Niu ","Di ","Qing ","Zhong ","Mu ","Bei ","Pi ","Ju ","Ni ","Sheng ","Pao ","Xia ","Tuo ","Hu ","Ling ","Fei ","Pi ","Ni ","Ao ","You ","Gou ","Yue ","Ju ","Dan ","Po ","Gu ","Xian ","Ning ","Huan ","Hen ","Jiao ","He ","Zhao ","Ji ","Xun ","Shan ","Ta ","Rong ","Shou ","Tong ","Lao ","Du ","Xia ","Shi ","Hua ","Zheng ","Yu ","Sun ","Yu ","Bi ","Mang ","Xi ","Juan ","Li ","Xia ","Yin ","Suan ","Lang ","Bei ","Zhi ","Yan " ];
|
|
|
|
},{}],107:[function(require,module,exports){
|
|
module.exports = [ "Sha ","Li ","Han ","Xian ","Jing ","Pai ","Fei ","Yao ","Ba ","Qi ","Ni ","Biao ","Yin ","Lai ","Xi ","Jian ","Qiang ","Kun ","Yan ","Guo ","Zong ","Mi ","Chang ","Yi ","Zhi ","Zheng ","Ya ","Meng ","Cai ","Cu ","She ","Kari ","Cen ","Luo ","Hu ","Zong ","Ji ","Wei ","Feng ","Wo ","Yuan ","Xing ","Zhu ","Mao ","Wei ","Yuan ","Xian ","Tuan ","Ya ","Nao ","Xie ","Jia ","Hou ","Bian ","You ","You ","Mei ","Zha ","Yao ","Sun ","Bo ","Ming ","Hua ","Yuan ","Sou ","Ma ","Yuan ","Dai ","Yu ","Shi ","Hao ","[?] ","Yi ","Zhen ","Chuang ","Hao ","Man ","Jing ","Jiang ","Mu ","Zhang ","Chan ","Ao ","Ao ","Hao ","Cui ","Fen ","Jue ","Bi ","Bi ","Huang ","Pu ","Lin ","Yu ","Tong ","Yao ","Liao ","Shuo ","Xiao ","Swu ","Ton ","Xi ","Ge ","Juan ","Du ","Hui ","Kuai ","Xian ","Xie ","Ta ","Xian ","Xun ","Ning ","Pin ","Huo ","Nou ","Meng ","Lie ","Nao ","Guang ","Shou ","Lu ","Ta ","Xian ","Mi ","Rang ","Huan ","Nao ","Luo ","Xian ","Qi ","Jue ","Xuan ","Miao ","Zi ","Lu ","Lu ","Yu ","Su ","Wang ","Qiu ","Ga ","Ding ","Le ","Ba ","Ji ","Hong ","Di ","Quan ","Gan ","Jiu ","Yu ","Ji ","Yu ","Yang ","Ma ","Gong ","Wu ","Fu ","Wen ","Jie ","Ya ","Fen ","Bian ","Beng ","Yue ","Jue ","Yun ","Jue ","Wan ","Jian ","Mei ","Dan ","Pi ","Wei ","Huan ","Xian ","Qiang ","Ling ","Dai ","Yi ","An ","Ping ","Dian ","Fu ","Xuan ","Xi ","Bo ","Ci ","Gou ","Jia ","Shao ","Po ","Ci ","Ke ","Ran ","Sheng ","Shen ","Yi ","Zu ","Jia ","Min ","Shan ","Liu ","Bi ","Zhen ","Zhen ","Jue ","Fa ","Long ","Jin ","Jiao ","Jian ","Li ","Guang ","Xian ","Zhou ","Gong ","Yan ","Xiu ","Yang ","Xu ","Luo ","Su ","Zhu ","Qin ","Ken ","Xun ","Bao ","Er ","Xiang ","Yao ","Xia ","Heng ","Gui ","Chong ","Xu ","Ban ","Pei ","[?] ","Dang ","Ei ","Hun ","Wen ","E ","Cheng ","Ti ","Wu ","Wu ","Cheng ","Jun ","Mei ","Bei ","Ting ","Xian ","Chuo " ];
|
|
|
|
},{}],108:[function(require,module,exports){
|
|
module.exports = [ "Han ","Xuan ","Yan ","Qiu ","Quan ","Lang ","Li ","Xiu ","Fu ","Liu ","Ye ","Xi ","Ling ","Li ","Jin ","Lian ","Suo ","Chiisai ","[?] ","Wan ","Dian ","Pin ","Zhan ","Cui ","Min ","Yu ","Ju ","Chen ","Lai ","Wen ","Sheng ","Wei ","Dian ","Chu ","Zhuo ","Pei ","Cheng ","Hu ","Qi ","E ","Kun ","Chang ","Qi ","Beng ","Wan ","Lu ","Cong ","Guan ","Yan ","Diao ","Bei ","Lin ","Qin ","Pi ","Pa ","Que ","Zhuo ","Qin ","Fa ","[?] ","Qiong ","Du ","Jie ","Hun ","Yu ","Mao ","Mei ","Chun ","Xuan ","Ti ","Xing ","Dai ","Rou ","Min ","Zhen ","Wei ","Ruan ","Huan ","Jie ","Chuan ","Jian ","Zhuan ","Yang ","Lian ","Quan ","Xia ","Duan ","Yuan ","Ye ","Nao ","Hu ","Ying ","Yu ","Huang ","Rui ","Se ","Liu ","Shi ","Rong ","Suo ","Yao ","Wen ","Wu ","Jin ","Jin ","Ying ","Ma ","Tao ","Liu ","Tang ","Li ","Lang ","Gui ","Zhen ","Qiang ","Cuo ","Jue ","Zhao ","Yao ","Ai ","Bin ","Tu ","Chang ","Kun ","Zhuan ","Cong ","Jin ","Yi ","Cui ","Cong ","Qi ","Li ","Ying ","Suo ","Qiu ","Xuan ","Ao ","Lian ","Man ","Zhang ","Yin ","[?] ","Ying ","Zhi ","Lu ","Wu ","Deng ","Xiou ","Zeng ","Xun ","Qu ","Dang ","Lin ","Liao ","Qiong ","Su ","Huang ","Gui ","Pu ","Jing ","Fan ","Jin ","Liu ","Ji ","[?] ","Jing ","Ai ","Bi ","Can ","Qu ","Zao ","Dang ","Jiao ","Gun ","Tan ","Hui ","Huan ","Se ","Sui ","Tian ","[?] ","Yu ","Jin ","Lu ","Bin ","Shou ","Wen ","Zui ","Lan ","Xi ","Ji ","Xuan ","Ruan ","Huo ","Gai ","Lei ","Du ","Li ","Zhi ","Rou ","Li ","Zan ","Qiong ","Zhe ","Gui ","Sui ","La ","Long ","Lu ","Li ","Zan ","Lan ","Ying ","Mi ","Xiang ","Xi ","Guan ","Dao ","Zan ","Huan ","Gua ","Bo ","Die ","Bao ","Hu ","Zhi ","Piao ","Ban ","Rang ","Li ","Wa ","Dekaguramu ","Jiang ","Qian ","Fan ","Pen ","Fang ","Dan ","Weng ","Ou ","Deshiguramu ","Miriguramu ","Thon ","Hu ","Ling ","Yi ","Ping ","Ci ","Hekutogura ","Juan ","Chang ","Chi ","Sarake ","Dang ","Meng ","Pou " ];
|
|
|
|
},{}],109:[function(require,module,exports){
|
|
module.exports = [ "Zhui ","Ping ","Bian ","Zhou ","Zhen ","Senchigura ","Ci ","Ying ","Qi ","Xian ","Lou ","Di ","Ou ","Meng ","Zhuan ","Peng ","Lin ","Zeng ","Wu ","Pi ","Dan ","Weng ","Ying ","Yan ","Gan ","Dai ","Shen ","Tian ","Tian ","Han ","Chang ","Sheng ","Qing ","Sheng ","Chan ","Chan ","Rui ","Sheng ","Su ","Sen ","Yong ","Shuai ","Lu ","Fu ","Yong ","Beng ","Feng ","Ning ","Tian ","You ","Jia ","Shen ","Zha ","Dian ","Fu ","Nan ","Dian ","Ping ","Ting ","Hua ","Ting ","Quan ","Zi ","Meng ","Bi ","Qi ","Liu ","Xun ","Liu ","Chang ","Mu ","Yun ","Fan ","Fu ","Geng ","Tian ","Jie ","Jie ","Quan ","Wei ","Fu ","Tian ","Mu ","Tap ","Pan ","Jiang ","Wa ","Da ","Nan ","Liu ","Ben ","Zhen ","Chu ","Mu ","Mu ","Ce ","Cen ","Gai ","Bi ","Da ","Zhi ","Lue ","Qi ","Lue ","Pan ","Kesa ","Fan ","Hua ","Yu ","Yu ","Mu ","Jun ","Yi ","Liu ","Yu ","Die ","Chou ","Hua ","Dang ","Chuo ","Ji ","Wan ","Jiang ","Sheng ","Chang ","Tuan ","Lei ","Ji ","Cha ","Liu ","Tatamu ","Tuan ","Lin ","Jiang ","Jiang ","Chou ","Bo ","Die ","Die ","Pi ","Nie ","Dan ","Shu ","Shu ","Zhi ","Yi ","Chuang ","Nai ","Ding ","Bi ","Jie ","Liao ","Gong ","Ge ","Jiu ","Zhou ","Xia ","Shan ","Xu ","Nue ","Li ","Yang ","Chen ","You ","Ba ","Jie ","Jue ","Zhi ","Xia ","Cui ","Bi ","Yi ","Li ","Zong ","Chuang ","Feng ","Zhu ","Pao ","Pi ","Gan ","Ke ","Ci ","Xie ","Qi ","Dan ","Zhen ","Fa ","Zhi ","Teng ","Ju ","Ji ","Fei ","Qu ","Dian ","Jia ","Xian ","Cha ","Bing ","Ni ","Zheng ","Yong ","Jing ","Quan ","Chong ","Tong ","Yi ","Kai ","Wei ","Hui ","Duo ","Yang ","Chi ","Zhi ","Hen ","Ya ","Mei ","Dou ","Jing ","Xiao ","Tong ","Tu ","Mang ","Pi ","Xiao ","Suan ","Pu ","Li ","Zhi ","Cuo ","Duo ","Wu ","Sha ","Lao ","Shou ","Huan ","Xian ","Yi ","Peng ","Zhang ","Guan ","Tan ","Fei ","Ma ","Lin ","Chi ","Ji ","Dian ","An ","Chi ","Bi ","Bei ","Min ","Gu ","Dui ","E ","Wei " ];
|
|
|
|
},{}],110:[function(require,module,exports){
|
|
module.exports = [ "Yu ","Cui ","Ya ","Zhu ","Cu ","Dan ","Shen ","Zhung ","Ji ","Yu ","Hou ","Feng ","La ","Yang ","Shen ","Tu ","Yu ","Gua ","Wen ","Huan ","Ku ","Jia ","Yin ","Yi ","Lu ","Sao ","Jue ","Chi ","Xi ","Guan ","Yi ","Wen ","Ji ","Chuang ","Ban ","Lei ","Liu ","Chai ","Shou ","Nue ","Dian ","Da ","Pie ","Tan ","Zhang ","Biao ","Shen ","Cu ","Luo ","Yi ","Zong ","Chou ","Zhang ","Zhai ","Sou ","Suo ","Que ","Diao ","Lou ","Lu ","Mo ","Jin ","Yin ","Ying ","Huang ","Fu ","Liao ","Long ","Qiao ","Liu ","Lao ","Xian ","Fei ","Dan ","Yin ","He ","Yan ","Ban ","Xian ","Guan ","Guai ","Nong ","Yu ","Wei ","Yi ","Yong ","Pi ","Lei ","Li ","Shu ","Dan ","Lin ","Dian ","Lin ","Lai ","Pie ","Ji ","Chi ","Yang ","Xian ","Jie ","Zheng ","[?] ","Li ","Huo ","Lai ","Shaku ","Dian ","Xian ","Ying ","Yin ","Qu ","Yong ","Tan ","Dian ","Luo ","Luan ","Luan ","Bo ","[?] ","Gui ","Po ","Fa ","Deng ","Fa ","Bai ","Bai ","Qie ","Bi ","Zao ","Zao ","Mao ","De ","Pa ","Jie ","Huang ","Gui ","Ci ","Ling ","Gao ","Mo ","Ji ","Jiao ","Peng ","Gao ","Ai ","E ","Hao ","Han ","Bi ","Wan ","Chou ","Qian ","Xi ","Ai ","Jiong ","Hao ","Huang ","Hao ","Ze ","Cui ","Hao ","Xiao ","Ye ","Po ","Hao ","Jiao ","Ai ","Xing ","Huang ","Li ","Piao ","He ","Jiao ","Pi ","Gan ","Pao ","Zhou ","Jun ","Qiu ","Cun ","Que ","Zha ","Gu ","Jun ","Jun ","Zhou ","Zha ","Gu ","Zhan ","Du ","Min ","Qi ","Ying ","Yu ","Bei ","Zhao ","Zhong ","Pen ","He ","Ying ","He ","Yi ","Bo ","Wan ","He ","Ang ","Zhan ","Yan ","Jian ","He ","Yu ","Kui ","Fan ","Gai ","Dao ","Pan ","Fu ","Qiu ","Sheng ","Dao ","Lu ","Zhan ","Meng ","Li ","Jin ","Xu ","Jian ","Pan ","Guan ","An ","Lu ","Shu ","Zhou ","Dang ","An ","Gu ","Li ","Mu ","Cheng ","Gan ","Xu ","Mang ","Mang ","Zhi ","Qi ","Ruan ","Tian ","Xiang ","Dun ","Xin ","Xi ","Pan ","Feng ","Dun ","Min " ];
|
|
|
|
},{}],111:[function(require,module,exports){
|
|
module.exports = [ "Ming ","Sheng ","Shi ","Yun ","Mian ","Pan ","Fang ","Miao ","Dan ","Mei ","Mao ","Kan ","Xian ","Ou ","Shi ","Yang ","Zheng ","Yao ","Shen ","Huo ","Da ","Zhen ","Kuang ","Ju ","Shen ","Chi ","Sheng ","Mei ","Mo ","Zhu ","Zhen ","Zhen ","Mian ","Di ","Yuan ","Die ","Yi ","Zi ","Zi ","Chao ","Zha ","Xuan ","Bing ","Mi ","Long ","Sui ","Dong ","Mi ","Die ","Yi ","Er ","Ming ","Xuan ","Chi ","Kuang ","Juan ","Mou ","Zhen ","Tiao ","Yang ","Yan ","Mo ","Zhong ","Mai ","Zhao ","Zheng ","Mei ","Jun ","Shao ","Han ","Huan ","Di ","Cheng ","Cuo ","Juan ","E ","Wan ","Xian ","Xi ","Kun ","Lai ","Jian ","Shan ","Tian ","Hun ","Wan ","Ling ","Shi ","Qiong ","Lie ","Yai ","Jing ","Zheng ","Li ","Lai ","Sui ","Juan ","Shui ","Sui ","Du ","Bi ","Bi ","Mu ","Hun ","Ni ","Lu ","Yi ","Jie ","Cai ","Zhou ","Yu ","Hun ","Ma ","Xia ","Xing ","Xi ","Gun ","Cai ","Chun ","Jian ","Mei ","Du ","Hou ","Xuan ","Ti ","Kui ","Gao ","Rui ","Mou ","Xu ","Fa ","Wen ","Miao ","Chou ","Kui ","Mi ","Weng ","Kou ","Dang ","Chen ","Ke ","Sou ","Xia ","Qiong ","Mao ","Ming ","Man ","Shui ","Ze ","Zhang ","Yi ","Diao ","Ou ","Mo ","Shun ","Cong ","Lou ","Chi ","Man ","Piao ","Cheng ","Ji ","Meng ","[?] ","Run ","Pie ","Xi ","Qiao ","Pu ","Zhu ","Deng ","Shen ","Shun ","Liao ","Che ","Xian ","Kan ","Ye ","Xu ","Tong ","Mou ","Lin ","Kui ","Xian ","Ye ","Ai ","Hui ","Zhan ","Jian ","Gu ","Zhao ","Qu ","Wei ","Chou ","Sao ","Ning ","Xun ","Yao ","Huo ","Meng ","Mian ","Bin ","Mian ","Li ","Kuang ","Jue ","Xuan ","Mian ","Huo ","Lu ","Meng ","Long ","Guan ","Man ","Xi ","Chu ","Tang ","Kan ","Zhu ","Mao ","Jin ","Lin ","Yu ","Shuo ","Ce ","Jue ","Shi ","Yi ","Shen ","Zhi ","Hou ","Shen ","Ying ","Ju ","Zhou ","Jiao ","Cuo ","Duan ","Ai ","Jiao ","Zeng ","Huo ","Bai ","Shi ","Ding ","Qi ","Ji ","Zi ","Gan ","Wu ","Tuo ","Ku ","Qiang ","Xi ","Fan ","Kuang " ];
|
|
|
|
},{}],112:[function(require,module,exports){
|
|
module.exports = [ "Dang ","Ma ","Sha ","Dan ","Jue ","Li ","Fu ","Min ","Nuo ","Huo ","Kang ","Zhi ","Qi ","Kan ","Jie ","Fen ","E ","Ya ","Pi ","Zhe ","Yan ","Sui ","Zhuan ","Che ","Dun ","Pan ","Yan ","[?] ","Feng ","Fa ","Mo ","Zha ","Qu ","Yu ","Luo ","Tuo ","Tuo ","Di ","Zhai ","Zhen ","Ai ","Fei ","Mu ","Zhu ","Li ","Bian ","Nu ","Ping ","Peng ","Ling ","Pao ","Le ","Po ","Bo ","Po ","Shen ","Za ","Nuo ","Li ","Long ","Tong ","[?] ","Li ","Aragane ","Chu ","Keng ","Quan ","Zhu ","Kuang ","Huo ","E ","Nao ","Jia ","Lu ","Wei ","Ai ","Luo ","Ken ","Xing ","Yan ","Tong ","Peng ","Xi ","[?] ","Hong ","Shuo ","Xia ","Qiao ","[?] ","Wei ","Qiao ","[?] ","Keng ","Xiao ","Que ","Chan ","Lang ","Hong ","Yu ","Xiao ","Xia ","Mang ","Long ","Iong ","Che ","Che ","E ","Liu ","Ying ","Mang ","Que ","Yan ","Sha ","Kun ","Yu ","[?] ","Kaki ","Lu ","Chen ","Jian ","Nue ","Song ","Zhuo ","Keng ","Peng ","Yan ","Zhui ","Kong ","Ceng ","Qi ","Zong ","Qing ","Lin ","Jun ","Bo ","Ding ","Min ","Diao ","Jian ","He ","Lu ","Ai ","Sui ","Que ","Ling ","Bei ","Yin ","Dui ","Wu ","Qi ","Lun ","Wan ","Dian ","Gang ","Pei ","Qi ","Chen ","Ruan ","Yan ","Die ","Ding ","Du ","Tuo ","Jie ","Ying ","Bian ","Ke ","Bi ","Wei ","Shuo ","Zhen ","Duan ","Xia ","Dang ","Ti ","Nao ","Peng ","Jian ","Di ","Tan ","Cha ","Seki ","Qi ","[?] ","Feng ","Xuan ","Que ","Que ","Ma ","Gong ","Nian ","Su ","E ","Ci ","Liu ","Si ","Tang ","Bang ","Hua ","Pi ","Wei ","Sang ","Lei ","Cuo ","Zhen ","Xia ","Qi ","Lian ","Pan ","Wei ","Yun ","Dui ","Zhe ","Ke ","La ","[?] ","Qing ","Gun ","Zhuan ","Chan ","Qi ","Ao ","Peng ","Lu ","Lu ","Kan ","Qiang ","Chen ","Yin ","Lei ","Biao ","Qi ","Mo ","Qi ","Cui ","Zong ","Qing ","Chuo ","[?] ","Ji ","Shan ","Lao ","Qu ","Zeng ","Deng ","Jian ","Xi ","Lin ","Ding ","Dian ","Huang ","Pan ","Za ","Qiao ","Di ","Li " ];
|
|
|
|
},{}],113:[function(require,module,exports){
|
|
module.exports = [ "Tani ","Jiao ","[?] ","Zhang ","Qiao ","Dun ","Xian ","Yu ","Zhui ","He ","Huo ","Zhai ","Lei ","Ke ","Chu ","Ji ","Que ","Dang ","Yi ","Jiang ","Pi ","Pi ","Yu ","Pin ","Qi ","Ai ","Kai ","Jian ","Yu ","Ruan ","Meng ","Pao ","Ci ","[?] ","[?] ","Mie ","Ca ","Xian ","Kuang ","Lei ","Lei ","Zhi ","Li ","Li ","Fan ","Que ","Pao ","Ying ","Li ","Long ","Long ","Mo ","Bo ","Shuang ","Guan ","Lan ","Zan ","Yan ","Shi ","Shi ","Li ","Reng ","She ","Yue ","Si ","Qi ","Ta ","Ma ","Xie ","Xian ","Xian ","Zhi ","Qi ","Zhi ","Beng ","Dui ","Zhong ","[?] ","Yi ","Shi ","You ","Zhi ","Tiao ","Fu ","Fu ","Mi ","Zu ","Zhi ","Suan ","Mei ","Zuo ","Qu ","Hu ","Zhu ","Shen ","Sui ","Ci ","Chai ","Mi ","Lu ","Yu ","Xiang ","Wu ","Tiao ","Piao ","Zhu ","Gui ","Xia ","Zhi ","Ji ","Gao ","Zhen ","Gao ","Shui ","Jin ","Chen ","Gai ","Kun ","Di ","Dao ","Huo ","Tao ","Qi ","Gu ","Guan ","Zui ","Ling ","Lu ","Bing ","Jin ","Dao ","Zhi ","Lu ","Shan ","Bei ","Zhe ","Hui ","You ","Xi ","Yin ","Zi ","Huo ","Zhen ","Fu ","Yuan ","Wu ","Xian ","Yang ","Ti ","Yi ","Mei ","Si ","Di ","[?] ","Zhuo ","Zhen ","Yong ","Ji ","Gao ","Tang ","Si ","Ma ","Ta ","[?] ","Xuan ","Qi ","Yu ","Xi ","Ji ","Si ","Chan ","Tan ","Kuai ","Sui ","Li ","Nong ","Ni ","Dao ","Li ","Rang ","Yue ","Ti ","Zan ","Lei ","Rou ","Yu ","Yu ","Chi ","Xie ","Qin ","He ","Tu ","Xiu ","Si ","Ren ","Tu ","Zi ","Cha ","Gan ","Yi ","Xian ","Bing ","Nian ","Qiu ","Qiu ","Chong ","Fen ","Hao ","Yun ","Ke ","Miao ","Zhi ","Geng ","Bi ","Zhi ","Yu ","Mi ","Ku ","Ban ","Pi ","Ni ","Li ","You ","Zu ","Pi ","Ba ","Ling ","Mo ","Cheng ","Nian ","Qin ","Yang ","Zuo ","Zhi ","Zhi ","Shu ","Ju ","Zi ","Huo ","Ji ","Cheng ","Tong ","Zhi ","Huo ","He ","Yin ","Zi ","Zhi ","Jie ","Ren ","Du ","Yi ","Zhu ","Hui ","Nong ","Fu " ];
|
|
|
|
},{}],114:[function(require,module,exports){
|
|
module.exports = [ "Xi ","Kao ","Lang ","Fu ","Ze ","Shui ","Lu ","Kun ","Gan ","Geng ","Ti ","Cheng ","Tu ","Shao ","Shui ","Ya ","Lun ","Lu ","Gu ","Zuo ","Ren ","Zhun ","Bang ","Bai ","Ji ","Zhi ","Zhi ","Kun ","Leng ","Peng ","Ke ","Bing ","Chou ","Zu ","Yu ","Su ","Lue ","[?] ","Yi ","Xi ","Bian ","Ji ","Fu ","Bi ","Nuo ","Jie ","Zhong ","Zong ","Xu ","Cheng ","Dao ","Wen ","Lian ","Zi ","Yu ","Ji ","Xu ","Zhen ","Zhi ","Dao ","Jia ","Ji ","Gao ","Gao ","Gu ","Rong ","Sui ","You ","Ji ","Kang ","Mu ","Shan ","Men ","Zhi ","Ji ","Lu ","Su ","Ji ","Ying ","Wen ","Qiu ","Se ","[?] ","Yi ","Huang ","Qie ","Ji ","Sui ","Xiao ","Pu ","Jiao ","Zhuo ","Tong ","Sai ","Lu ","Sui ","Nong ","Se ","Hui ","Rang ","Nuo ","Yu ","Bin ","Ji ","Tui ","Wen ","Cheng ","Huo ","Gong ","Lu ","Biao ","[?] ","Rang ","Zhuo ","Li ","Zan ","Xue ","Wa ","Jiu ","Qiong ","Xi ","Qiong ","Kong ","Yu ","Sen ","Jing ","Yao ","Chuan ","Zhun ","Tu ","Lao ","Qie ","Zhai ","Yao ","Bian ","Bao ","Yao ","Bing ","Wa ","Zhu ","Jiao ","Qiao ","Diao ","Wu ","Gui ","Yao ","Zhi ","Chuang ","Yao ","Tiao ","Jiao ","Chuang ","Jiong ","Xiao ","Cheng ","Kou ","Cuan ","Wo ","Dan ","Ku ","Ke ","Zhui ","Xu ","Su ","Guan ","Kui ","Dou ","[?] ","Yin ","Wo ","Wa ","Ya ","Yu ","Ju ","Qiong ","Yao ","Yao ","Tiao ","Chao ","Yu ","Tian ","Diao ","Ju ","Liao ","Xi ","Wu ","Kui ","Chuang ","Zhao ","[?] ","Kuan ","Long ","Cheng ","Cui ","Piao ","Zao ","Cuan ","Qiao ","Qiong ","Dou ","Zao ","Long ","Qie ","Li ","Chu ","Shi ","Fou ","Qian ","Chu ","Hong ","Qi ","Qian ","Gong ","Shi ","Shu ","Miao ","Ju ","Zhan ","Zhu ","Ling ","Long ","Bing ","Jing ","Jing ","Zhang ","Yi ","Si ","Jun ","Hong ","Tong ","Song ","Jing ","Diao ","Yi ","Shu ","Jing ","Qu ","Jie ","Ping ","Duan ","Shao ","Zhuan ","Ceng ","Deng ","Cui ","Huai ","Jing ","Kan ","Jing ","Zhu ","Zhu ","Le ","Peng ","Yu ","Chi ","Gan " ];
|
|
|
|
},{}],115:[function(require,module,exports){
|
|
module.exports = [ "Mang ","Zhu ","Utsubo ","Du ","Ji ","Xiao ","Ba ","Suan ","Ji ","Zhen ","Zhao ","Sun ","Ya ","Zhui ","Yuan ","Hu ","Gang ","Xiao ","Cen ","Pi ","Bi ","Jian ","Yi ","Dong ","Shan ","Sheng ","Xia ","Di ","Zhu ","Na ","Chi ","Gu ","Li ","Qie ","Min ","Bao ","Tiao ","Si ","Fu ","Ce ","Ben ","Pei ","Da ","Zi ","Di ","Ling ","Ze ","Nu ","Fu ","Gou ","Fan ","Jia ","Ge ","Fan ","Shi ","Mao ","Po ","Sey ","Jian ","Qiong ","Long ","Souke ","Bian ","Luo ","Gui ","Qu ","Chi ","Yin ","Yao ","Xian ","Bi ","Qiong ","Gua ","Deng ","Jiao ","Jin ","Quan ","Sun ","Ru ","Fa ","Kuang ","Zhu ","Tong ","Ji ","Da ","Xing ","Ce ","Zhong ","Kou ","Lai ","Bi ","Shai ","Dang ","Zheng ","Ce ","Fu ","Yun ","Tu ","Pa ","Li ","Lang ","Ju ","Guan ","Jian ","Han ","Tong ","Xia ","Zhi ","Cheng ","Suan ","Shi ","Zhu ","Zuo ","Xiao ","Shao ","Ting ","Ce ","Yan ","Gao ","Kuai ","Gan ","Chou ","Kago ","Gang ","Yun ","O ","Qian ","Xiao ","Jian ","Pu ","Lai ","Zou ","Bi ","Bi ","Bi ","Ge ","Chi ","Guai ","Yu ","Jian ","Zhao ","Gu ","Chi ","Zheng ","Jing ","Sha ","Zhou ","Lu ","Bo ","Ji ","Lin ","Suan ","Jun ","Fu ","Zha ","Gu ","Kong ","Qian ","Quan ","Jun ","Chui ","Guan ","Yuan ","Ce ","Ju ","Bo ","Ze ","Qie ","Tuo ","Luo ","Dan ","Xiao ","Ruo ","Jian ","Xuan ","Bian ","Sun ","Xiang ","Xian ","Ping ","Zhen ","Sheng ","Hu ","Shi ","Zhu ","Yue ","Chun ","Lu ","Wu ","Dong ","Xiao ","Ji ","Jie ","Huang ","Xing ","Mei ","Fan ","Chui ","Zhuan ","Pian ","Feng ","Zhu ","Hong ","Qie ","Hou ","Qiu ","Miao ","Qian ","[?] ","Kui ","Sik ","Lou ","Yun ","He ","Tang ","Yue ","Chou ","Gao ","Fei ","Ruo ","Zheng ","Gou ","Nie ","Qian ","Xiao ","Cuan ","Gong ","Pang ","Du ","Li ","Bi ","Zhuo ","Chu ","Shai ","Chi ","Zhu ","Qiang ","Long ","Lan ","Jian ","Bu ","Li ","Hui ","Bi ","Di ","Cong ","Yan ","Peng ","Sen ","Zhuan ","Pai ","Piao ","Dou ","Yu ","Mie ","Zhuan " ];
|
|
|
|
},{}],116:[function(require,module,exports){
|
|
module.exports = [ "Ze ","Xi ","Guo ","Yi ","Hu ","Chan ","Kou ","Cu ","Ping ","Chou ","Ji ","Gui ","Su ","Lou ","Zha ","Lu ","Nian ","Suo ","Cuan ","Sasara ","Suo ","Le ","Duan ","Yana ","Xiao ","Bo ","Mi ","Si ","Dang ","Liao ","Dan ","Dian ","Fu ","Jian ","Min ","Kui ","Dai ","Qiao ","Deng ","Huang ","Sun ","Lao ","Zan ","Xiao ","Du ","Shi ","Zan ","[?] ","Pai ","Hata ","Pai ","Gan ","Ju ","Du ","Lu ","Yan ","Bo ","Dang ","Sai ","Ke ","Long ","Qian ","Lian ","Bo ","Zhou ","Lai ","[?] ","Lan ","Kui ","Yu ","Yue ","Hao ","Zhen ","Tai ","Ti ","Mi ","Chou ","Ji ","[?] ","Hata ","Teng ","Zhuan ","Zhou ","Fan ","Sou ","Zhou ","Kuji ","Zhuo ","Teng ","Lu ","Lu ","Jian ","Tuo ","Ying ","Yu ","Lai ","Long ","Shinshi ","Lian ","Lan ","Qian ","Yue ","Zhong ","Qu ","Lian ","Bian ","Duan ","Zuan ","Li ","Si ","Luo ","Ying ","Yue ","Zhuo ","Xu ","Mi ","Di ","Fan ","Shen ","Zhe ","Shen ","Nu ","Xie ","Lei ","Xian ","Zi ","Ni ","Cun ","[?] ","Qian ","Kume ","Bi ","Ban ","Wu ","Sha ","Kang ","Rou ","Fen ","Bi ","Cui ","[?] ","Li ","Chi ","Nukamiso ","Ro ","Ba ","Li ","Gan ","Ju ","Po ","Mo ","Cu ","Nian ","Zhou ","Li ","Su ","Tiao ","Li ","Qi ","Su ","Hong ","Tong ","Zi ","Ce ","Yue ","Zhou ","Lin ","Zhuang ","Bai ","[?] ","Fen ","Ji ","[?] ","Sukumo ","Liang ","Xian ","Fu ","Liang ","Can ","Geng ","Li ","Yue ","Lu ","Ju ","Qi ","Cui ","Bai ","Zhang ","Lin ","Zong ","Jing ","Guo ","Kouji ","San ","San ","Tang ","Bian ","Rou ","Mian ","Hou ","Xu ","Zong ","Hu ","Jian ","Zan ","Ci ","Li ","Xie ","Fu ","Ni ","Bei ","Gu ","Xiu ","Gao ","Tang ","Qiu ","Sukumo ","Cao ","Zhuang ","Tang ","Mi ","San ","Fen ","Zao ","Kang ","Jiang ","Mo ","San ","San ","Nuo ","Xi ","Liang ","Jiang ","Kuai ","Bo ","Huan ","[?] ","Zong ","Xian ","Nuo ","Tuan ","Nie ","Li ","Zuo ","Di ","Nie ","Tiao ","Lan ","Mi ","Jiao ","Jiu ","Xi ","Gong ","Zheng ","Jiu ","You " ];
|
|
|
|
},{}],117:[function(require,module,exports){
|
|
module.exports = [ "Ji ","Cha ","Zhou ","Xun ","Yue ","Hong ","Yu ","He ","Wan ","Ren ","Wen ","Wen ","Qiu ","Na ","Zi ","Tou ","Niu ","Fou ","Jie ","Shu ","Chun ","Pi ","Yin ","Sha ","Hong ","Zhi ","Ji ","Fen ","Yun ","Ren ","Dan ","Jin ","Su ","Fang ","Suo ","Cui ","Jiu ","Zha ","Kinu ","Jin ","Fu ","Zhi ","Ci ","Zi ","Chou ","Hong ","Zha ","Lei ","Xi ","Fu ","Xie ","Shen ","Bei ","Zhu ","Qu ","Ling ","Zhu ","Shao ","Gan ","Yang ","Fu ","Tuo ","Zhen ","Dai ","Zhuo ","Shi ","Zhong ","Xian ","Zu ","Jiong ","Ban ","Ju ","Mo ","Shu ","Zui ","Wata ","Jing ","Ren ","Heng ","Xie ","Jie ","Zhu ","Chou ","Gua ","Bai ","Jue ","Kuang ","Hu ","Ci ","Geng ","Geng ","Tao ","Xie ","Ku ","Jiao ","Quan ","Gai ","Luo ","Xuan ","Bing ","Xian ","Fu ","Gei ","Tong ","Rong ","Tiao ","Yin ","Lei ","Xie ","Quan ","Xu ","Lun ","Die ","Tong ","Si ","Jiang ","Xiang ","Hui ","Jue ","Zhi ","Jian ","Juan ","Chi ","Mian ","Zhen ","Lu ","Cheng ","Qiu ","Shu ","Bang ","Tong ","Xiao ","Wan ","Qin ","Geng ","Xiu ","Ti ","Xiu ","Xie ","Hong ","Xi ","Fu ","Ting ","Sui ","Dui ","Kun ","Fu ","Jing ","Hu ","Zhi ","Yan ","Jiong ","Feng ","Ji ","Sok ","Kase ","Zong ","Lin ","Duo ","Li ","Lu ","Liang ","Chou ","Quan ","Shao ","Qi ","Qi ","Zhun ","Qi ","Wan ","Qian ","Xian ","Shou ","Wei ","Qi ","Tao ","Wan ","Gang ","Wang ","Beng ","Zhui ","Cai ","Guo ","Cui ","Lun ","Liu ","Qi ","Zhan ","Bei ","Chuo ","Ling ","Mian ","Qi ","Qie ","Tan ","Zong ","Gun ","Zou ","Yi ","Zi ","Xing ","Liang ","Jin ","Fei ","Rui ","Min ","Yu ","Zong ","Fan ","Lu ","Xu ","Yingl ","Zhang ","Kasuri ","Xu ","Xiang ","Jian ","Ke ","Xian ","Ruan ","Mian ","Qi ","Duan ","Zhong ","Di ","Min ","Miao ","Yuan ","Xie ","Bao ","Si ","Qiu ","Bian ","Huan ","Geng ","Cong ","Mian ","Wei ","Fu ","Wei ","Yu ","Gou ","Miao ","Xie ","Lian ","Zong ","Bian ","Yun ","Yin ","Ti ","Gua ","Zhi ","Yun ","Cheng ","Chan ","Dai " ];
|
|
|
|
},{}],118:[function(require,module,exports){
|
|
module.exports = [ "Xia ","Yuan ","Zong ","Xu ","Nawa ","Odoshi ","Geng ","Sen ","Ying ","Jin ","Yi ","Zhui ","Ni ","Bang ","Gu ","Pan ","Zhou ","Jian ","Cuo ","Quan ","Shuang ","Yun ","Xia ","Shuai ","Xi ","Rong ","Tao ","Fu ","Yun ","Zhen ","Gao ","Ru ","Hu ","Zai ","Teng ","Xian ","Su ","Zhen ","Zong ","Tao ","Horo ","Cai ","Bi ","Feng ","Cu ","Li ","Suo ","Yin ","Xi ","Zong ","Lei ","Zhuan ","Qian ","Man ","Zhi ","Lu ","Mo ","Piao ","Lian ","Mi ","Xuan ","Zong ","Ji ","Shan ","Sui ","Fan ","Shuai ","Beng ","Yi ","Sao ","Mou ","Zhou ","Qiang ","Hun ","Sem ","Xi ","Jung ","Xiu ","Ran ","Xuan ","Hui ","Qiao ","Zeng ","Zuo ","Zhi ","Shan ","San ","Lin ","Yu ","Fan ","Liao ","Chuo ","Zun ","Jian ","Rao ","Chan ","Rui ","Xiu ","Hui ","Hua ","Zuan ","Xi ","Qiang ","Un ","Da ","Sheng ","Hui ","Xi ","Se ","Jian ","Jiang ","Huan ","Zao ","Cong ","Jie ","Jiao ","Bo ","Chan ","Yi ","Nao ","Sui ","Yi ","Shai ","Xu ","Ji ","Bin ","Qian ","Lan ","Pu ","Xun ","Zuan ","Qi ","Peng ","Li ","Mo ","Lei ","Xie ","Zuan ","Kuang ","You ","Xu ","Lei ","Xian ","Chan ","Kou ","Lu ","Chan ","Ying ","Cai ","Xiang ","Xian ","Zui ","Zuan ","Luo ","Xi ","Dao ","Lan ","Lei ","Lian ","Si ","Jiu ","Yu ","Hong ","Zhou ","Xian ","He ","Yue ","Ji ","Wan ","Kuang ","Ji ","Ren ","Wei ","Yun ","Hong ","Chun ","Pi ","Sha ","Gang ","Na ","Ren ","Zong ","Lun ","Fen ","Zhi ","Wen ","Fang ","Zhu ","Yin ","Niu ","Shu ","Xian ","Gan ","Xie ","Fu ","Lian ","Zu ","Shen ","Xi ","Zhi ","Zhong ","Zhou ","Ban ","Fu ","Zhuo ","Shao ","Yi ","Jing ","Dai ","Bang ","Rong ","Jie ","Ku ","Rao ","Die ","Heng ","Hui ","Gei ","Xuan ","Jiang ","Luo ","Jue ","Jiao ","Tong ","Geng ","Xiao ","Juan ","Xiu ","Xi ","Sui ","Tao ","Ji ","Ti ","Ji ","Xu ","Ling ","[?] ","Xu ","Qi ","Fei ","Chuo ","Zhang ","Gun ","Sheng ","Wei ","Mian ","Shou ","Beng ","Chou ","Tao ","Liu ","Quan ","Zong ","Zhan ","Wan ","Lu " ];
|
|
|
|
},{}],119:[function(require,module,exports){
|
|
module.exports = [ "Zhui ","Zi ","Ke ","Xiang ","Jian ","Mian ","Lan ","Ti ","Miao ","Qi ","Yun ","Hui ","Si ","Duo ","Duan ","Bian ","Xian ","Gou ","Zhui ","Huan ","Di ","Lu ","Bian ","Min ","Yuan ","Jin ","Fu ","Ru ","Zhen ","Feng ","Shuai ","Gao ","Chan ","Li ","Yi ","Jian ","Bin ","Piao ","Man ","Lei ","Ying ","Suo ","Mou ","Sao ","Xie ","Liao ","Shan ","Zeng ","Jiang ","Qian ","Zao ","Huan ","Jiao ","Zuan ","Fou ","Xie ","Gang ","Fou ","Que ","Fou ","Kaakeru ","Bo ","Ping ","Hou ","[?] ","Gang ","Ying ","Ying ","Qing ","Xia ","Guan ","Zun ","Tan ","Chang ","Qi ","Weng ","Ying ","Lei ","Tan ","Lu ","Guan ","Wang ","Wang ","Gang ","Wang ","Han ","[?] ","Luo ","Fu ","Mi ","Fa ","Gu ","Zhu ","Ju ","Mao ","Gu ","Min ","Gang ","Ba ","Gua ","Ti ","Juan ","Fu ","Lin ","Yan ","Zhao ","Zui ","Gua ","Zhuo ","Yu ","Zhi ","An ","Fa ","Nan ","Shu ","Si ","Pi ","Ma ","Liu ","Ba ","Fa ","Li ","Chao ","Wei ","Bi ","Ji ","Zeng ","Tong ","Liu ","Ji ","Juan ","Mi ","Zhao ","Luo ","Pi ","Ji ","Ji ","Luan ","Yang ","Mie ","Qiang ","Ta ","Mei ","Yang ","You ","You ","Fen ","Ba ","Gao ","Yang ","Gu ","Qiang ","Zang ","Gao ","Ling ","Yi ","Zhu ","Di ","Xiu ","Qian ","Yi ","Xian ","Rong ","Qun ","Qun ","Qian ","Huan ","Zui ","Xian ","Yi ","Yashinau ","Qiang ","Xian ","Yu ","Geng ","Jie ","Tang ","Yuan ","Xi ","Fan ","Shan ","Fen ","Shan ","Lian ","Lei ","Geng ","Nou ","Qiang ","Chan ","Yu ","Gong ","Yi ","Chong ","Weng ","Fen ","Hong ","Chi ","Chi ","Cui ","Fu ","Xia ","Pen ","Yi ","La ","Yi ","Pi ","Ling ","Liu ","Zhi ","Qu ","Xi ","Xie ","Xiang ","Xi ","Xi ","Qi ","Qiao ","Hui ","Hui ","Xiao ","Se ","Hong ","Jiang ","Di ","Cui ","Fei ","Tao ","Sha ","Chi ","Zhu ","Jian ","Xuan ","Shi ","Pian ","Zong ","Wan ","Hui ","Hou ","He ","He ","Han ","Ao ","Piao ","Yi ","Lian ","Qu ","[?] ","Lin ","Pen ","Qiao ","Ao ","Fan ","Yi ","Hui ","Xuan ","Dao " ];
|
|
|
|
},{}],120:[function(require,module,exports){
|
|
module.exports = [ "Yao ","Lao ","[?] ","Kao ","Mao ","Zhe ","Qi ","Gou ","Gou ","Gou ","Die ","Die ","Er ","Shua ","Ruan ","Er ","Nai ","Zhuan ","Lei ","Ting ","Zi ","Geng ","Chao ","Hao ","Yun ","Pa ","Pi ","Chi ","Si ","Chu ","Jia ","Ju ","He ","Chu ","Lao ","Lun ","Ji ","Tang ","Ou ","Lou ","Nou ","Gou ","Pang ","Ze ","Lou ","Ji ","Lao ","Huo ","You ","Mo ","Huai ","Er ","Zhe ","Ting ","Ye ","Da ","Song ","Qin ","Yun ","Chi ","Dan ","Dan ","Hong ","Geng ","Zhi ","[?] ","Nie ","Dan ","Zhen ","Che ","Ling ","Zheng ","You ","Wa ","Liao ","Long ","Zhi ","Ning ","Tiao ","Er ","Ya ","Die ","Gua ","[?] ","Lian ","Hao ","Sheng ","Lie ","Pin ","Jing ","Ju ","Bi ","Di ","Guo ","Wen ","Xu ","Ping ","Cong ","Shikato ","[?] ","Ting ","Yu ","Cong ","Kui ","Tsuraneru ","Kui ","Cong ","Lian ","Weng ","Kui ","Lian ","Lian ","Cong ","Ao ","Sheng ","Song ","Ting ","Kui ","Nie ","Zhi ","Dan ","Ning ","Qie ","Ji ","Ting ","Ting ","Long ","Yu ","Yu ","Zhao ","Si ","Su ","Yi ","Su ","Si ","Zhao ","Zhao ","Rou ","Yi ","Le ","Ji ","Qiu ","Ken ","Cao ","Ge ","Di ","Huan ","Huang ","Yi ","Ren ","Xiao ","Ru ","Zhou ","Yuan ","Du ","Gang ","Rong ","Gan ","Cha ","Wo ","Chang ","Gu ","Zhi ","Han ","Fu ","Fei ","Fen ","Pei ","Pang ","Jian ","Fang ","Zhun ","You ","Na ","Hang ","Ken ","Ran ","Gong ","Yu ","Wen ","Yao ","Jin ","Pi ","Qian ","Xi ","Xi ","Fei ","Ken ","Jing ","Tai ","Shen ","Zhong ","Zhang ","Xie ","Shen ","Wei ","Zhou ","Die ","Dan ","Fei ","Ba ","Bo ","Qu ","Tian ","Bei ","Gua ","Tai ","Zi ","Ku ","Zhi ","Ni ","Ping ","Zi ","Fu ","Pang ","Zhen ","Xian ","Zuo ","Pei ","Jia ","Sheng ","Zhi ","Bao ","Mu ","Qu ","Hu ","Ke ","Yi ","Yin ","Xu ","Yang ","Long ","Dong ","Ka ","Lu ","Jing ","Nu ","Yan ","Pang ","Kua ","Yi ","Guang ","Gai ","Ge ","Dong ","Zhi ","Xiao ","Xiong ","Xiong ","Er ","E ","Xing ","Pian ","Neng ","Zi ","Gui " ];
|
|
|
|
},{}],121:[function(require,module,exports){
|
|
module.exports = [ "Cheng ","Tiao ","Zhi ","Cui ","Mei ","Xie ","Cui ","Xie ","Mo ","Mai ","Ji ","Obiyaakasu ","[?] ","Kuai ","Sa ","Zang ","Qi ","Nao ","Mi ","Nong ","Luan ","Wan ","Bo ","Wen ","Guan ","Qiu ","Jiao ","Jing ","Rou ","Heng ","Cuo ","Lie ","Shan ","Ting ","Mei ","Chun ","Shen ","Xie ","De ","Zui ","Cu ","Xiu ","Xin ","Tuo ","Pao ","Cheng ","Nei ","Fu ","Dou ","Tuo ","Niao ","Noy ","Pi ","Gu ","Gua ","Li ","Lian ","Zhang ","Cui ","Jie ","Liang ","Zhou ","Pi ","Biao ","Lun ","Pian ","Guo ","Kui ","Chui ","Dan ","Tian ","Nei ","Jing ","Jie ","La ","Yi ","An ","Ren ","Shen ","Chuo ","Fu ","Fu ","Ju ","Fei ","Qiang ","Wan ","Dong ","Pi ","Guo ","Zong ","Ding ","Wu ","Mei ","Ruan ","Zhuan ","Zhi ","Cou ","Gua ","Ou ","Di ","An ","Xing ","Nao ","Yu ","Chuan ","Nan ","Yun ","Zhong ","Rou ","E ","Sai ","Tu ","Yao ","Jian ","Wei ","Jiao ","Yu ","Jia ","Duan ","Bi ","Chang ","Fu ","Xian ","Ni ","Mian ","Wa ","Teng ","Tui ","Bang ","Qian ","Lu ","Wa ","Sou ","Tang ","Su ","Zhui ","Ge ","Yi ","Bo ","Liao ","Ji ","Pi ","Xie ","Gao ","Lu ","Bin ","Ou ","Chang ","Lu ","Guo ","Pang ","Chuai ","Piao ","Jiang ","Fu ","Tang ","Mo ","Xi ","Zhuan ","Lu ","Jiao ","Ying ","Lu ","Zhi ","Tara ","Chun ","Lian ","Tong ","Peng ","Ni ","Zha ","Liao ","Cui ","Gui ","Xiao ","Teng ","Fan ","Zhi ","Jiao ","Shan ","Wu ","Cui ","Run ","Xiang ","Sui ","Fen ","Ying ","Tan ","Zhua ","Dan ","Kuai ","Nong ","Tun ","Lian ","Bi ","Yong ","Jue ","Chu ","Yi ","Juan ","La ","Lian ","Sao ","Tun ","Gu ","Qi ","Cui ","Bin ","Xun ","Ru ","Huo ","Zang ","Xian ","Biao ","Xing ","Kuan ","La ","Yan ","Lu ","Huo ","Zang ","Luo ","Qu ","Zang ","Luan ","Ni ","Zang ","Chen ","Qian ","Wo ","Guang ","Zang ","Lin ","Guang ","Zi ","Jiao ","Nie ","Chou ","Ji ","Gao ","Chou ","Mian ","Nie ","Zhi ","Zhi ","Ge ","Jian ","Die ","Zhi ","Xiu ","Tai ","Zhen ","Jiu ","Xian ","Yu ","Cha " ];
|
|
|
|
},{}],122:[function(require,module,exports){
|
|
module.exports = [ "Yao ","Yu ","Chong ","Xi ","Xi ","Jiu ","Yu ","Yu ","Xing ","Ju ","Jiu ","Xin ","She ","She ","Yadoru ","Jiu ","Shi ","Tan ","Shu ","Shi ","Tian ","Dan ","Pu ","Pu ","Guan ","Hua ","Tan ","Chuan ","Shun ","Xia ","Wu ","Zhou ","Dao ","Gang ","Shan ","Yi ","[?] ","Pa ","Tai ","Fan ","Ban ","Chuan ","Hang ","Fang ","Ban ","Que ","Hesaki ","Zhong ","Jian ","Cang ","Ling ","Zhu ","Ze ","Duo ","Bo ","Xian ","Ge ","Chuan ","Jia ","Lu ","Hong ","Pang ","Xi ","[?] ","Fu ","Zao ","Feng ","Li ","Shao ","Yu ","Lang ","Ting ","[?] ","Wei ","Bo ","Meng ","Nian ","Ju ","Huang ","Shou ","Zong ","Bian ","Mao ","Die ","[?] ","Bang ","Cha ","Yi ","Sao ","Cang ","Cao ","Lou ","Dai ","Sori ","Yao ","Tong ","Yofune ","Dang ","Tan ","Lu ","Yi ","Jie ","Jian ","Huo ","Meng ","Qi ","Lu ","Lu ","Chan ","Shuang ","Gen ","Liang ","Jian ","Jian ","Se ","Yan ","Fu ","Ping ","Yan ","Yan ","Cao ","Cao ","Yi ","Le ","Ting ","Qiu ","Ai ","Nai ","Tiao ","Jiao ","Jie ","Peng ","Wan ","Yi ","Chai ","Mian ","Mie ","Gan ","Qian ","Yu ","Yu ","Shuo ","Qiong ","Tu ","Xia ","Qi ","Mang ","Zi ","Hui ","Sui ","Zhi ","Xiang ","Bi ","Fu ","Tun ","Wei ","Wu ","Zhi ","Qi ","Shan ","Wen ","Qian ","Ren ","Fou ","Kou ","Jie ","Lu ","Xu ","Ji ","Qin ","Qi ","Yuan ","Fen ","Ba ","Rui ","Xin ","Ji ","Hua ","Hua ","Fang ","Wu ","Jue ","Gou ","Zhi ","Yun ","Qin ","Ao ","Chu ","Mao ","Ya ","Fei ","Reng ","Hang ","Cong ","Yin ","You ","Bian ","Yi ","Susa ","Wei ","Li ","Pi ","E ","Xian ","Chang ","Cang ","Meng ","Su ","Yi ","Yuan ","Ran ","Ling ","Tai ","Tiao ","Di ","Miao ","Qiong ","Li ","Yong ","Ke ","Mu ","Pei ","Bao ","Gou ","Min ","Yi ","Yi ","Ju ","Pi ","Ruo ","Ku ","Zhu ","Ni ","Bo ","Bing ","Shan ","Qiu ","Yao ","Xian ","Ben ","Hong ","Ying ","Zha ","Dong ","Ju ","Die ","Nie ","Gan ","Hu ","Ping ","Mei ","Fu ","Sheng ","Gu ","Bi ","Wei " ];
|
|
|
|
},{}],123:[function(require,module,exports){
|
|
module.exports = [ "Fu ","Zhuo ","Mao ","Fan ","Qie ","Mao ","Mao ","Ba ","Zi ","Mo ","Zi ","Di ","Chi ","Ji ","Jing ","Long ","[?] ","Niao ","[?] ","Xue ","Ying ","Qiong ","Ge ","Ming ","Li ","Rong ","Yin ","Gen ","Qian ","Chai ","Chen ","Yu ","Xiu ","Zi ","Lie ","Wu ","Ji ","Kui ","Ce ","Chong ","Ci ","Gou ","Guang ","Mang ","Chi ","Jiao ","Jiao ","Fu ","Yu ","Zhu ","Zi ","Jiang ","Hui ","Yin ","Cha ","Fa ","Rong ","Ru ","Chong ","Mang ","Tong ","Zhong ","[?] ","Zhu ","Xun ","Huan ","Kua ","Quan ","Gai ","Da ","Jing ","Xing ","Quan ","Cao ","Jing ","Er ","An ","Shou ","Chi ","Ren ","Jian ","Ti ","Huang ","Ping ","Li ","Jin ","Lao ","Shu ","Zhuang ","Da ","Jia ","Rao ","Bi ","Ze ","Qiao ","Hui ","Qi ","Dang ","[?] ","Rong ","Hun ","Ying ","Luo ","Ying ","Xun ","Jin ","Sun ","Yin ","Mai ","Hong ","Zhou ","Yao ","Du ","Wei ","Chu ","Dou ","Fu ","Ren ","Yin ","He ","Bi ","Bu ","Yun ","Di ","Tu ","Sui ","Sui ","Cheng ","Chen ","Wu ","Bie ","Xi ","Geng ","Li ","Fu ","Zhu ","Mo ","Li ","Zhuang ","Ji ","Duo ","Qiu ","Sha ","Suo ","Chen ","Feng ","Ju ","Mei ","Meng ","Xing ","Jing ","Che ","Xin ","Jun ","Yan ","Ting ","Diao ","Cuo ","Wan ","Han ","You ","Cuo ","Jia ","Wang ","You ","Niu ","Shao ","Xian ","Lang ","Fu ","E ","Mo ","Wen ","Jie ","Nan ","Mu ","Kan ","Lai ","Lian ","Shi ","Wo ","Usagi ","Lian ","Huo ","You ","Ying ","Ying ","Nuc ","Chun ","Mang ","Mang ","Ci ","Wan ","Jing ","Di ","Qu ","Dong ","Jian ","Zou ","Gu ","La ","Lu ","Ju ","Wei ","Jun ","Nie ","Kun ","He ","Pu ","Zi ","Gao ","Guo ","Fu ","Lun ","Chang ","Chou ","Song ","Chui ","Zhan ","Men ","Cai ","Ba ","Li ","Tu ","Bo ","Han ","Bao ","Qin ","Juan ","Xi ","Qin ","Di ","Jie ","Pu ","Dang ","Jin ","Zhao ","Tai ","Geng ","Hua ","Gu ","Ling ","Fei ","Jin ","An ","Wang ","Beng ","Zhou ","Yan ","Ju ","Jian ","Lin ","Tan ","Shu ","Tian ","Dao " ];
|
|
|
|
},{}],124:[function(require,module,exports){
|
|
module.exports = [ "Hu ","Qi ","He ","Cui ","Tao ","Chun ","Bei ","Chang ","Huan ","Fei ","Lai ","Qi ","Meng ","Ping ","Wei ","Dan ","Sha ","Huan ","Yan ","Yi ","Tiao ","Qi ","Wan ","Ce ","Nai ","Kutabireru ","Tuo ","Jiu ","Tie ","Luo ","[?] ","[?] ","Meng ","[?] ","Yaji ","[?] ","Ying ","Ying ","Ying ","Xiao ","Sa ","Qiu ","Ke ","Xiang ","Wan ","Yu ","Yu ","Fu ","Lian ","Xuan ","Yuan ","Nan ","Ze ","Wo ","Chun ","Xiao ","Yu ","Pian ","Mao ","An ","E ","Luo ","Ying ","Huo ","Gua ","Jiang ","Mian ","Zuo ","Zuo ","Ju ","Bao ","Rou ","Xi ","Xie ","An ","Qu ","Jian ","Fu ","Lu ","Jing ","Pen ","Feng ","Hong ","Hong ","Hou ","Yan ","Tu ","Zhu ","Zi ","Xiang ","Shen ","Ge ","Jie ","Jing ","Mi ","Huang ","Shen ","Pu ","Gai ","Dong ","Zhou ","Qian ","Wei ","Bo ","Wei ","Pa ","Ji ","Hu ","Zang ","Jia ","Duan ","Yao ","Jun ","Cong ","Quan ","Wei ","Xian ","Kui ","Ting ","Hun ","Xi ","Shi ","Qi ","Lan ","Zong ","Yao ","Yuan ","Mei ","Yun ","Shu ","Di ","Zhuan ","Guan ","Sukumo ","Xue ","Chan ","Kai ","Kui ","[?] ","Jiang ","Lou ","Wei ","Pai ","[?] ","Sou ","Yin ","Shi ","Chun ","Shi ","Yun ","Zhen ","Lang ","Nu ","Meng ","He ","Que ","Suan ","Yuan ","Li ","Ju ","Xi ","Pang ","Chu ","Xu ","Tu ","Liu ","Wo ","Zhen ","Qian ","Zu ","Po ","Cuo ","Yuan ","Chu ","Yu ","Kuai ","Pan ","Pu ","Pu ","Na ","Shuo ","Xi ","Fen ","Yun ","Zheng ","Jian ","Ji ","Ruo ","Cang ","En ","Mi ","Hao ","Sun ","Zhen ","Ming ","Sou ","Xu ","Liu ","Xi ","Gu ","Lang ","Rong ","Weng ","Gai ","Cuo ","Shi ","Tang ","Luo ","Ru ","Suo ","Xian ","Bei ","Yao ","Gui ","Bi ","Zong ","Gun ","Za ","Xiu ","Ce ","Hai ","Lan ","[?] ","Ji ","Li ","Can ","Lang ","Yu ","[?] ","Ying ","Mo ","Diao ","Tiao ","Mao ","Tong ","Zhu ","Peng ","An ","Lian ","Cong ","Xi ","Ping ","Qiu ","Jin ","Chun ","Jie ","Wei ","Tui ","Cao ","Yu ","Yi ","Ji ","Liao ","Bi ","Lu ","Su " ];
|
|
|
|
},{}],125:[function(require,module,exports){
|
|
module.exports = [ "Bu ","Zhang ","Luo ","Jiang ","Man ","Yan ","Ling ","Ji ","Piao ","Gun ","Han ","Di ","Su ","Lu ","She ","Shang ","Di ","Mie ","Xun ","Man ","Bo ","Di ","Cuo ","Zhe ","Sen ","Xuan ","Wei ","Hu ","Ao ","Mi ","Lou ","Cu ","Zhong ","Cai ","Po ","Jiang ","Mi ","Cong ","Niao ","Hui ","Jun ","Yin ","Jian ","Yan ","Shu ","Yin ","Kui ","Chen ","Hu ","Sha ","Kou ","Qian ","Ma ","Zang ","Sonoko ","Qiang ","Dou ","Lian ","Lin ","Kou ","Ai ","Bi ","Li ","Wei ","Ji ","Xun ","Sheng ","Fan ","Meng ","Ou ","Chan ","Dian ","Xun ","Jiao ","Rui ","Rui ","Lei ","Yu ","Qiao ","Chu ","Hua ","Jian ","Mai ","Yun ","Bao ","You ","Qu ","Lu ","Rao ","Hui ","E ","Teng ","Fei ","Jue ","Zui ","Fa ","Ru ","Fen ","Kui ","Shun ","Rui ","Ya ","Xu ","Fu ","Jue ","Dang ","Wu ","Tong ","Si ","Xiao ","Xi ","Long ","Yun ","[?] ","Qi ","Jian ","Yun ","Sun ","Ling ","Yu ","Xia ","Yong ","Ji ","Hong ","Si ","Nong ","Lei ","Xuan ","Yun ","Yu ","Xi ","Hao ","Bo ","Hao ","Ai ","Wei ","Hui ","Wei ","Ji ","Ci ","Xiang ","Luan ","Mie ","Yi ","Leng ","Jiang ","Can ","Shen ","Qiang ","Lian ","Ke ","Yuan ","Da ","Ti ","Tang ","Xie ","Bi ","Zhan ","Sun ","Lian ","Fan ","Ding ","Jie ","Gu ","Xie ","Shu ","Jian ","Kao ","Hong ","Sa ","Xin ","Xun ","Yao ","Hie ","Sou ","Shu ","Xun ","Dui ","Pin ","Wei ","Neng ","Chou ","Mai ","Ru ","Piao ","Tai ","Qi ","Zao ","Chen ","Zhen ","Er ","Ni ","Ying ","Gao ","Cong ","Xiao ","Qi ","Fa ","Jian ","Xu ","Kui ","Jie ","Bian ","Diao ","Mi ","Lan ","Jin ","Cang ","Miao ","Qiong ","Qie ","Xian ","[?] ","Ou ","Xian ","Su ","Lu ","Yi ","Xu ","Xie ","Li ","Yi ","La ","Lei ","Xiao ","Di ","Zhi ","Bei ","Teng ","Yao ","Mo ","Huan ","Piao ","Fan ","Sou ","Tan ","Tui ","Qiong ","Qiao ","Wei ","Liu ","Hui ","[?] ","Gao ","Yun ","[?] ","Li ","Shu ","Chu ","Ai ","Lin ","Zao ","Xuan ","Chen ","Lai ","Huo " ];
|
|
|
|
},{}],126:[function(require,module,exports){
|
|
module.exports = [ "Tuo ","Wu ","Rui ","Rui ","Qi ","Heng ","Lu ","Su ","Tui ","Mang ","Yun ","Pin ","Yu ","Xun ","Ji ","Jiong ","Xian ","Mo ","Hagi ","Su ","Jiong ","[?] ","Nie ","Bo ","Rang ","Yi ","Xian ","Yu ","Ju ","Lian ","Lian ","Yin ","Qiang ","Ying ","Long ","Tong ","Wei ","Yue ","Ling ","Qu ","Yao ","Fan ","Mi ","Lan ","Kui ","Lan ","Ji ","Dang ","Katsura ","Lei ","Lei ","Hua ","Feng ","Zhi ","Wei ","Kui ","Zhan ","Huai ","Li ","Ji ","Mi ","Lei ","Huai ","Luo ","Ji ","Kui ","Lu ","Jian ","San ","[?] ","Lei ","Quan ","Xiao ","Yi ","Luan ","Men ","Bie ","Hu ","Hu ","Lu ","Nue ","Lu ","Si ","Xiao ","Qian ","Chu ","Hu ","Xu ","Cuo ","Fu ","Xu ","Xu ","Lu ","Hu ","Yu ","Hao ","Jiao ","Ju ","Guo ","Bao ","Yan ","Zhan ","Zhan ","Kui ","Ban ","Xi ","Shu ","Chong ","Qiu ","Diao ","Ji ","Qiu ","Cheng ","Shi ","[?] ","Di ","Zhe ","She ","Yu ","Gan ","Zi ","Hong ","Hui ","Meng ","Ge ","Sui ","Xia ","Chai ","Shi ","Yi ","Ma ","Xiang ","Fang ","E ","Pa ","Chi ","Qian ","Wen ","Wen ","Rui ","Bang ","Bi ","Yue ","Yue ","Jun ","Qi ","Ran ","Yin ","Qi ","Tian ","Yuan ","Jue ","Hui ","Qin ","Qi ","Zhong ","Ya ","Ci ","Mu ","Wang ","Fen ","Fen ","Hang ","Gong ","Zao ","Fu ","Ran ","Jie ","Fu ","Chi ","Dou ","Piao ","Xian ","Ni ","Te ","Qiu ","You ","Zha ","Ping ","Chi ","You ","He ","Han ","Ju ","Li ","Fu ","Ran ","Zha ","Gou ","Pi ","Bo ","Xian ","Zhu ","Diao ","Bie ","Bing ","Gu ","Ran ","Qu ","She ","Tie ","Ling ","Gu ","Dan ","Gu ","Ying ","Li ","Cheng ","Qu ","Mou ","Ge ","Ci ","Hui ","Hui ","Mang ","Fu ","Yang ","Wa ","Lie ","Zhu ","Yi ","Xian ","Kuo ","Jiao ","Li ","Yi ","Ping ","Ji ","Ha ","She ","Yi ","Wang ","Mo ","Qiong ","Qie ","Gui ","Gong ","Zhi ","Man ","Ebi ","Zhi ","Jia ","Rao ","Si ","Qi ","Xing ","Lie ","Qiu ","Shao ","Yong ","Jia ","Shui ","Che ","Bai ","E ","Han " ];
|
|
|
|
},{}],127:[function(require,module,exports){
|
|
module.exports = [ "Shu ","Xuan ","Feng ","Shen ","Zhen ","Fu ","Xian ","Zhe ","Wu ","Fu ","Li ","Lang ","Bi ","Chu ","Yuan ","You ","Jie ","Dan ","Yan ","Ting ","Dian ","Shui ","Hui ","Gua ","Zhi ","Song ","Fei ","Ju ","Mi ","Qi ","Qi ","Yu ","Jun ","Zha ","Meng ","Qiang ","Si ","Xi ","Lun ","Li ","Die ","Tiao ","Tao ","Kun ","Gan ","Han ","Yu ","Bang ","Fei ","Pi ","Wei ","Dun ","Yi ","Yuan ","Su ","Quan ","Qian ","Rui ","Ni ","Qing ","Wei ","Liang ","Guo ","Wan ","Dong ","E ","Ban ","Di ","Wang ","Can ","Yang ","Ying ","Guo ","Chan ","[?] ","La ","Ke ","Ji ","He ","Ting ","Mai ","Xu ","Mian ","Yu ","Jie ","Shi ","Xuan ","Huang ","Yan ","Bian ","Rou ","Wei ","Fu ","Yuan ","Mei ","Wei ","Fu ","Ruan ","Xie ","You ","Qiu ","Mao ","Xia ","Ying ","Shi ","Chong ","Tang ","Zhu ","Zong ","Ti ","Fu ","Yuan ","Hui ","Meng ","La ","Du ","Hu ","Qiu ","Die ","Li ","Gua ","Yun ","Ju ","Nan ","Lou ","Qun ","Rong ","Ying ","Jiang ","[?] ","Lang ","Pang ","Si ","Xi ","Ci ","Xi ","Yuan ","Weng ","Lian ","Sou ","Ban ","Rong ","Rong ","Ji ","Wu ","Qiu ","Han ","Qin ","Yi ","Bi ","Hua ","Tang ","Yi ","Du ","Nai ","He ","Hu ","Hui ","Ma ","Ming ","Yi ","Wen ","Ying ","Teng ","Yu ","Cang ","So ","Ebi ","Man ","[?] ","Shang ","Zhe ","Cao ","Chi ","Di ","Ao ","Lu ","Wei ","Zhi ","Tang ","Chen ","Piao ","Qu ","Pi ","Yu ","Jian ","Luo ","Lou ","Qin ","Zhong ","Yin ","Jiang ","Shuai ","Wen ","Jiao ","Wan ","Zhi ","Zhe ","Ma ","Ma ","Guo ","Liu ","Mao ","Xi ","Cong ","Li ","Man ","Xiao ","Kamakiri ","Zhang ","Mang ","Xiang ","Mo ","Zui ","Si ","Qiu ","Te ","Zhi ","Peng ","Peng ","Jiao ","Qu ","Bie ","Liao ","Pan ","Gui ","Xi ","Ji ","Zhuan ","Huang ","Fei ","Lao ","Jue ","Jue ","Hui ","Yin ","Chan ","Jiao ","Shan ","Rao ","Xiao ","Mou ","Chong ","Xun ","Si ","[?] ","Cheng ","Dang ","Li ","Xie ","Shan ","Yi ","Jing ","Da ","Chan ","Qi " ];
|
|
|
|
},{}],128:[function(require,module,exports){
|
|
module.exports = [ "Ci ","Xiang ","She ","Luo ","Qin ","Ying ","Chai ","Li ","Ze ","Xuan ","Lian ","Zhu ","Ze ","Xie ","Mang ","Xie ","Qi ","Rong ","Jian ","Meng ","Hao ","Ruan ","Huo ","Zhuo ","Jie ","Bin ","He ","Mie ","Fan ","Lei ","Jie ","La ","Mi ","Li ","Chun ","Li ","Qiu ","Nie ","Lu ","Du ","Xiao ","Zhu ","Long ","Li ","Long ","Feng ","Ye ","Beng ","Shang ","Gu ","Juan ","Ying ","[?] ","Xi ","Can ","Qu ","Quan ","Du ","Can ","Man ","Jue ","Jie ","Zhu ","Zha ","Xie ","Huang ","Niu ","Pei ","Nu ","Xin ","Zhong ","Mo ","Er ","Ke ","Mie ","Xi ","Xing ","Yan ","Kan ","Yuan ","[?] ","Ling ","Xuan ","Shu ","Xian ","Tong ","Long ","Jie ","Xian ","Ya ","Hu ","Wei ","Dao ","Chong ","Wei ","Dao ","Zhun ","Heng ","Qu ","Yi ","Yi ","Bu ","Gan ","Yu ","Biao ","Cha ","Yi ","Shan ","Chen ","Fu ","Gun ","Fen ","Shuai ","Jie ","Na ","Zhong ","Dan ","Ri ","Zhong ","Zhong ","Xie ","Qi ","Xie ","Ran ","Zhi ","Ren ","Qin ","Jin ","Jun ","Yuan ","Mei ","Chai ","Ao ","Niao ","Hui ","Ran ","Jia ","Tuo ","Ling ","Dai ","Bao ","Pao ","Yao ","Zuo ","Bi ","Shao ","Tan ","Ju ","He ","Shu ","Xiu ","Zhen ","Yi ","Pa ","Bo ","Di ","Wa ","Fu ","Gun ","Zhi ","Zhi ","Ran ","Pan ","Yi ","Mao ","Tuo ","Na ","Kou ","Xian ","Chan ","Qu ","Bei ","Gun ","Xi ","Ne ","Bo ","Horo ","Fu ","Yi ","Chi ","Ku ","Ren ","Jiang ","Jia ","Cun ","Mo ","Jie ","Er ","Luo ","Ru ","Zhu ","Gui ","Yin ","Cai ","Lie ","Kamishimo ","Yuki ","Zhuang ","Dang ","[?] ","Kun ","Ken ","Niao ","Shu ","Jia ","Kun ","Cheng ","Li ","Juan ","Shen ","Pou ","Ge ","Yi ","Yu ","Zhen ","Liu ","Qiu ","Qun ","Ji ","Yi ","Bu ","Zhuang ","Shui ","Sha ","Qun ","Li ","Lian ","Lian ","Ku ","Jian ","Fou ","Chan ","Bi ","Gun ","Tao ","Yuan ","Ling ","Chi ","Chang ","Chou ","Duo ","Biao ","Liang ","Chang ","Pei ","Pei ","Fei ","Yuan ","Luo ","Guo ","Yan ","Du ","Xi ","Zhi ","Ju ","Qi " ];
|
|
|
|
},{}],129:[function(require,module,exports){
|
|
module.exports = [ "Ji ","Zhi ","Gua ","Ken ","Che ","Ti ","Ti ","Fu ","Chong ","Xie ","Bian ","Die ","Kun ","Duan ","Xiu ","Xiu ","He ","Yuan ","Bao ","Bao ","Fu ","Yu ","Tuan ","Yan ","Hui ","Bei ","Chu ","Lu ","Ena ","Hitoe ","Yun ","Da ","Gou ","Da ","Huai ","Rong ","Yuan ","Ru ","Nai ","Jiong ","Suo ","Ban ","Tun ","Chi ","Sang ","Niao ","Ying ","Jie ","Qian ","Huai ","Ku ","Lian ","Bao ","Li ","Zhe ","Shi ","Lu ","Yi ","Die ","Xie ","Xian ","Wei ","Biao ","Cao ","Ji ","Jiang ","Sen ","Bao ","Xiang ","Chihaya ","Pu ","Jian ","Zhuan ","Jian ","Zui ","Ji ","Dan ","Za ","Fan ","Bo ","Xiang ","Xin ","Bie ","Rao ","Man ","Lan ","Ao ","Duo ","Gui ","Cao ","Sui ","Nong ","Chan ","Lian ","Bi ","Jin ","Dang ","Shu ","Tan ","Bi ","Lan ","Pu ","Ru ","Zhi ","[?] ","Shu ","Wa ","Shi ","Bai ","Xie ","Bo ","Chen ","Lai ","Long ","Xi ","Xian ","Lan ","Zhe ","Dai ","Tasuki ","Zan ","Shi ","Jian ","Pan ","Yi ","Ran ","Ya ","Xi ","Xi ","Yao ","Feng ","Tan ","[?] ","Biao ","Fu ","Ba ","He ","Ji ","Ji ","Jian ","Guan ","Bian ","Yan ","Gui ","Jue ","Pian ","Mao ","Mi ","Mi ","Mie ","Shi ","Si ","Zhan ","Luo ","Jue ","Mi ","Tiao ","Lian ","Yao ","Zhi ","Jun ","Xi ","Shan ","Wei ","Xi ","Tian ","Yu ","Lan ","E ","Du ","Qin ","Pang ","Ji ","Ming ","Ying ","Gou ","Qu ","Zhan ","Jin ","Guan ","Deng ","Jian ","Luo ","Qu ","Jian ","Wei ","Jue ","Qu ","Luo ","Lan ","Shen ","Di ","Guan ","Jian ","Guan ","Yan ","Gui ","Mi ","Shi ","Zhan ","Lan ","Jue ","Ji ","Xi ","Di ","Tian ","Yu ","Gou ","Jin ","Qu ","Jiao ","Jiu ","Jin ","Cu ","Jue ","Zhi ","Chao ","Ji ","Gu ","Dan ","Zui ","Di ","Shang ","Hua ","Quan ","Ge ","Chi ","Jie ","Gui ","Gong ","Hong ","Jie ","Hun ","Qiu ","Xing ","Su ","Ni ","Ji ","Lu ","Zhi ","Zha ","Bi ","Xing ","Hu ","Shang ","Gong ","Zhi ","Xue ","Chu ","Xi ","Yi ","Lu ","Jue ","Xi ","Yan ","Xi " ];
|
|
|
|
},{}],130:[function(require,module,exports){
|
|
module.exports = [ "Yan ","Yan ","Ding ","Fu ","Qiu ","Qiu ","Jiao ","Hong ","Ji ","Fan ","Xun ","Diao ","Hong ","Cha ","Tao ","Xu ","Jie ","Yi ","Ren ","Xun ","Yin ","Shan ","Qi ","Tuo ","Ji ","Xun ","Yin ","E ","Fen ","Ya ","Yao ","Song ","Shen ","Yin ","Xin ","Jue ","Xiao ","Ne ","Chen ","You ","Zhi ","Xiong ","Fang ","Xin ","Chao ","She ","Xian ","Sha ","Tun ","Xu ","Yi ","Yi ","Su ","Chi ","He ","Shen ","He ","Xu ","Zhen ","Zhu ","Zheng ","Gou ","Zi ","Zi ","Zhan ","Gu ","Fu ","Quan ","Die ","Ling ","Di ","Yang ","Li ","Nao ","Pan ","Zhou ","Gan ","Yi ","Ju ","Ao ","Zha ","Tuo ","Yi ","Qu ","Zhao ","Ping ","Bi ","Xiong ","Qu ","Ba ","Da ","Zu ","Tao ","Zhu ","Ci ","Zhe ","Yong ","Xu ","Xun ","Yi ","Huang ","He ","Shi ","Cha ","Jiao ","Shi ","Hen ","Cha ","Gou ","Gui ","Quan ","Hui ","Jie ","Hua ","Gai ","Xiang ","Wei ","Shen ","Chou ","Tong ","Mi ","Zhan ","Ming ","E ","Hui ","Yan ","Xiong ","Gua ","Er ","Beng ","Tiao ","Chi ","Lei ","Zhu ","Kuang ","Kua ","Wu ","Yu ","Teng ","Ji ","Zhi ","Ren ","Su ","Lang ","E ","Kuang ","E ","Shi ","Ting ","Dan ","Bo ","Chan ","You ","Heng ","Qiao ","Qin ","Shua ","An ","Yu ","Xiao ","Cheng ","Jie ","Xian ","Wu ","Wu ","Gao ","Song ","Pu ","Hui ","Jing ","Shuo ","Zhen ","Shuo ","Du ","Yasashi ","Chang ","Shui ","Jie ","Ke ","Qu ","Cong ","Xiao ","Sui ","Wang ","Xuan ","Fei ","Chi ","Ta ","Yi ","Na ","Yin ","Diao ","Pi ","Chuo ","Chan ","Chen ","Zhun ","Ji ","Qi ","Tan ","Zhui ","Wei ","Ju ","Qing ","Jian ","Zheng ","Ze ","Zou ","Qian ","Zhuo ","Liang ","Jian ","Zhu ","Hao ","Lun ","Shen ","Biao ","Huai ","Pian ","Yu ","Die ","Xu ","Pian ","Shi ","Xuan ","Shi ","Hun ","Hua ","E ","Zhong ","Di ","Xie ","Fu ","Pu ","Ting ","Jian ","Qi ","Yu ","Zi ","Chuan ","Xi ","Hui ","Yin ","An ","Xian ","Nan ","Chen ","Feng ","Zhu ","Yang ","Yan ","Heng ","Xuan ","Ge ","Nuo ","Qi " ];
|
|
|
|
},{}],131:[function(require,module,exports){
|
|
module.exports = [ "Mou ","Ye ","Wei ","[?] ","Teng ","Zou ","Shan ","Jian ","Bo ","Ku ","Huang ","Huo ","Ge ","Ying ","Mi ","Xiao ","Mi ","Xi ","Qiang ","Chen ","Nue ","Ti ","Su ","Bang ","Chi ","Qian ","Shi ","Jiang ","Yuan ","Xie ","Xue ","Tao ","Yao ","Yao ","[?] ","Yu ","Biao ","Cong ","Qing ","Li ","Mo ","Mo ","Shang ","Zhe ","Miu ","Jian ","Ze ","Jie ","Lian ","Lou ","Can ","Ou ","Guan ","Xi ","Zhuo ","Ao ","Ao ","Jin ","Zhe ","Yi ","Hu ","Jiang ","Man ","Chao ","Han ","Hua ","Chan ","Xu ","Zeng ","Se ","Xi ","She ","Dui ","Zheng ","Nao ","Lan ","E ","Ying ","Jue ","Ji ","Zun ","Jiao ","Bo ","Hui ","Zhuan ","Mu ","Zen ","Zha ","Shi ","Qiao ","Tan ","Zen ","Pu ","Sheng ","Xuan ","Zao ","Tan ","Dang ","Sui ","Qian ","Ji ","Jiao ","Jing ","Lian ","Nou ","Yi ","Ai ","Zhan ","Pi ","Hui ","Hua ","Yi ","Yi ","Shan ","Rang ","Nou ","Qian ","Zhui ","Ta ","Hu ","Zhou ","Hao ","Ye ","Ying ","Jian ","Yu ","Jian ","Hui ","Du ","Zhe ","Xuan ","Zan ","Lei ","Shen ","Wei ","Chan ","Li ","Yi ","Bian ","Zhe ","Yan ","E ","Chou ","Wei ","Chou ","Yao ","Chan ","Rang ","Yin ","Lan ","Chen ","Huo ","Zhe ","Huan ","Zan ","Yi ","Dang ","Zhan ","Yan ","Du ","Yan ","Ji ","Ding ","Fu ","Ren ","Ji ","Jie ","Hong ","Tao ","Rang ","Shan ","Qi ","Tuo ","Xun ","Yi ","Xun ","Ji ","Ren ","Jiang ","Hui ","Ou ","Ju ","Ya ","Ne ","Xu ","E ","Lun ","Xiong ","Song ","Feng ","She ","Fang ","Jue ","Zheng ","Gu ","He ","Ping ","Zu ","Shi ","Xiong ","Zha ","Su ","Zhen ","Di ","Zou ","Ci ","Qu ","Zhao ","Bi ","Yi ","Yi ","Kuang ","Lei ","Shi ","Gua ","Shi ","Jie ","Hui ","Cheng ","Zhu ","Shen ","Hua ","Dan ","Gou ","Quan ","Gui ","Xun ","Yi ","Zheng ","Gai ","Xiang ","Cha ","Hun ","Xu ","Zhou ","Jie ","Wu ","Yu ","Qiao ","Wu ","Gao ","You ","Hui ","Kuang ","Shuo ","Song ","Ai ","Qing ","Zhu ","Zou ","Nuo ","Du ","Zhuo ","Fei ","Ke ","Wei " ];
|
|
|
|
},{}],132:[function(require,module,exports){
|
|
module.exports = [ "Yu ","Shui ","Shen ","Diao ","Chan ","Liang ","Zhun ","Sui ","Tan ","Shen ","Yi ","Mou ","Chen ","Die ","Huang ","Jian ","Xie ","Nue ","Ye ","Wei ","E ","Yu ","Xuan ","Chan ","Zi ","An ","Yan ","Di ","Mi ","Pian ","Xu ","Mo ","Dang ","Su ","Xie ","Yao ","Bang ","Shi ","Qian ","Mi ","Jin ","Man ","Zhe ","Jian ","Miu ","Tan ","Zen ","Qiao ","Lan ","Pu ","Jue ","Yan ","Qian ","Zhan ","Chen ","Gu ","Qian ","Hong ","Xia ","Jue ","Hong ","Han ","Hong ","Xi ","Xi ","Huo ","Liao ","Han ","Du ","Long ","Dou ","Jiang ","Qi ","Shi ","Li ","Deng ","Wan ","Bi ","Shu ","Xian ","Feng ","Zhi ","Zhi ","Yan ","Yan ","Shi ","Chu ","Hui ","Tun ","Yi ","Tun ","Yi ","Jian ","Ba ","Hou ","E ","Cu ","Xiang ","Huan ","Jian ","Ken ","Gai ","Qu ","Fu ","Xi ","Bin ","Hao ","Yu ","Zhu ","Jia ","[?] ","Xi ","Bo ","Wen ","Huan ","Bin ","Di ","Zong ","Fen ","Yi ","Zhi ","Bao ","Chai ","Han ","Pi ","Na ","Pi ","Gou ","Na ","You ","Diao ","Mo ","Si ","Xiu ","Huan ","Kun ","He ","He ","Mo ","Han ","Mao ","Li ","Ni ","Bi ","Yu ","Jia ","Tuan ","Mao ","Pi ","Xi ","E ","Ju ","Mo ","Chu ","Tan ","Huan ","Jue ","Bei ","Zhen ","Yuan ","Fu ","Cai ","Gong ","Te ","Yi ","Hang ","Wan ","Pin ","Huo ","Fan ","Tan ","Guan ","Ze ","Zhi ","Er ","Zhu ","Shi ","Bi ","Zi ","Er ","Gui ","Pian ","Bian ","Mai ","Dai ","Sheng ","Kuang ","Fei ","Tie ","Yi ","Chi ","Mao ","He ","Bi ","Lu ","Ren ","Hui ","Gai ","Pian ","Zi ","Jia ","Xu ","Zei ","Jiao ","Gai ","Zang ","Jian ","Ying ","Xun ","Zhen ","She ","Bin ","Bin ","Qiu ","She ","Chuan ","Zang ","Zhou ","Lai ","Zan ","Si ","Chen ","Shang ","Tian ","Pei ","Geng ","Xian ","Mai ","Jian ","Sui ","Fu ","Tan ","Cong ","Cong ","Zhi ","Ji ","Zhang ","Du ","Jin ","Xiong ","Shun ","Yun ","Bao ","Zai ","Lai ","Feng ","Cang ","Ji ","Sheng ","Ai ","Zhuan ","Fu ","Gou ","Sai ","Ze ","Liao " ];
|
|
|
|
},{}],133:[function(require,module,exports){
|
|
module.exports = [ "Wei ","Bai ","Chen ","Zhuan ","Zhi ","Zhui ","Biao ","Yun ","Zeng ","Tan ","Zan ","Yan ","[?] ","Shan ","Wan ","Ying ","Jin ","Gan ","Xian ","Zang ","Bi ","Du ","Shu ","Yan ","[?] ","Xuan ","Long ","Gan ","Zang ","Bei ","Zhen ","Fu ","Yuan ","Gong ","Cai ","Ze ","Xian ","Bai ","Zhang ","Huo ","Zhi ","Fan ","Tan ","Pin ","Bian ","Gou ","Zhu ","Guan ","Er ","Jian ","Bi ","Shi ","Tie ","Gui ","Kuang ","Dai ","Mao ","Fei ","He ","Yi ","Zei ","Zhi ","Jia ","Hui ","Zi ","Ren ","Lu ","Zang ","Zi ","Gai ","Jin ","Qiu ","Zhen ","Lai ","She ","Fu ","Du ","Ji ","Shu ","Shang ","Si ","Bi ","Zhou ","Geng ","Pei ","Tan ","Lai ","Feng ","Zhui ","Fu ","Zhuan ","Sai ","Ze ","Yan ","Zan ","Yun ","Zeng ","Shan ","Ying ","Gan ","Chi ","Xi ","She ","Nan ","Xiong ","Xi ","Cheng ","He ","Cheng ","Zhe ","Xia ","Tang ","Zou ","Zou ","Li ","Jiu ","Fu ","Zhao ","Gan ","Qi ","Shan ","Qiong ","Qin ","Xian ","Ci ","Jue ","Qin ","Chi ","Ci ","Chen ","Chen ","Die ","Ju ","Chao ","Di ","Se ","Zhan ","Zhu ","Yue ","Qu ","Jie ","Chi ","Chu ","Gua ","Xue ","Ci ","Tiao ","Duo ","Lie ","Gan ","Suo ","Cu ","Xi ","Zhao ","Su ","Yin ","Ju ","Jian ","Que ","Tang ","Chuo ","Cui ","Lu ","Qu ","Dang ","Qiu ","Zi ","Ti ","Qu ","Chi ","Huang ","Qiao ","Qiao ","Yao ","Zao ","Ti ","[?] ","Zan ","Zan ","Zu ","Pa ","Bao ","Ku ","Ke ","Dun ","Jue ","Fu ","Chen ","Jian ","Fang ","Zhi ","Sa ","Yue ","Pa ","Qi ","Yue ","Qiang ","Tuo ","Tai ","Yi ","Nian ","Ling ","Mei ","Ba ","Die ","Ku ","Tuo ","Jia ","Ci ","Pao ","Qia ","Zhu ","Ju ","Die ","Zhi ","Fu ","Pan ","Ju ","Shan ","Bo ","Ni ","Ju ","Li ","Gen ","Yi ","Ji ","Dai ","Xian ","Jiao ","Duo ","Zhu ","Zhuan ","Kua ","Zhuai ","Gui ","Qiong ","Kui ","Xiang ","Chi ","Lu ","Beng ","Zhi ","Jia ","Tiao ","Cai ","Jian ","Ta ","Qiao ","Bi ","Xian ","Duo ","Ji ","Ju ","Ji ","Shu ","Tu " ];
|
|
|
|
},{}],134:[function(require,module,exports){
|
|
module.exports = [ "Chu ","Jing ","Nie ","Xiao ","Bo ","Chi ","Qun ","Mou ","Shu ","Lang ","Yong ","Jiao ","Chou ","Qiao ","[?] ","Ta ","Jian ","Qi ","Wo ","Wei ","Zhuo ","Jie ","Ji ","Nie ","Ju ","Ju ","Lun ","Lu ","Leng ","Huai ","Ju ","Chi ","Wan ","Quan ","Ti ","Bo ","Zu ","Qie ","Ji ","Cu ","Zong ","Cai ","Zong ","Peng ","Zhi ","Zheng ","Dian ","Zhi ","Yu ","Duo ","Dun ","Chun ","Yong ","Zhong ","Di ","Zhe ","Chen ","Chuai ","Jian ","Gua ","Tang ","Ju ","Fu ","Zu ","Die ","Pian ","Rou ","Nuo ","Ti ","Cha ","Tui ","Jian ","Dao ","Cuo ","Xi ","Ta ","Qiang ","Zhan ","Dian ","Ti ","Ji ","Nie ","Man ","Liu ","Zhan ","Bi ","Chong ","Lu ","Liao ","Cu ","Tang ","Dai ","Suo ","Xi ","Kui ","Ji ","Zhi ","Qiang ","Di ","Man ","Zong ","Lian ","Beng ","Zao ","Nian ","Bie ","Tui ","Ju ","Deng ","Ceng ","Xian ","Fan ","Chu ","Zhong ","Dun ","Bo ","Cu ","Zu ","Jue ","Jue ","Lin ","Ta ","Qiao ","Qiao ","Pu ","Liao ","Dun ","Cuan ","Kuang ","Zao ","Ta ","Bi ","Bi ","Zhu ","Ju ","Chu ","Qiao ","Dun ","Chou ","Ji ","Wu ","Yue ","Nian ","Lin ","Lie ","Zhi ","Li ","Zhi ","Chan ","Chu ","Duan ","Wei ","Long ","Lin ","Xian ","Wei ","Zuan ","Lan ","Xie ","Rang ","Xie ","Nie ","Ta ","Qu ","Jie ","Cuan ","Zuan ","Xi ","Kui ","Jue ","Lin ","Shen ","Gong ","Dan ","Segare ","Qu ","Ti ","Duo ","Duo ","Gong ","Lang ","Nerau ","Luo ","Ai ","Ji ","Ju ","Tang ","Utsuke ","[?] ","Yan ","Shitsuke ","Kang ","Qu ","Lou ","Lao ","Tuo ","Zhi ","Yagate ","Ti ","Dao ","Yagate ","Yu ","Che ","Ya ","Gui ","Jun ","Wei ","Yue ","Xin ","Di ","Xuan ","Fan ","Ren ","Shan ","Qiang ","Shu ","Tun ","Chen ","Dai ","E ","Na ","Qi ","Mao ","Ruan ","Ren ","Fan ","Zhuan ","Hong ","Hu ","Qu ","Huang ","Di ","Ling ","Dai ","Ao ","Zhen ","Fan ","Kuang ","Ang ","Peng ","Bei ","Gu ","Ku ","Pao ","Zhu ","Rong ","E ","Ba ","Zhou ","Zhi ","Yao ","Ke ","Yi ","Qing ","Shi ","Ping " ];
|
|
|
|
},{}],135:[function(require,module,exports){
|
|
module.exports = [ "Er ","Qiong ","Ju ","Jiao ","Guang ","Lu ","Kai ","Quan ","Zhou ","Zai ","Zhi ","She ","Liang ","Yu ","Shao ","You ","Huan ","Yun ","Zhe ","Wan ","Fu ","Qing ","Zhou ","Ni ","Ling ","Zhe ","Zhan ","Liang ","Zi ","Hui ","Wang ","Chuo ","Guo ","Kan ","Yi ","Peng ","Qian ","Gun ","Nian ","Pian ","Guan ","Bei ","Lun ","Pai ","Liang ","Ruan ","Rou ","Ji ","Yang ","Xian ","Chuan ","Cou ","Qun ","Ge ","You ","Hong ","Shu ","Fu ","Zi ","Fu ","Wen ","Ben ","Zhan ","Yu ","Wen ","Tao ","Gu ","Zhen ","Xia ","Yuan ","Lu ","Jiu ","Chao ","Zhuan ","Wei ","Hun ","Sori ","Che ","Jiao ","Zhan ","Pu ","Lao ","Fen ","Fan ","Lin ","Ge ","Se ","Kan ","Huan ","Yi ","Ji ","Dui ","Er ","Yu ","Xian ","Hong ","Lei ","Pei ","Li ","Li ","Lu ","Lin ","Che ","Ya ","Gui ","Xuan ","Di ","Ren ","Zhuan ","E ","Lun ","Ruan ","Hong ","Ku ","Ke ","Lu ","Zhou ","Zhi ","Yi ","Hu ","Zhen ","Li ","Yao ","Qing ","Shi ","Zai ","Zhi ","Jiao ","Zhou ","Quan ","Lu ","Jiao ","Zhe ","Fu ","Liang ","Nian ","Bei ","Hui ","Gun ","Wang ","Liang ","Chuo ","Zi ","Cou ","Fu ","Ji ","Wen ","Shu ","Pei ","Yuan ","Xia ","Zhan ","Lu ","Che ","Lin ","Xin ","Gu ","Ci ","Ci ","Pi ","Zui ","Bian ","La ","La ","Ci ","Xue ","Ban ","Bian ","Bian ","Bian ","[?] ","Bian ","Ban ","Ci ","Bian ","Bian ","Chen ","Ru ","Nong ","Nong ","Zhen ","Chuo ","Chuo ","Suberu ","Reng ","Bian ","Bian ","Sip ","Ip ","Liao ","Da ","Chan ","Gan ","Qian ","Yu ","Yu ","Qi ","Xun ","Yi ","Guo ","Mai ","Qi ","Za ","Wang ","Jia ","Zhun ","Ying ","Ti ","Yun ","Jin ","Hang ","Ya ","Fan ","Wu ","Da ","E ","Huan ","Zhe ","Totemo ","Jin ","Yuan ","Wei ","Lian ","Chi ","Che ","Ni ","Tiao ","Zhi ","Yi ","Jiong ","Jia ","Chen ","Dai ","Er ","Di ","Po ","Wang ","Die ","Ze ","Tao ","Shu ","Tuo ","Kep ","Jing ","Hui ","Tong ","You ","Mi ","Beng ","Ji ","Nai ","Yi ","Jie ","Zhui ","Lie ","Xun " ];
|
|
|
|
},{}],136:[function(require,module,exports){
|
|
module.exports = [ "Tui ","Song ","Gua ","Tao ","Pang ","Hou ","Ni ","Dun ","Jiong ","Xuan ","Xun ","Bu ","You ","Xiao ","Qiu ","Tou ","Zhu ","Qiu ","Di ","Di ","Tu ","Jing ","Ti ","Dou ","Yi ","Zhe ","Tong ","Guang ","Wu ","Shi ","Cheng ","Su ","Zao ","Qun ","Feng ","Lian ","Suo ","Hui ","Li ","Sako ","Lai ","Ben ","Cuo ","Jue ","Beng ","Huan ","Dai ","Lu ","You ","Zhou ","Jin ","Yu ","Chuo ","Kui ","Wei ","Ti ","Yi ","Da ","Yuan ","Luo ","Bi ","Nuo ","Yu ","Dang ","Sui ","Dun ","Sui ","Yan ","Chuan ","Chi ","Ti ","Yu ","Shi ","Zhen ","You ","Yun ","E ","Bian ","Guo ","E ","Xia ","Huang ","Qiu ","Dao ","Da ","Wei ","Appare ","Yi ","Gou ","Yao ","Chu ","Liu ","Xun ","Ta ","Di ","Chi ","Yuan ","Su ","Ta ","Qian ","[?] ","Yao ","Guan ","Zhang ","Ao ","Shi ","Ce ","Chi ","Su ","Zao ","Zhe ","Dun ","Di ","Lou ","Chi ","Cuo ","Lin ","Zun ","Rao ","Qian ","Xuan ","Yu ","Yi ","Wu ","Liao ","Ju ","Shi ","Bi ","Yao ","Mai ","Xie ","Sui ","Huan ","Zhan ","Teng ","Er ","Miao ","Bian ","Bian ","La ","Li ","Yuan ","Yao ","Luo ","Li ","Yi ","Ting ","Deng ","Qi ","Yong ","Shan ","Han ","Yu ","Mang ","Ru ","Qiong ","[?] ","Kuang ","Fu ","Kang ","Bin ","Fang ","Xing ","Na ","Xin ","Shen ","Bang ","Yuan ","Cun ","Huo ","Xie ","Bang ","Wu ","Ju ","You ","Han ","Tai ","Qiu ","Bi ","Pei ","Bing ","Shao ","Bei ","Wa ","Di ","Zou ","Ye ","Lin ","Kuang ","Gui ","Zhu ","Shi ","Ku ","Yu ","Gai ","Ge ","Xi ","Zhi ","Ji ","Xun ","Hou ","Xing ","Jiao ","Xi ","Gui ","Nuo ","Lang ","Jia ","Kuai ","Zheng ","Otoko ","Yun ","Yan ","Cheng ","Dou ","Chi ","Lu ","Fu ","Wu ","Fu ","Gao ","Hao ","Lang ","Jia ","Geng ","Jun ","Ying ","Bo ","Xi ","Bei ","Li ","Yun ","Bu ","Xiao ","Qi ","Pi ","Qing ","Guo ","Zhou ","Tan ","Zou ","Ping ","Lai ","Ni ","Chen ","You ","Bu ","Xiang ","Dan ","Ju ","Yong ","Qiao ","Yi ","Du ","Yan ","Mei " ];
|
|
|
|
},{}],137:[function(require,module,exports){
|
|
module.exports = [ "Ruo ","Bei ","E ","Yu ","Juan ","Yu ","Yun ","Hou ","Kui ","Xiang ","Xiang ","Sou ","Tang ","Ming ","Xi ","Ru ","Chu ","Zi ","Zou ","Ju ","Wu ","Xiang ","Yun ","Hao ","Yong ","Bi ","Mo ","Chao ","Fu ","Liao ","Yin ","Zhuan ","Hu ","Qiao ","Yan ","Zhang ","Fan ","Qiao ","Xu ","Deng ","Bi ","Xin ","Bi ","Ceng ","Wei ","Zheng ","Mao ","Shan ","Lin ","Po ","Dan ","Meng ","Ye ","Cao ","Kuai ","Feng ","Meng ","Zou ","Kuang ","Lian ","Zan ","Chan ","You ","Qi ","Yan ","Chan ","Zan ","Ling ","Huan ","Xi ","Feng ","Zan ","Li ","You ","Ding ","Qiu ","Zhuo ","Pei ","Zhou ","Yi ","Hang ","Yu ","Jiu ","Yan ","Zui ","Mao ","Dan ","Xu ","Tou ","Zhen ","Fen ","Sakenomoto ","[?] ","Yun ","Tai ","Tian ","Qia ","Tuo ","Zuo ","Han ","Gu ","Su ","Po ","Chou ","Zai ","Ming ","Luo ","Chuo ","Chou ","You ","Tong ","Zhi ","Xian ","Jiang ","Cheng ","Yin ","Tu ","Xiao ","Mei ","Ku ","Suan ","Lei ","Pu ","Zui ","Hai ","Yan ","Xi ","Niang ","Wei ","Lu ","Lan ","Yan ","Tao ","Pei ","Zhan ","Chun ","Tan ","Zui ","Chuo ","Cu ","Kun ","Ti ","Mian ","Du ","Hu ","Xu ","Xing ","Tan ","Jiu ","Chun ","Yun ","Po ","Ke ","Sou ","Mi ","Quan ","Chou ","Cuo ","Yun ","Yong ","Ang ","Zha ","Hai ","Tang ","Jiang ","Piao ","Shan ","Yu ","Li ","Zao ","Lao ","Yi ","Jiang ","Pu ","Jiao ","Xi ","Tan ","Po ","Nong ","Yi ","Li ","Ju ","Jiao ","Yi ","Niang ","Ru ","Xun ","Chou ","Yan ","Ling ","Mi ","Mi ","Niang ","Xin ","Jiao ","Xi ","Mi ","Yan ","Bian ","Cai ","Shi ","You ","Shi ","Shi ","Li ","Zhong ","Ye ","Liang ","Li ","Jin ","Jin ","Qiu ","Yi ","Diao ","Dao ","Zhao ","Ding ","Po ","Qiu ","He ","Fu ","Zhen ","Zhi ","Ba ","Luan ","Fu ","Nai ","Diao ","Shan ","Qiao ","Kou ","Chuan ","Zi ","Fan ","Yu ","Hua ","Han ","Gong ","Qi ","Mang ","Ri ","Di ","Si ","Xi ","Yi ","Chai ","Shi ","Tu ","Xi ","Nu ","Qian ","Ishiyumi ","Jian ","Pi ","Ye ","Yin " ];
|
|
|
|
},{}],138:[function(require,module,exports){
|
|
module.exports = [ "Ba ","Fang ","Chen ","Xing ","Tou ","Yue ","Yan ","Fu ","Pi ","Na ","Xin ","E ","Jue ","Dun ","Gou ","Yin ","Qian ","Ban ","Ji ","Ren ","Chao ","Niu ","Fen ","Yun ","Ji ","Qin ","Pi ","Guo ","Hong ","Yin ","Jun ","Shi ","Yi ","Zhong ","Nie ","Gai ","Ri ","Huo ","Tai ","Kang ","Habaki ","Irori ","Ngaak ","[?] ","Duo ","Zi ","Ni ","Tu ","Shi ","Min ","Gu ","E ","Ling ","Bing ","Yi ","Gu ","Ba ","Pi ","Yu ","Si ","Zuo ","Bu ","You ","Dian ","Jia ","Zhen ","Shi ","Shi ","Tie ","Ju ","Zhan ","Shi ","She ","Xuan ","Zhao ","Bao ","He ","Bi ","Sheng ","Chu ","Shi ","Bo ","Zhu ","Chi ","Za ","Po ","Tong ","Qian ","Fu ","Zhai ","Liu ","Qian ","Fu ","Li ","Yue ","Pi ","Yang ","Ban ","Bo ","Jie ","Gou ","Shu ","Zheng ","Mu ","Ni ","Nie ","Di ","Jia ","Mu ","Dan ","Shen ","Yi ","Si ","Kuang ","Ka ","Bei ","Jian ","Tong ","Xing ","Hong ","Jiao ","Chi ","Er ","Ge ","Bing ","Shi ","Mou ","Jia ","Yin ","Jun ","Zhou ","Chong ","Shang ","Tong ","Mo ","Lei ","Ji ","Yu ","Xu ","Ren ","Zun ","Zhi ","Qiong ","Shan ","Chi ","Xian ","Xing ","Quan ","Pi ","Tie ","Zhu ","Hou ","Ming ","Kua ","Yao ","Xian ","Xian ","Xiu ","Jun ","Cha ","Lao ","Ji ","Pi ","Ru ","Mi ","Yi ","Yin ","Guang ","An ","Diou ","You ","Se ","Kao ","Qian ","Luan ","Kasugai ","Ai ","Diao ","Han ","Rui ","Shi ","Keng ","Qiu ","Xiao ","Zhe ","Xiu ","Zang ","Ti ","Cuo ","Gua ","Gong ","Zhong ","Dou ","Lu ","Mei ","Lang ","Wan ","Xin ","Yun ","Bei ","Wu ","Su ","Yu ","Chan ","Ting ","Bo ","Han ","Jia ","Hong ","Cuan ","Feng ","Chan ","Wan ","Zhi ","Si ","Xuan ","Wu ","Wu ","Tiao ","Gong ","Zhuo ","Lue ","Xing ","Qian ","Shen ","Han ","Lue ","Xie ","Chu ","Zheng ","Ju ","Xian ","Tie ","Mang ","Pu ","Li ","Pan ","Rui ","Cheng ","Gao ","Li ","Te ","Pyeng ","Zhu ","[?] ","Tu ","Liu ","Zui ","Ju ","Chang ","Yuan ","Jian ","Gang ","Diao ","Tao ","Chang " ];
|
|
|
|
},{}],139:[function(require,module,exports){
|
|
module.exports = [ "Lun ","Kua ","Ling ","Bei ","Lu ","Li ","Qiang ","Pou ","Juan ","Min ","Zui ","Peng ","An ","Pi ","Xian ","Ya ","Zhui ","Lei ","A ","Kong ","Ta ","Kun ","Du ","Wei ","Chui ","Zi ","Zheng ","Ben ","Nie ","Cong ","Qun ","Tan ","Ding ","Qi ","Qian ","Zhuo ","Qi ","Yu ","Jin ","Guan ","Mao ","Chang ","Tian ","Xi ","Lian ","Tao ","Gu ","Cuo ","Shu ","Zhen ","Lu ","Meng ","Lu ","Hua ","Biao ","Ga ","Lai ","Ken ","Kazari ","Bu ","Nai ","Wan ","Zan ","[?] ","De ","Xian ","[?] ","Huo ","Liang ","[?] ","Men ","Kai ","Ying ","Di ","Lian ","Guo ","Xian ","Du ","Tu ","Wei ","Cong ","Fu ","Rou ","Ji ","E ","Rou ","Chen ","Ti ","Zha ","Hong ","Yang ","Duan ","Xia ","Yu ","Keng ","Xing ","Huang ","Wei ","Fu ","Zhao ","Cha ","Qie ","She ","Hong ","Kui ","Tian ","Mou ","Qiao ","Qiao ","Hou ","Tou ","Cong ","Huan ","Ye ","Min ","Jian ","Duan ","Jian ","Song ","Kui ","Hu ","Xuan ","Duo ","Jie ","Zhen ","Bian ","Zhong ","Zi ","Xiu ","Ye ","Mei ","Pai ","Ai ","Jie ","[?] ","Mei ","Chuo ","Ta ","Bang ","Xia ","Lian ","Suo ","Xi ","Liu ","Zu ","Ye ","Nou ","Weng ","Rong ","Tang ","Suo ","Qiang ","Ge ","Shuo ","Chui ","Bo ","Pan ","Sa ","Bi ","Sang ","Gang ","Zi ","Wu ","Ying ","Huang ","Tiao ","Liu ","Kai ","Sun ","Sha ","Sou ","Wan ","Hao ","Zhen ","Zhen ","Luo ","Yi ","Yuan ","Tang ","Nie ","Xi ","Jia ","Ge ","Ma ","Juan ","Kasugai ","Habaki ","Suo ","[?] ","[?] ","[?] ","Na ","Lu ","Suo ","Ou ","Zu ","Tuan ","Xiu ","Guan ","Xuan ","Lian ","Shou ","Ao ","Man ","Mo ","Luo ","Bi ","Wei ","Liu ","Di ","Qiao ","Cong ","Yi ","Lu ","Ao ","Keng ","Qiang ","Cui ","Qi ","Chang ","Tang ","Man ","Yong ","Chan ","Feng ","Jing ","Biao ","Shu ","Lou ","Xiu ","Cong ","Long ","Zan ","Jian ","Cao ","Li ","Xia ","Xi ","Kang ","[?] ","Beng ","[?] ","[?] ","Zheng ","Lu ","Hua ","Ji ","Pu ","Hui ","Qiang ","Po ","Lin ","Suo ","Xiu ","San ","Cheng " ];
|
|
|
|
},{}],140:[function(require,module,exports){
|
|
module.exports = [ "Kui ","Si ","Liu ","Nao ","Heng ","Pie ","Sui ","Fan ","Qiao ","Quan ","Yang ","Tang ","Xiang ","Jue ","Jiao ","Zun ","Liao ","Jie ","Lao ","Dui ","Tan ","Zan ","Ji ","Jian ","Zhong ","Deng ","Ya ","Ying ","Dui ","Jue ","Nou ","Ti ","Pu ","Tie ","[?] ","[?] ","Ding ","Shan ","Kai ","Jian ","Fei ","Sui ","Lu ","Juan ","Hui ","Yu ","Lian ","Zhuo ","Qiao ","Qian ","Zhuo ","Lei ","Bi ","Tie ","Huan ","Ye ","Duo ","Guo ","Dang ","Ju ","Fen ","Da ","Bei ","Yi ","Ai ","Zong ","Xun ","Diao ","Zhu ","Heng ","Zhui ","Ji ","Nie ","Ta ","Huo ","Qing ","Bin ","Ying ","Kui ","Ning ","Xu ","Jian ","Jian ","Yari ","Cha ","Zhi ","Mie ","Li ","Lei ","Ji ","Zuan ","Kuang ","Shang ","Peng ","La ","Du ","Shuo ","Chuo ","Lu ","Biao ","Bao ","Lu ","[?] ","[?] ","Long ","E ","Lu ","Xin ","Jian ","Lan ","Bo ","Jian ","Yao ","Chan ","Xiang ","Jian ","Xi ","Guan ","Cang ","Nie ","Lei ","Cuan ","Qu ","Pan ","Luo ","Zuan ","Luan ","Zao ","Nie ","Jue ","Tang ","Shu ","Lan ","Jin ","Qiu ","Yi ","Zhen ","Ding ","Zhao ","Po ","Diao ","Tu ","Qian ","Chuan ","Shan ","Ji ","Fan ","Diao ","Men ","Nu ","Xi ","Chai ","Xing ","Gai ","Bu ","Tai ","Ju ","Dun ","Chao ","Zhong ","Na ","Bei ","Gang ","Ban ","Qian ","Yao ","Qin ","Jun ","Wu ","Gou ","Kang ","Fang ","Huo ","Tou ","Niu ","Ba ","Yu ","Qian ","Zheng ","Qian ","Gu ","Bo ","E ","Po ","Bu ","Ba ","Yue ","Zuan ","Mu ","Dan ","Jia ","Dian ","You ","Tie ","Bo ","Ling ","Shuo ","Qian ","Liu ","Bao ","Shi ","Xuan ","She ","Bi ","Ni ","Pi ","Duo ","Xing ","Kao ","Lao ","Er ","Mang ","Ya ","You ","Cheng ","Jia ","Ye ","Nao ","Zhi ","Dang ","Tong ","Lu ","Diao ","Yin ","Kai ","Zha ","Zhu ","Xian ","Ting ","Diu ","Xian ","Hua ","Quan ","Sha ","Jia ","Yao ","Ge ","Ming ","Zheng ","Se ","Jiao ","Yi ","Chan ","Chong ","Tang ","An ","Yin ","Ru ","Zhu ","Lao ","Pu ","Wu ","Lai ","Te ","Lian ","Keng " ];
|
|
|
|
},{}],141:[function(require,module,exports){
|
|
module.exports = [ "Xiao ","Suo ","Li ","Zheng ","Chu ","Guo ","Gao ","Tie ","Xiu ","Cuo ","Lue ","Feng ","Xin ","Liu ","Kai ","Jian ","Rui ","Ti ","Lang ","Qian ","Ju ","A ","Qiang ","Duo ","Tian ","Cuo ","Mao ","Ben ","Qi ","De ","Kua ","Kun ","Chang ","Xi ","Gu ","Luo ","Chui ","Zhui ","Jin ","Zhi ","Xian ","Juan ","Huo ","Pou ","Tan ","Ding ","Jian ","Ju ","Meng ","Zi ","Qie ","Ying ","Kai ","Qiang ","Song ","E ","Cha ","Qiao ","Zhong ","Duan ","Sou ","Huang ","Huan ","Ai ","Du ","Mei ","Lou ","Zi ","Fei ","Mei ","Mo ","Zhen ","Bo ","Ge ","Nie ","Tang ","Juan ","Nie ","Na ","Liu ","Hao ","Bang ","Yi ","Jia ","Bin ","Rong ","Biao ","Tang ","Man ","Luo ","Beng ","Yong ","Jing ","Di ","Zu ","Xuan ","Liu ","Tan ","Jue ","Liao ","Pu ","Lu ","Dui ","Lan ","Pu ","Cuan ","Qiang ","Deng ","Huo ","Lei ","Huan ","Zhuo ","Lian ","Yi ","Cha ","Biao ","La ","Chan ","Xiang ","Chang ","Chang ","Jiu ","Ao ","Die ","Qu ","Liao ","Mi ","Chang ","Men ","Ma ","Shuan ","Shan ","Huo ","Men ","Yan ","Bi ","Han ","Bi ","San ","Kai ","Kang ","Beng ","Hong ","Run ","San ","Xian ","Xian ","Jian ","Min ","Xia ","Yuru ","Dou ","Zha ","Nao ","Jian ","Peng ","Xia ","Ling ","Bian ","Bi ","Run ","He ","Guan ","Ge ","Ge ","Fa ","Chu ","Hong ","Gui ","Min ","Se ","Kun ","Lang ","Lu ","Ting ","Sha ","Ju ","Yue ","Yue ","Chan ","Qu ","Lin ","Chang ","Shai ","Kun ","Yan ","Min ","Yan ","E ","Hun ","Yu ","Wen ","Xiang ","Bao ","Xiang ","Qu ","Yao ","Wen ","Ban ","An ","Wei ","Yin ","Kuo ","Que ","Lan ","Du ","[?] ","Phwung ","Tian ","Nie ","Ta ","Kai ","He ","Que ","Chuang ","Guan ","Dou ","Qi ","Kui ","Tang ","Guan ","Piao ","Kan ","Xi ","Hui ","Chan ","Pi ","Dang ","Huan ","Ta ","Wen ","[?] ","Men ","Shuan ","Shan ","Yan ","Han ","Bi ","Wen ","Chuang ","Run ","Wei ","Xian ","Hong ","Jian ","Min ","Kang ","Men ","Zha ","Nao ","Gui ","Wen ","Ta ","Min ","Lu ","Kai " ];
|
|
|
|
},{}],142:[function(require,module,exports){
|
|
module.exports = [ "Fa ","Ge ","He ","Kun ","Jiu ","Yue ","Lang ","Du ","Yu ","Yan ","Chang ","Xi ","Wen ","Hun ","Yan ","E ","Chan ","Lan ","Qu ","Hui ","Kuo ","Que ","Ge ","Tian ","Ta ","Que ","Kan ","Huan ","Fu ","Fu ","Le ","Dui ","Xin ","Qian ","Wu ","Yi ","Tuo ","Yin ","Yang ","Dou ","E ","Sheng ","Ban ","Pei ","Keng ","Yun ","Ruan ","Zhi ","Pi ","Jing ","Fang ","Yang ","Yin ","Zhen ","Jie ","Cheng ","E ","Qu ","Di ","Zu ","Zuo ","Dian ","Ling ","A ","Tuo ","Tuo ","Po ","Bing ","Fu ","Ji ","Lu ","Long ","Chen ","Xing ","Duo ","Lou ","Mo ","Jiang ","Shu ","Duo ","Xian ","Er ","Gui ","Yu ","Gai ","Shan ","Xun ","Qiao ","Xing ","Chun ","Fu ","Bi ","Xia ","Shan ","Sheng ","Zhi ","Pu ","Dou ","Yuan ","Zhen ","Chu ","Xian ","Tou ","Nie ","Yun ","Xian ","Pei ","Pei ","Zou ","Yi ","Dui ","Lun ","Yin ","Ju ","Chui ","Chen ","Pi ","Ling ","Tao ","Xian ","Lu ","Sheng ","Xian ","Yin ","Zhu ","Yang ","Reng ","Shan ","Chong ","Yan ","Yin ","Yu ","Ti ","Yu ","Long ","Wei ","Wei ","Nie ","Dui ","Sui ","An ","Huang ","Jie ","Sui ","Yin ","Gai ","Yan ","Hui ","Ge ","Yun ","Wu ","Wei ","Ai ","Xi ","Tang ","Ji ","Zhang ","Dao ","Ao ","Xi ","Yin ","[?] ","Rao ","Lin ","Tui ","Deng ","Pi ","Sui ","Sui ","Yu ","Xian ","Fen ","Ni ","Er ","Ji ","Dao ","Xi ","Yin ","E ","Hui ","Long ","Xi ","Li ","Li ","Li ","Zhui ","He ","Zhi ","Zhun ","Jun ","Nan ","Yi ","Que ","Yan ","Qian ","Ya ","Xiong ","Ya ","Ji ","Gu ","Huan ","Zhi ","Gou ","Jun ","Ci ","Yong ","Ju ","Chu ","Hu ","Za ","Luo ","Yu ","Chou ","Diao ","Sui ","Han ","Huo ","Shuang ","Guan ","Chu ","Za ","Yong ","Ji ","Xi ","Chou ","Liu ","Li ","Nan ","Xue ","Za ","Ji ","Ji ","Yu ","Yu ","Xue ","Na ","Fou ","Se ","Mu ","Wen ","Fen ","Pang ","Yun ","Li ","Li ","Ang ","Ling ","Lei ","An ","Bao ","Meng ","Dian ","Dang ","Xing ","Wu ","Zhao " ];
|
|
|
|
},{}],143:[function(require,module,exports){
|
|
module.exports = [ "Xu ","Ji ","Mu ","Chen ","Xiao ","Zha ","Ting ","Zhen ","Pei ","Mei ","Ling ","Qi ","Chou ","Huo ","Sha ","Fei ","Weng ","Zhan ","Yin ","Ni ","Chou ","Tun ","Lin ","[?] ","Dong ","Ying ","Wu ","Ling ","Shuang ","Ling ","Xia ","Hong ","Yin ","Mo ","Mai ","Yun ","Liu ","Meng ","Bin ","Wu ","Wei ","Huo ","Yin ","Xi ","Yi ","Ai ","Dan ","Deng ","Xian ","Yu ","Lu ","Long ","Dai ","Ji ","Pang ","Yang ","Ba ","Pi ","Wei ","[?] ","Xi ","Ji ","Mai ","Meng ","Meng ","Lei ","Li ","Huo ","Ai ","Fei ","Dai ","Long ","Ling ","Ai ","Feng ","Li ","Bao ","[?] ","He ","He ","Bing ","Qing ","Qing ","Jing ","Tian ","Zhen ","Jing ","Cheng ","Qing ","Jing ","Jing ","Dian ","Jing ","Tian ","Fei ","Fei ","Kao ","Mi ","Mian ","Mian ","Pao ","Ye ","Tian ","Hui ","Ye ","Ge ","Ding ","Cha ","Jian ","Ren ","Di ","Du ","Wu ","Ren ","Qin ","Jin ","Xue ","Niu ","Ba ","Yin ","Sa ","Na ","Mo ","Zu ","Da ","Ban ","Yi ","Yao ","Tao ","Tuo ","Jia ","Hong ","Pao ","Yang ","Tomo ","Yin ","Jia ","Tao ","Ji ","Xie ","An ","An ","Hen ","Gong ","Kohaze ","Da ","Qiao ","Ting ","Wan ","Ying ","Sui ","Tiao ","Qiao ","Xuan ","Kong ","Beng ","Ta ","Zhang ","Bing ","Kuo ","Ju ","La ","Xie ","Rou ","Bang ","Yi ","Qiu ","Qiu ","He ","Xiao ","Mu ","Ju ","Jian ","Bian ","Di ","Jian ","On ","Tao ","Gou ","Ta ","Bei ","Xie ","Pan ","Ge ","Bi ","Kuo ","Tang ","Lou ","Gui ","Qiao ","Xue ","Ji ","Jian ","Jiang ","Chan ","Da ","Huo ","Xian ","Qian ","Du ","Wa ","Jian ","Lan ","Wei ","Ren ","Fu ","Mei ","Juan ","Ge ","Wei ","Qiao ","Han ","Chang ","[?] ","Rou ","Xun ","She ","Wei ","Ge ","Bei ","Tao ","Gou ","Yun ","[?] ","Bi ","Wei ","Hui ","Du ","Wa ","Du ","Wei ","Ren ","Fu ","Han ","Wei ","Yun ","Tao ","Jiu ","Jiu ","Xian ","Xie ","Xian ","Ji ","Yin ","Za ","Yun ","Shao ","Le ","Peng ","Heng ","Ying ","Yun ","Peng ","Yin ","Yin ","Xiang " ];
|
|
|
|
},{}],144:[function(require,module,exports){
|
|
module.exports = [ "Hu ","Ye ","Ding ","Qing ","Pan ","Xiang ","Shun ","Han ","Xu ","Yi ","Xu ","Gu ","Song ","Kui ","Qi ","Hang ","Yu ","Wan ","Ban ","Dun ","Di ","Dan ","Pan ","Po ","Ling ","Ce ","Jing ","Lei ","He ","Qiao ","E ","E ","Wei ","Jie ","Gua ","Shen ","Yi ","Shen ","Hai ","Dui ","Pian ","Ping ","Lei ","Fu ","Jia ","Tou ","Hui ","Kui ","Jia ","Le ","Tian ","Cheng ","Ying ","Jun ","Hu ","Han ","Jing ","Tui ","Tui ","Pin ","Lai ","Tui ","Zi ","Zi ","Chui ","Ding ","Lai ","Yan ","Han ","Jian ","Ke ","Cui ","Jiong ","Qin ","Yi ","Sai ","Ti ","E ","E ","Yan ","Hun ","Kan ","Yong ","Zhuan ","Yan ","Xian ","Xin ","Yi ","Yuan ","Sang ","Dian ","Dian ","Jiang ","Ku ","Lei ","Liao ","Piao ","Yi ","Man ","Qi ","Rao ","Hao ","Qiao ","Gu ","Xun ","Qian ","Hui ","Zhan ","Ru ","Hong ","Bin ","Xian ","Pin ","Lu ","Lan ","Nie ","Quan ","Ye ","Ding ","Qing ","Han ","Xiang ","Shun ","Xu ","Xu ","Wan ","Gu ","Dun ","Qi ","Ban ","Song ","Hang ","Yu ","Lu ","Ling ","Po ","Jing ","Jie ","Jia ","Tian ","Han ","Ying ","Jiong ","Hai ","Yi ","Pin ","Hui ","Tui ","Han ","Ying ","Ying ","Ke ","Ti ","Yong ","E ","Zhuan ","Yan ","E ","Nie ","Man ","Dian ","Sang ","Hao ","Lei ","Zhan ","Ru ","Pin ","Quan ","Feng ","Biao ","Oroshi ","Fu ","Xia ","Zhan ","Biao ","Sa ","Ba ","Tai ","Lie ","Gua ","Xuan ","Shao ","Ju ","Bi ","Si ","Wei ","Yang ","Yao ","Sou ","Kai ","Sao ","Fan ","Liu ","Xi ","Liao ","Piao ","Piao ","Liu ","Biao ","Biao ","Biao ","Liao ","[?] ","Se ","Feng ","Biao ","Feng ","Yang ","Zhan ","Biao ","Sa ","Ju ","Si ","Sou ","Yao ","Liu ","Piao ","Biao ","Biao ","Fei ","Fan ","Fei ","Fei ","Shi ","Shi ","Can ","Ji ","Ding ","Si ","Tuo ","Zhan ","Sun ","Xiang ","Tun ","Ren ","Yu ","Juan ","Chi ","Yin ","Fan ","Fan ","Sun ","Yin ","Zhu ","Yi ","Zhai ","Bi ","Jie ","Tao ","Liu ","Ci ","Tie ","Si ","Bao ","Shi ","Duo " ];
|
|
|
|
},{}],145:[function(require,module,exports){
|
|
module.exports = [ "Hai ","Ren ","Tian ","Jiao ","Jia ","Bing ","Yao ","Tong ","Ci ","Xiang ","Yang ","Yang ","Er ","Yan ","Le ","Yi ","Can ","Bo ","Nei ","E ","Bu ","Jun ","Dou ","Su ","Yu ","Shi ","Yao ","Hun ","Guo ","Shi ","Jian ","Zhui ","Bing ","Xian ","Bu ","Ye ","Tan ","Fei ","Zhang ","Wei ","Guan ","E ","Nuan ","Hun ","Hu ","Huang ","Tie ","Hui ","Jian ","Hou ","He ","Xing ","Fen ","Wei ","Gu ","Cha ","Song ","Tang ","Bo ","Gao ","Xi ","Kui ","Liu ","Sou ","Tao ","Ye ","Yun ","Mo ","Tang ","Man ","Bi ","Yu ","Xiu ","Jin ","San ","Kui ","Zhuan ","Shan ","Chi ","Dan ","Yi ","Ji ","Rao ","Cheng ","Yong ","Tao ","Hui ","Xiang ","Zhan ","Fen ","Hai ","Meng ","Yan ","Mo ","Chan ","Xiang ","Luo ","Zuan ","Nang ","Shi ","Ding ","Ji ","Tuo ","Xing ","Tun ","Xi ","Ren ","Yu ","Chi ","Fan ","Yin ","Jian ","Shi ","Bao ","Si ","Duo ","Yi ","Er ","Rao ","Xiang ","Jia ","Le ","Jiao ","Yi ","Bing ","Bo ","Dou ","E ","Yu ","Nei ","Jun ","Guo ","Hun ","Xian ","Guan ","Cha ","Kui ","Gu ","Sou ","Chan ","Ye ","Mo ","Bo ","Liu ","Xiu ","Jin ","Man ","San ","Zhuan ","Nang ","Shou ","Kui ","Guo ","Xiang ","Fen ","Ba ","Ni ","Bi ","Bo ","Tu ","Han ","Fei ","Jian ","An ","Ai ","Fu ","Xian ","Wen ","Xin ","Fen ","Bin ","Xing ","Ma ","Yu ","Feng ","Han ","Di ","Tuo ","Tuo ","Chi ","Xun ","Zhu ","Zhi ","Pei ","Xin ","Ri ","Sa ","Yin ","Wen ","Zhi ","Dan ","Lu ","You ","Bo ","Bao ","Kuai ","Tuo ","Yi ","Qu ","[?] ","Qu ","Jiong ","Bo ","Zhao ","Yuan ","Peng ","Zhou ","Ju ","Zhu ","Nu ","Ju ","Pi ","Zang ","Jia ","Ling ","Zhen ","Tai ","Fu ","Yang ","Shi ","Bi ","Tuo ","Tuo ","Si ","Liu ","Ma ","Pian ","Tao ","Zhi ","Rong ","Teng ","Dong ","Xun ","Quan ","Shen ","Jiong ","Er ","Hai ","Bo ","Zhu ","Yin ","Luo ","Shuu ","Dan ","Xie ","Liu ","Ju ","Song ","Qin ","Mang ","Liang ","Han ","Tu ","Xuan ","Tui ","Jun " ];
|
|
|
|
},{}],146:[function(require,module,exports){
|
|
module.exports = [ "E ","Cheng ","Xin ","Ai ","Lu ","Zhui ","Zhou ","She ","Pian ","Kun ","Tao ","Lai ","Zong ","Ke ","Qi ","Qi ","Yan ","Fei ","Sao ","Yan ","Jie ","Yao ","Wu ","Pian ","Cong ","Pian ","Qian ","Fei ","Huang ","Jian ","Huo ","Yu ","Ti ","Quan ","Xia ","Zong ","Kui ","Rou ","Si ","Gua ","Tuo ","Kui ","Sou ","Qian ","Cheng ","Zhi ","Liu ","Pang ","Teng ","Xi ","Cao ","Du ","Yan ","Yuan ","Zou ","Sao ","Shan ","Li ","Zhi ","Shuang ","Lu ","Xi ","Luo ","Zhang ","Mo ","Ao ","Can ","Piao ","Cong ","Qu ","Bi ","Zhi ","Yu ","Xu ","Hua ","Bo ","Su ","Xiao ","Lin ","Chan ","Dun ","Liu ","Tuo ","Zeng ","Tan ","Jiao ","Tie ","Yan ","Luo ","Zhan ","Jing ","Yi ","Ye ","Tuo ","Bin ","Zou ","Yan ","Peng ","Lu ","Teng ","Xiang ","Ji ","Shuang ","Ju ","Xi ","Huan ","Li ","Biao ","Ma ","Yu ","Tuo ","Xun ","Chi ","Qu ","Ri ","Bo ","Lu ","Zang ","Shi ","Si ","Fu ","Ju ","Zou ","Zhu ","Tuo ","Nu ","Jia ","Yi ","Tai ","Xiao ","Ma ","Yin ","Jiao ","Hua ","Luo ","Hai ","Pian ","Biao ","Li ","Cheng ","Yan ","Xin ","Qin ","Jun ","Qi ","Qi ","Ke ","Zhui ","Zong ","Su ","Can ","Pian ","Zhi ","Kui ","Sao ","Wu ","Ao ","Liu ","Qian ","Shan ","Piao ","Luo ","Cong ","Chan ","Zou ","Ji ","Shuang ","Xiang ","Gu ","Wei ","Wei ","Wei ","Yu ","Gan ","Yi ","Ang ","Tou ","Xie ","Bao ","Bi ","Chi ","Ti ","Di ","Ku ","Hai ","Qiao ","Gou ","Kua ","Ge ","Tui ","Geng ","Pian ","Bi ","Ke ","Ka ","Yu ","Sui ","Lou ","Bo ","Xiao ","Pang ","Bo ","Ci ","Kuan ","Bin ","Mo ","Liao ","Lou ","Nao ","Du ","Zang ","Sui ","Ti ","Bin ","Kuan ","Lu ","Gao ","Gao ","Qiao ","Kao ","Qiao ","Lao ","Zao ","Biao ","Kun ","Kun ","Ti ","Fang ","Xiu ","Ran ","Mao ","Dan ","Kun ","Bin ","Fa ","Tiao ","Peng ","Zi ","Fa ","Ran ","Ti ","Pao ","Pi ","Mao ","Fu ","Er ","Rong ","Qu ","Gong ","Xiu ","Gua ","Ji ","Peng ","Zhua ","Shao ","Sha " ];
|
|
|
|
},{}],147:[function(require,module,exports){
|
|
module.exports = [ "Ti ","Li ","Bin ","Zong ","Ti ","Peng ","Song ","Zheng ","Quan ","Zong ","Shun ","Jian ","Duo ","Hu ","La ","Jiu ","Qi ","Lian ","Zhen ","Bin ","Peng ","Mo ","San ","Man ","Man ","Seng ","Xu ","Lie ","Qian ","Qian ","Nong ","Huan ","Kuai ","Ning ","Bin ","Lie ","Rang ","Dou ","Dou ","Nao ","Hong ","Xi ","Dou ","Han ","Dou ","Dou ","Jiu ","Chang ","Yu ","Yu ","Li ","Juan ","Fu ","Qian ","Gui ","Zong ","Liu ","Gui ","Shang ","Yu ","Gui ","Mei ","Ji ","Qi ","Jie ","Kui ","Hun ","Ba ","Po ","Mei ","Xu ","Yan ","Xiao ","Liang ","Yu ","Tui ","Qi ","Wang ","Liang ","Wei ","Jian ","Chi ","Piao ","Bi ","Mo ","Ji ","Xu ","Chou ","Yan ","Zhan ","Yu ","Dao ","Ren ","Ji ","Eri ","Gong ","Tuo ","Diao ","Ji ","Xu ","E ","E ","Sha ","Hang ","Tun ","Mo ","Jie ","Shen ","Fan ","Yuan ","Bi ","Lu ","Wen ","Hu ","Lu ","Za ","Fang ","Fen ","Na ","You ","Namazu ","Todo ","He ","Xia ","Qu ","Han ","Pi ","Ling ","Tuo ","Bo ","Qiu ","Ping ","Fu ","Bi ","Ji ","Wei ","Ju ","Diao ","Bo ","You ","Gun ","Pi ","Nian ","Xing ","Tai ","Bao ","Fu ","Zha ","Ju ","Gu ","Kajika ","Tong ","[?] ","Ta ","Jie ","Shu ","Hou ","Xiang ","Er ","An ","Wei ","Tiao ","Zhu ","Yin ","Lie ","Luo ","Tong ","Yi ","Qi ","Bing ","Wei ","Jiao ","Bu ","Gui ","Xian ","Ge ","Hui ","Bora ","Mate ","Kao ","Gori ","Duo ","Jun ","Ti ","Man ","Xiao ","Za ","Sha ","Qin ","Yu ","Nei ","Zhe ","Gun ","Geng ","Su ","Wu ","Qiu ","Ting ","Fu ","Wan ","You ","Li ","Sha ","Sha ","Gao ","Meng ","Ugui ","Asari ","Subashiri ","Kazunoko ","Yong ","Ni ","Zi ","Qi ","Qing ","Xiang ","Nei ","Chun ","Ji ","Diao ","Qie ","Gu ","Zhou ","Dong ","Lai ","Fei ","Ni ","Yi ","Kun ","Lu ","Jiu ","Chang ","Jing ","Lun ","Ling ","Zou ","Li ","Meng ","Zong ","Zhi ","Nian ","Shachi ","Dojou ","Sukesou ","Shi ","Shen ","Hun ","Shi ","Hou ","Xing ","Zhu ","La ","Zong ","Ji ","Bian ","Bian " ];
|
|
|
|
},{}],148:[function(require,module,exports){
|
|
module.exports = [ "Huan ","Quan ","Ze ","Wei ","Wei ","Yu ","Qun ","Rou ","Die ","Huang ","Lian ","Yan ","Qiu ","Qiu ","Jian ","Bi ","E ","Yang ","Fu ","Sai ","Jian ","Xia ","Tuo ","Hu ","Muroaji ","Ruo ","Haraka ","Wen ","Jian ","Hao ","Wu ","Fang ","Sao ","Liu ","Ma ","Shi ","Shi ","Yin ","Z ","Teng ","Ta ","Yao ","Ge ","Rong ","Qian ","Qi ","Wen ","Ruo ","Hatahata ","Lian ","Ao ","Le ","Hui ","Min ","Ji ","Tiao ","Qu ","Jian ","Sao ","Man ","Xi ","Qiu ","Biao ","Ji ","Ji ","Zhu ","Jiang ","Qiu ","Zhuan ","Yong ","Zhang ","Kang ","Xue ","Bie ","Jue ","Qu ","Xiang ","Bo ","Jiao ","Xun ","Su ","Huang ","Zun ","Shan ","Shan ","Fan ","Jue ","Lin ","Xun ","Miao ","Xi ","Eso ","Kyou ","Fen ","Guan ","Hou ","Kuai ","Zei ","Sao ","Zhan ","Gan ","Gui ","Sheng ","Li ","Chang ","Hatahata ","Shiira ","Mutsu ","Ru ","Ji ","Xu ","Huo ","Shiira ","Li ","Lie ","Li ","Mie ","Zhen ","Xiang ","E ","Lu ","Guan ","Li ","Xian ","Yu ","Dao ","Ji ","You ","Tun ","Lu ","Fang ","Ba ","He ","Bo ","Ping ","Nian ","Lu ","You ","Zha ","Fu ","Bo ","Bao ","Hou ","Pi ","Tai ","Gui ","Jie ","Kao ","Wei ","Er ","Tong ","Ze ","Hou ","Kuai ","Ji ","Jiao ","Xian ","Za ","Xiang ","Xun ","Geng ","Li ","Lian ","Jian ","Li ","Shi ","Tiao ","Gun ","Sha ","Wan ","Jun ","Ji ","Yong ","Qing ","Ling ","Qi ","Zou ","Fei ","Kun ","Chang ","Gu ","Ni ","Nian ","Diao ","Jing ","Shen ","Shi ","Zi ","Fen ","Die ","Bi ","Chang ","Shi ","Wen ","Wei ","Sai ","E ","Qiu ","Fu ","Huang ","Quan ","Jiang ","Bian ","Sao ","Ao ","Qi ","Ta ","Yin ","Yao ","Fang ","Jian ","Le ","Biao ","Xue ","Bie ","Man ","Min ","Yong ","Wei ","Xi ","Jue ","Shan ","Lin ","Zun ","Huo ","Gan ","Li ","Zhan ","Guan ","Niao ","Yi ","Fu ","Li ","Jiu ","Bu ","Yan ","Fu ","Diao ","Ji ","Feng ","Nio ","Gan ","Shi ","Feng ","Ming ","Bao ","Yuan ","Zhi ","Hu ","Qin ","Fu ","Fen ","Wen ","Jian ","Shi ","Yu " ];
|
|
|
|
},{}],149:[function(require,module,exports){
|
|
module.exports = [ "Fou ","Yiao ","Jue ","Jue ","Pi ","Huan ","Zhen ","Bao ","Yan ","Ya ","Zheng ","Fang ","Feng ","Wen ","Ou ","Te ","Jia ","Nu ","Ling ","Mie ","Fu ","Tuo ","Wen ","Li ","Bian ","Zhi ","Ge ","Yuan ","Zi ","Qu ","Xiao ","Zhi ","Dan ","Ju ","You ","Gu ","Zhong ","Yu ","Yang ","Rong ","Ya ","Tie ","Yu ","Shigi ","Ying ","Zhui ","Wu ","Er ","Gua ","Ai ","Zhi ","Yan ","Heng ","Jiao ","Ji ","Lie ","Zhu ","Ren ","Yi ","Hong ","Luo ","Ru ","Mou ","Ge ","Ren ","Jiao ","Xiu ","Zhou ","Zhi ","Luo ","Chidori ","Toki ","Ten ","Luan ","Jia ","Ji ","Yu ","Huan ","Tuo ","Bu ","Wu ","Juan ","Yu ","Bo ","Xun ","Xun ","Bi ","Xi ","Jun ","Ju ","Tu ","Jing ","Ti ","E ","E ","Kuang ","Hu ","Wu ","Shen ","Lai ","Ikaruga ","Kakesu ","Lu ","Ping ","Shu ","Fu ","An ","Zhao ","Peng ","Qin ","Qian ","Bei ","Diao ","Lu ","Que ","Jian ","Ju ","Tu ","Ya ","Yuan ","Qi ","Li ","Ye ","Zhui ","Kong ","Zhui ","Kun ","Sheng ","Qi ","Jing ","Yi ","Yi ","Jing ","Zi ","Lai ","Dong ","Qi ","Chun ","Geng ","Ju ","Qu ","Isuka ","Kikuitadaki ","Ji ","Shu ","[?] ","Chi ","Miao ","Rou ","An ","Qiu ","Ti ","Hu ","Ti ","E ","Jie ","Mao ","Fu ","Chun ","Tu ","Yan ","He ","Yuan ","Pian ","Yun ","Mei ","Hu ","Ying ","Dun ","Mu ","Ju ","Tsugumi ","Cang ","Fang ","Gu ","Ying ","Yuan ","Xuan ","Weng ","Shi ","He ","Chu ","Tang ","Xia ","Ruo ","Liu ","Ji ","Gu ","Jian ","Zhun ","Han ","Zi ","Zi ","Ni ","Yao ","Yan ","Ji ","Li ","Tian ","Kou ","Ti ","Ti ","Ni ","Tu ","Ma ","Jiao ","Gao ","Tian ","Chen ","Li ","Zhuan ","Zhe ","Ao ","Yao ","Yi ","Ou ","Chi ","Zhi ","Liao ","Rong ","Lou ","Bi ","Shuang ","Zhuo ","Yu ","Wu ","Jue ","Yin ","Quan ","Si ","Jiao ","Yi ","Hua ","Bi ","Ying ","Su ","Huang ","Fan ","Jiao ","Liao ","Yan ","Kao ","Jiu ","Xian ","Xian ","Tu ","Mai ","Zun ","Yu ","Ying ","Lu ","Tuan ","Xian ","Xue ","Yi ","Pi " ];
|
|
|
|
},{}],150:[function(require,module,exports){
|
|
module.exports = [ "Shu ","Luo ","Qi ","Yi ","Ji ","Zhe ","Yu ","Zhan ","Ye ","Yang ","Pi ","Ning ","Huo ","Mi ","Ying ","Meng ","Di ","Yue ","Yu ","Lei ","Bao ","Lu ","He ","Long ","Shuang ","Yue ","Ying ","Guan ","Qu ","Li ","Luan ","Niao ","Jiu ","Ji ","Yuan ","Ming ","Shi ","Ou ","Ya ","Cang ","Bao ","Zhen ","Gu ","Dong ","Lu ","Ya ","Xiao ","Yang ","Ling ","Zhi ","Qu ","Yuan ","Xue ","Tuo ","Si ","Zhi ","Er ","Gua ","Xiu ","Heng ","Zhou ","Ge ","Luan ","Hong ","Wu ","Bo ","Li ","Juan ","Hu ","E ","Yu ","Xian ","Ti ","Wu ","Que ","Miao ","An ","Kun ","Bei ","Peng ","Qian ","Chun ","Geng ","Yuan ","Su ","Hu ","He ","E ","Gu ","Qiu ","Zi ","Mei ","Mu ","Ni ","Yao ","Weng ","Liu ","Ji ","Ni ","Jian ","He ","Yi ","Ying ","Zhe ","Liao ","Liao ","Jiao ","Jiu ","Yu ","Lu ","Xuan ","Zhan ","Ying ","Huo ","Meng ","Guan ","Shuang ","Lu ","Jin ","Ling ","Jian ","Xian ","Cuo ","Jian ","Jian ","Yan ","Cuo ","Lu ","You ","Cu ","Ji ","Biao ","Cu ","Biao ","Zhu ","Jun ","Zhu ","Jian ","Mi ","Mi ","Wu ","Liu ","Chen ","Jun ","Lin ","Ni ","Qi ","Lu ","Jiu ","Jun ","Jing ","Li ","Xiang ","Yan ","Jia ","Mi ","Li ","She ","Zhang ","Lin ","Jing ","Ji ","Ling ","Yan ","Cu ","Mai ","Mai ","Ge ","Chao ","Fu ","Mian ","Mian ","Fu ","Pao ","Qu ","Qu ","Mou ","Fu ","Xian ","Lai ","Qu ","Mian ","[?] ","Feng ","Fu ","Qu ","Mian ","Ma ","Mo ","Mo ","Hui ","Ma ","Zou ","Nen ","Fen ","Huang ","Huang ","Jin ","Guang ","Tian ","Tou ","Heng ","Xi ","Kuang ","Heng ","Shu ","Li ","Nian ","Chi ","Hei ","Hei ","Yi ","Qian ","Dan ","Xi ","Tuan ","Mo ","Mo ","Qian ","Dai ","Chu ","You ","Dian ","Yi ","Xia ","Yan ","Qu ","Mei ","Yan ","Jing ","Yu ","Li ","Dang ","Du ","Can ","Yin ","An ","Yan ","Tan ","An ","Zhen ","Dai ","Can ","Yi ","Mei ","Dan ","Yan ","Du ","Lu ","Zhi ","Fen ","Fu ","Fu ","Min ","Min ","Yuan " ];
|
|
|
|
},{}],151:[function(require,module,exports){
|
|
module.exports = [ "Cu ","Qu ","Chao ","Wa ","Zhu ","Zhi ","Mang ","Ao ","Bie ","Tuo ","Bi ","Yuan ","Chao ","Tuo ","Ding ","Mi ","Nai ","Ding ","Zi ","Gu ","Gu ","Dong ","Fen ","Tao ","Yuan ","Pi ","Chang ","Gao ","Qi ","Yuan ","Tang ","Teng ","Shu ","Shu ","Fen ","Fei ","Wen ","Ba ","Diao ","Tuo ","Tong ","Qu ","Sheng ","Shi ","You ","Shi ","Ting ","Wu ","Nian ","Jing ","Hun ","Ju ","Yan ","Tu ","Ti ","Xi ","Xian ","Yan ","Lei ","Bi ","Yao ","Qiu ","Han ","Wu ","Wu ","Hou ","Xi ","Ge ","Zha ","Xiu ","Weng ","Zha ","Nong ","Nang ","Qi ","Zhai ","Ji ","Zi ","Ji ","Ji ","Qi ","Ji ","Chi ","Chen ","Chen ","He ","Ya ","Ken ","Xie ","Pao ","Cuo ","Shi ","Zi ","Chi ","Nian ","Ju ","Tiao ","Ling ","Ling ","Chu ","Quan ","Xie ","Ken ","Nie ","Jiu ","Yao ","Chuo ","Kun ","Yu ","Chu ","Yi ","Ni ","Cuo ","Zou ","Qu ","Nen ","Xian ","Ou ","E ","Wo ","Yi ","Chuo ","Zou ","Dian ","Chu ","Jin ","Ya ","Chi ","Chen ","He ","Ken ","Ju ","Ling ","Pao ","Tiao ","Zi ","Ken ","Yu ","Chuo ","Qu ","Wo ","Long ","Pang ","Gong ","Pang ","Yan ","Long ","Long ","Gong ","Kan ","Ta ","Ling ","Ta ","Long ","Gong ","Kan ","Gui ","Qiu ","Bie ","Gui ","Yue ","Chui ","He ","Jue ","Xie ","Yu ","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]" ];
|
|
|
|
},{}],152:[function(require,module,exports){
|
|
module.exports = [ "it","ix","i","ip","iet","iex","ie","iep","at","ax","a","ap","uox","uo","uop","ot","ox","o","op","ex","e","wu","bit","bix","bi","bip","biet","biex","bie","biep","bat","bax","ba","bap","buox","buo","buop","bot","box","bo","bop","bex","be","bep","but","bux","bu","bup","burx","bur","byt","byx","by","byp","byrx","byr","pit","pix","pi","pip","piex","pie","piep","pat","pax","pa","pap","puox","puo","puop","pot","pox","po","pop","put","pux","pu","pup","purx","pur","pyt","pyx","py","pyp","pyrx","pyr","bbit","bbix","bbi","bbip","bbiet","bbiex","bbie","bbiep","bbat","bbax","bba","bbap","bbuox","bbuo","bbuop","bbot","bbox","bbo","bbop","bbex","bbe","bbep","bbut","bbux","bbu","bbup","bburx","bbur","bbyt","bbyx","bby","bbyp","nbit","nbix","nbi","nbip","nbiex","nbie","nbiep","nbat","nbax","nba","nbap","nbot","nbox","nbo","nbop","nbut","nbux","nbu","nbup","nburx","nbur","nbyt","nbyx","nby","nbyp","nbyrx","nbyr","hmit","hmix","hmi","hmip","hmiex","hmie","hmiep","hmat","hmax","hma","hmap","hmuox","hmuo","hmuop","hmot","hmox","hmo","hmop","hmut","hmux","hmu","hmup","hmurx","hmur","hmyx","hmy","hmyp","hmyrx","hmyr","mit","mix","mi","mip","miex","mie","miep","mat","max","ma","map","muot","muox","muo","muop","mot","mox","mo","mop","mex","me","mut","mux","mu","mup","murx","mur","myt","myx","my","myp","fit","fix","fi","fip","fat","fax","fa","fap","fox","fo","fop","fut","fux","fu","fup","furx","fur","fyt","fyx","fy","fyp","vit","vix","vi","vip","viet","viex","vie","viep","vat","vax","va","vap","vot","vox","vo","vop","vex","vep","vut","vux","vu","vup","vurx","vur","vyt","vyx","vy","vyp","vyrx","vyr" ];
|
|
|
|
},{}],153:[function(require,module,exports){
|
|
module.exports = [ "dit","dix","di","dip","diex","die","diep","dat","dax","da","dap","duox","duo","dot","dox","do","dop","dex","de","dep","dut","dux","du","dup","durx","dur","tit","tix","ti","tip","tiex","tie","tiep","tat","tax","ta","tap","tuot","tuox","tuo","tuop","tot","tox","to","top","tex","te","tep","tut","tux","tu","tup","turx","tur","ddit","ddix","ddi","ddip","ddiex","ddie","ddiep","ddat","ddax","dda","ddap","dduox","dduo","dduop","ddot","ddox","ddo","ddop","ddex","dde","ddep","ddut","ddux","ddu","ddup","ddurx","ddur","ndit","ndix","ndi","ndip","ndiex","ndie","ndat","ndax","nda","ndap","ndot","ndox","ndo","ndop","ndex","nde","ndep","ndut","ndux","ndu","ndup","ndurx","ndur","hnit","hnix","hni","hnip","hniet","hniex","hnie","hniep","hnat","hnax","hna","hnap","hnuox","hnuo","hnot","hnox","hnop","hnex","hne","hnep","hnut","nit","nix","ni","nip","niex","nie","niep","nax","na","nap","nuox","nuo","nuop","not","nox","no","nop","nex","ne","nep","nut","nux","nu","nup","nurx","nur","hlit","hlix","hli","hlip","hliex","hlie","hliep","hlat","hlax","hla","hlap","hluox","hluo","hluop","hlox","hlo","hlop","hlex","hle","hlep","hlut","hlux","hlu","hlup","hlurx","hlur","hlyt","hlyx","hly","hlyp","hlyrx","hlyr","lit","lix","li","lip","liet","liex","lie","liep","lat","lax","la","lap","luot","luox","luo","luop","lot","lox","lo","lop","lex","le","lep","lut","lux","lu","lup","lurx","lur","lyt","lyx","ly","lyp","lyrx","lyr","git","gix","gi","gip","giet","giex","gie","giep","gat","gax","ga","gap","guot","guox","guo","guop","got","gox","go","gop","get","gex","ge","gep","gut","gux","gu","gup","gurx","gur","kit","kix","ki","kip","kiex","kie","kiep","kat" ];
|
|
|
|
},{}],154:[function(require,module,exports){
|
|
module.exports = [ "kax","ka","kap","kuox","kuo","kuop","kot","kox","ko","kop","ket","kex","ke","kep","kut","kux","ku","kup","kurx","kur","ggit","ggix","ggi","ggiex","ggie","ggiep","ggat","ggax","gga","ggap","gguot","gguox","gguo","gguop","ggot","ggox","ggo","ggop","gget","ggex","gge","ggep","ggut","ggux","ggu","ggup","ggurx","ggur","mgiex","mgie","mgat","mgax","mga","mgap","mguox","mguo","mguop","mgot","mgox","mgo","mgop","mgex","mge","mgep","mgut","mgux","mgu","mgup","mgurx","mgur","hxit","hxix","hxi","hxip","hxiet","hxiex","hxie","hxiep","hxat","hxax","hxa","hxap","hxuot","hxuox","hxuo","hxuop","hxot","hxox","hxo","hxop","hxex","hxe","hxep","ngiex","ngie","ngiep","ngat","ngax","nga","ngap","nguot","nguox","nguo","ngot","ngox","ngo","ngop","ngex","nge","ngep","hit","hiex","hie","hat","hax","ha","hap","huot","huox","huo","huop","hot","hox","ho","hop","hex","he","hep","wat","wax","wa","wap","wuox","wuo","wuop","wox","wo","wop","wex","we","wep","zit","zix","zi","zip","ziex","zie","ziep","zat","zax","za","zap","zuox","zuo","zuop","zot","zox","zo","zop","zex","ze","zep","zut","zux","zu","zup","zurx","zur","zyt","zyx","zy","zyp","zyrx","zyr","cit","cix","ci","cip","ciet","ciex","cie","ciep","cat","cax","ca","cap","cuox","cuo","cuop","cot","cox","co","cop","cex","ce","cep","cut","cux","cu","cup","curx","cur","cyt","cyx","cy","cyp","cyrx","cyr","zzit","zzix","zzi","zzip","zziet","zziex","zzie","zziep","zzat","zzax","zza","zzap","zzox","zzo","zzop","zzex","zze","zzep","zzux","zzu","zzup","zzurx","zzur","zzyt","zzyx","zzy","zzyp","zzyrx","zzyr","nzit","nzix","nzi","nzip","nziex","nzie","nziep","nzat","nzax","nza","nzap","nzuox","nzuo","nzox","nzop","nzex","nze","nzux","nzu" ];
|
|
|
|
},{}],155:[function(require,module,exports){
|
|
module.exports = [ "nzup","nzurx","nzur","nzyt","nzyx","nzy","nzyp","nzyrx","nzyr","sit","six","si","sip","siex","sie","siep","sat","sax","sa","sap","suox","suo","suop","sot","sox","so","sop","sex","se","sep","sut","sux","su","sup","surx","sur","syt","syx","sy","syp","syrx","syr","ssit","ssix","ssi","ssip","ssiex","ssie","ssiep","ssat","ssax","ssa","ssap","ssot","ssox","sso","ssop","ssex","sse","ssep","ssut","ssux","ssu","ssup","ssyt","ssyx","ssy","ssyp","ssyrx","ssyr","zhat","zhax","zha","zhap","zhuox","zhuo","zhuop","zhot","zhox","zho","zhop","zhet","zhex","zhe","zhep","zhut","zhux","zhu","zhup","zhurx","zhur","zhyt","zhyx","zhy","zhyp","zhyrx","zhyr","chat","chax","cha","chap","chuot","chuox","chuo","chuop","chot","chox","cho","chop","chet","chex","che","chep","chux","chu","chup","churx","chur","chyt","chyx","chy","chyp","chyrx","chyr","rrax","rra","rruox","rruo","rrot","rrox","rro","rrop","rret","rrex","rre","rrep","rrut","rrux","rru","rrup","rrurx","rrur","rryt","rryx","rry","rryp","rryrx","rryr","nrat","nrax","nra","nrap","nrox","nro","nrop","nret","nrex","nre","nrep","nrut","nrux","nru","nrup","nrurx","nrur","nryt","nryx","nry","nryp","nryrx","nryr","shat","shax","sha","shap","shuox","shuo","shuop","shot","shox","sho","shop","shet","shex","she","shep","shut","shux","shu","shup","shurx","shur","shyt","shyx","shy","shyp","shyrx","shyr","rat","rax","ra","rap","ruox","ruo","ruop","rot","rox","ro","rop","rex","re","rep","rut","rux","ru","rup","rurx","rur","ryt","ryx","ry","ryp","ryrx","ryr","jit","jix","ji","jip","jiet","jiex","jie","jiep","juot","juox","juo","juop","jot","jox","jo","jop","jut","jux","ju","jup","jurx","jur","jyt","jyx","jy","jyp","jyrx","jyr","qit","qix","qi","qip" ];
|
|
|
|
},{}],156:[function(require,module,exports){
|
|
module.exports = [ "qiet","qiex","qie","qiep","quot","quox","quo","quop","qot","qox","qo","qop","qut","qux","qu","qup","qurx","qur","qyt","qyx","qy","qyp","qyrx","qyr","jjit","jjix","jji","jjip","jjiet","jjiex","jjie","jjiep","jjuox","jjuo","jjuop","jjot","jjox","jjo","jjop","jjut","jjux","jju","jjup","jjurx","jjur","jjyt","jjyx","jjy","jjyp","njit","njix","nji","njip","njiet","njiex","njie","njiep","njuox","njuo","njot","njox","njo","njop","njux","nju","njup","njurx","njur","njyt","njyx","njy","njyp","njyrx","njyr","nyit","nyix","nyi","nyip","nyiet","nyiex","nyie","nyiep","nyuox","nyuo","nyuop","nyot","nyox","nyo","nyop","nyut","nyux","nyu","nyup","xit","xix","xi","xip","xiet","xiex","xie","xiep","xuox","xuo","xot","xox","xo","xop","xyt","xyx","xy","xyp","xyrx","xyr","yit","yix","yi","yip","yiet","yiex","yie","yiep","yuot","yuox","yuo","yuop","yot","yox","yo","yop","yut","yux","yu","yup","yurx","yur","yyt","yyx","yy","yyp","yyrx","yyr","[?]","[?]","[?]","Qot","Li","Kit","Nyip","Cyp","Ssi","Ggop","Gep","Mi","Hxit","Lyr","Bbut","Mop","Yo","Put","Hxuo","Tat","Ga","[?]","[?]","Ddur","Bur","Gguo","Nyop","Tu","Op","Jjut","Zot","Pyt","Hmo","Yit","Vur","Shy","Vep","Za","Jo","[?]","Jjy","Got","Jjie","Wo","Du","Shur","Lie","Cy","Cuop","Cip","Hxop","Shat","[?]","Shop","Che","Zziet","[?]","Ke","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]" ];
|
|
|
|
},{}],157:[function(require,module,exports){
|
|
module.exports = [ "ga","gag","gagg","gags","gan","ganj","ganh","gad","gal","galg","galm","galb","gals","galt","galp","galh","gam","gab","gabs","gas","gass","gang","gaj","gac","gak","gat","gap","gah","gae","gaeg","gaegg","gaegs","gaen","gaenj","gaenh","gaed","gael","gaelg","gaelm","gaelb","gaels","gaelt","gaelp","gaelh","gaem","gaeb","gaebs","gaes","gaess","gaeng","gaej","gaec","gaek","gaet","gaep","gaeh","gya","gyag","gyagg","gyags","gyan","gyanj","gyanh","gyad","gyal","gyalg","gyalm","gyalb","gyals","gyalt","gyalp","gyalh","gyam","gyab","gyabs","gyas","gyass","gyang","gyaj","gyac","gyak","gyat","gyap","gyah","gyae","gyaeg","gyaegg","gyaegs","gyaen","gyaenj","gyaenh","gyaed","gyael","gyaelg","gyaelm","gyaelb","gyaels","gyaelt","gyaelp","gyaelh","gyaem","gyaeb","gyaebs","gyaes","gyaess","gyaeng","gyaej","gyaec","gyaek","gyaet","gyaep","gyaeh","geo","geog","geogg","geogs","geon","geonj","geonh","geod","geol","geolg","geolm","geolb","geols","geolt","geolp","geolh","geom","geob","geobs","geos","geoss","geong","geoj","geoc","geok","geot","geop","geoh","ge","geg","gegg","gegs","gen","genj","genh","ged","gel","gelg","gelm","gelb","gels","gelt","gelp","gelh","gem","geb","gebs","ges","gess","geng","gej","gec","gek","get","gep","geh","gyeo","gyeog","gyeogg","gyeogs","gyeon","gyeonj","gyeonh","gyeod","gyeol","gyeolg","gyeolm","gyeolb","gyeols","gyeolt","gyeolp","gyeolh","gyeom","gyeob","gyeobs","gyeos","gyeoss","gyeong","gyeoj","gyeoc","gyeok","gyeot","gyeop","gyeoh","gye","gyeg","gyegg","gyegs","gyen","gyenj","gyenh","gyed","gyel","gyelg","gyelm","gyelb","gyels","gyelt","gyelp","gyelh","gyem","gyeb","gyebs","gyes","gyess","gyeng","gyej","gyec","gyek","gyet","gyep","gyeh","go","gog","gogg","gogs","gon","gonj","gonh","god","gol","golg","golm","golb","gols","golt","golp","golh","gom","gob","gobs","gos","goss","gong","goj","goc","gok","got","gop","goh","gwa","gwag","gwagg","gwags" ];
|
|
|
|
},{}],158:[function(require,module,exports){
|
|
module.exports = [ "gwan","gwanj","gwanh","gwad","gwal","gwalg","gwalm","gwalb","gwals","gwalt","gwalp","gwalh","gwam","gwab","gwabs","gwas","gwass","gwang","gwaj","gwac","gwak","gwat","gwap","gwah","gwae","gwaeg","gwaegg","gwaegs","gwaen","gwaenj","gwaenh","gwaed","gwael","gwaelg","gwaelm","gwaelb","gwaels","gwaelt","gwaelp","gwaelh","gwaem","gwaeb","gwaebs","gwaes","gwaess","gwaeng","gwaej","gwaec","gwaek","gwaet","gwaep","gwaeh","goe","goeg","goegg","goegs","goen","goenj","goenh","goed","goel","goelg","goelm","goelb","goels","goelt","goelp","goelh","goem","goeb","goebs","goes","goess","goeng","goej","goec","goek","goet","goep","goeh","gyo","gyog","gyogg","gyogs","gyon","gyonj","gyonh","gyod","gyol","gyolg","gyolm","gyolb","gyols","gyolt","gyolp","gyolh","gyom","gyob","gyobs","gyos","gyoss","gyong","gyoj","gyoc","gyok","gyot","gyop","gyoh","gu","gug","gugg","gugs","gun","gunj","gunh","gud","gul","gulg","gulm","gulb","guls","gult","gulp","gulh","gum","gub","gubs","gus","guss","gung","guj","guc","guk","gut","gup","guh","gweo","gweog","gweogg","gweogs","gweon","gweonj","gweonh","gweod","gweol","gweolg","gweolm","gweolb","gweols","gweolt","gweolp","gweolh","gweom","gweob","gweobs","gweos","gweoss","gweong","gweoj","gweoc","gweok","gweot","gweop","gweoh","gwe","gweg","gwegg","gwegs","gwen","gwenj","gwenh","gwed","gwel","gwelg","gwelm","gwelb","gwels","gwelt","gwelp","gwelh","gwem","gweb","gwebs","gwes","gwess","gweng","gwej","gwec","gwek","gwet","gwep","gweh","gwi","gwig","gwigg","gwigs","gwin","gwinj","gwinh","gwid","gwil","gwilg","gwilm","gwilb","gwils","gwilt","gwilp","gwilh","gwim","gwib","gwibs","gwis","gwiss","gwing","gwij","gwic","gwik","gwit","gwip","gwih","gyu","gyug","gyugg","gyugs","gyun","gyunj","gyunh","gyud","gyul","gyulg","gyulm","gyulb","gyuls","gyult","gyulp","gyulh","gyum","gyub","gyubs","gyus","gyuss","gyung","gyuj","gyuc","gyuk","gyut","gyup","gyuh","geu","geug","geugg","geugs","geun","geunj","geunh","geud" ];
|
|
|
|
},{}],159:[function(require,module,exports){
|
|
module.exports = [ "geul","geulg","geulm","geulb","geuls","geult","geulp","geulh","geum","geub","geubs","geus","geuss","geung","geuj","geuc","geuk","geut","geup","geuh","gyi","gyig","gyigg","gyigs","gyin","gyinj","gyinh","gyid","gyil","gyilg","gyilm","gyilb","gyils","gyilt","gyilp","gyilh","gyim","gyib","gyibs","gyis","gyiss","gying","gyij","gyic","gyik","gyit","gyip","gyih","gi","gig","gigg","gigs","gin","ginj","ginh","gid","gil","gilg","gilm","gilb","gils","gilt","gilp","gilh","gim","gib","gibs","gis","giss","ging","gij","gic","gik","git","gip","gih","gga","ggag","ggagg","ggags","ggan","gganj","gganh","ggad","ggal","ggalg","ggalm","ggalb","ggals","ggalt","ggalp","ggalh","ggam","ggab","ggabs","ggas","ggass","ggang","ggaj","ggac","ggak","ggat","ggap","ggah","ggae","ggaeg","ggaegg","ggaegs","ggaen","ggaenj","ggaenh","ggaed","ggael","ggaelg","ggaelm","ggaelb","ggaels","ggaelt","ggaelp","ggaelh","ggaem","ggaeb","ggaebs","ggaes","ggaess","ggaeng","ggaej","ggaec","ggaek","ggaet","ggaep","ggaeh","ggya","ggyag","ggyagg","ggyags","ggyan","ggyanj","ggyanh","ggyad","ggyal","ggyalg","ggyalm","ggyalb","ggyals","ggyalt","ggyalp","ggyalh","ggyam","ggyab","ggyabs","ggyas","ggyass","ggyang","ggyaj","ggyac","ggyak","ggyat","ggyap","ggyah","ggyae","ggyaeg","ggyaegg","ggyaegs","ggyaen","ggyaenj","ggyaenh","ggyaed","ggyael","ggyaelg","ggyaelm","ggyaelb","ggyaels","ggyaelt","ggyaelp","ggyaelh","ggyaem","ggyaeb","ggyaebs","ggyaes","ggyaess","ggyaeng","ggyaej","ggyaec","ggyaek","ggyaet","ggyaep","ggyaeh","ggeo","ggeog","ggeogg","ggeogs","ggeon","ggeonj","ggeonh","ggeod","ggeol","ggeolg","ggeolm","ggeolb","ggeols","ggeolt","ggeolp","ggeolh","ggeom","ggeob","ggeobs","ggeos","ggeoss","ggeong","ggeoj","ggeoc","ggeok","ggeot","ggeop","ggeoh","gge","ggeg","ggegg","ggegs","ggen","ggenj","ggenh","gged","ggel","ggelg","ggelm","ggelb","ggels","ggelt","ggelp","ggelh","ggem","ggeb","ggebs","gges","ggess","ggeng","ggej","ggec","ggek","gget","ggep","ggeh","ggyeo","ggyeog","ggyeogg","ggyeogs","ggyeon","ggyeonj","ggyeonh","ggyeod","ggyeol","ggyeolg","ggyeolm","ggyeolb" ];
|
|
|
|
},{}],160:[function(require,module,exports){
|
|
module.exports = [ "ggyeols","ggyeolt","ggyeolp","ggyeolh","ggyeom","ggyeob","ggyeobs","ggyeos","ggyeoss","ggyeong","ggyeoj","ggyeoc","ggyeok","ggyeot","ggyeop","ggyeoh","ggye","ggyeg","ggyegg","ggyegs","ggyen","ggyenj","ggyenh","ggyed","ggyel","ggyelg","ggyelm","ggyelb","ggyels","ggyelt","ggyelp","ggyelh","ggyem","ggyeb","ggyebs","ggyes","ggyess","ggyeng","ggyej","ggyec","ggyek","ggyet","ggyep","ggyeh","ggo","ggog","ggogg","ggogs","ggon","ggonj","ggonh","ggod","ggol","ggolg","ggolm","ggolb","ggols","ggolt","ggolp","ggolh","ggom","ggob","ggobs","ggos","ggoss","ggong","ggoj","ggoc","ggok","ggot","ggop","ggoh","ggwa","ggwag","ggwagg","ggwags","ggwan","ggwanj","ggwanh","ggwad","ggwal","ggwalg","ggwalm","ggwalb","ggwals","ggwalt","ggwalp","ggwalh","ggwam","ggwab","ggwabs","ggwas","ggwass","ggwang","ggwaj","ggwac","ggwak","ggwat","ggwap","ggwah","ggwae","ggwaeg","ggwaegg","ggwaegs","ggwaen","ggwaenj","ggwaenh","ggwaed","ggwael","ggwaelg","ggwaelm","ggwaelb","ggwaels","ggwaelt","ggwaelp","ggwaelh","ggwaem","ggwaeb","ggwaebs","ggwaes","ggwaess","ggwaeng","ggwaej","ggwaec","ggwaek","ggwaet","ggwaep","ggwaeh","ggoe","ggoeg","ggoegg","ggoegs","ggoen","ggoenj","ggoenh","ggoed","ggoel","ggoelg","ggoelm","ggoelb","ggoels","ggoelt","ggoelp","ggoelh","ggoem","ggoeb","ggoebs","ggoes","ggoess","ggoeng","ggoej","ggoec","ggoek","ggoet","ggoep","ggoeh","ggyo","ggyog","ggyogg","ggyogs","ggyon","ggyonj","ggyonh","ggyod","ggyol","ggyolg","ggyolm","ggyolb","ggyols","ggyolt","ggyolp","ggyolh","ggyom","ggyob","ggyobs","ggyos","ggyoss","ggyong","ggyoj","ggyoc","ggyok","ggyot","ggyop","ggyoh","ggu","ggug","ggugg","ggugs","ggun","ggunj","ggunh","ggud","ggul","ggulg","ggulm","ggulb","gguls","ggult","ggulp","ggulh","ggum","ggub","ggubs","ggus","gguss","ggung","gguj","gguc","gguk","ggut","ggup","gguh","ggweo","ggweog","ggweogg","ggweogs","ggweon","ggweonj","ggweonh","ggweod","ggweol","ggweolg","ggweolm","ggweolb","ggweols","ggweolt","ggweolp","ggweolh","ggweom","ggweob","ggweobs","ggweos","ggweoss","ggweong","ggweoj","ggweoc","ggweok","ggweot","ggweop","ggweoh","ggwe","ggweg","ggwegg","ggwegs","ggwen","ggwenj","ggwenh","ggwed","ggwel","ggwelg","ggwelm","ggwelb","ggwels","ggwelt","ggwelp","ggwelh" ];
|
|
|
|
},{}],161:[function(require,module,exports){
|
|
module.exports = [ "ggwem","ggweb","ggwebs","ggwes","ggwess","ggweng","ggwej","ggwec","ggwek","ggwet","ggwep","ggweh","ggwi","ggwig","ggwigg","ggwigs","ggwin","ggwinj","ggwinh","ggwid","ggwil","ggwilg","ggwilm","ggwilb","ggwils","ggwilt","ggwilp","ggwilh","ggwim","ggwib","ggwibs","ggwis","ggwiss","ggwing","ggwij","ggwic","ggwik","ggwit","ggwip","ggwih","ggyu","ggyug","ggyugg","ggyugs","ggyun","ggyunj","ggyunh","ggyud","ggyul","ggyulg","ggyulm","ggyulb","ggyuls","ggyult","ggyulp","ggyulh","ggyum","ggyub","ggyubs","ggyus","ggyuss","ggyung","ggyuj","ggyuc","ggyuk","ggyut","ggyup","ggyuh","ggeu","ggeug","ggeugg","ggeugs","ggeun","ggeunj","ggeunh","ggeud","ggeul","ggeulg","ggeulm","ggeulb","ggeuls","ggeult","ggeulp","ggeulh","ggeum","ggeub","ggeubs","ggeus","ggeuss","ggeung","ggeuj","ggeuc","ggeuk","ggeut","ggeup","ggeuh","ggyi","ggyig","ggyigg","ggyigs","ggyin","ggyinj","ggyinh","ggyid","ggyil","ggyilg","ggyilm","ggyilb","ggyils","ggyilt","ggyilp","ggyilh","ggyim","ggyib","ggyibs","ggyis","ggyiss","ggying","ggyij","ggyic","ggyik","ggyit","ggyip","ggyih","ggi","ggig","ggigg","ggigs","ggin","gginj","gginh","ggid","ggil","ggilg","ggilm","ggilb","ggils","ggilt","ggilp","ggilh","ggim","ggib","ggibs","ggis","ggiss","gging","ggij","ggic","ggik","ggit","ggip","ggih","na","nag","nagg","nags","nan","nanj","nanh","nad","nal","nalg","nalm","nalb","nals","nalt","nalp","nalh","nam","nab","nabs","nas","nass","nang","naj","nac","nak","nat","nap","nah","nae","naeg","naegg","naegs","naen","naenj","naenh","naed","nael","naelg","naelm","naelb","naels","naelt","naelp","naelh","naem","naeb","naebs","naes","naess","naeng","naej","naec","naek","naet","naep","naeh","nya","nyag","nyagg","nyags","nyan","nyanj","nyanh","nyad","nyal","nyalg","nyalm","nyalb","nyals","nyalt","nyalp","nyalh","nyam","nyab","nyabs","nyas","nyass","nyang","nyaj","nyac","nyak","nyat","nyap","nyah","nyae","nyaeg","nyaegg","nyaegs","nyaen","nyaenj","nyaenh","nyaed","nyael","nyaelg","nyaelm","nyaelb","nyaels","nyaelt","nyaelp","nyaelh","nyaem","nyaeb","nyaebs","nyaes" ];
|
|
|
|
},{}],162:[function(require,module,exports){
|
|
module.exports = [ "nyaess","nyaeng","nyaej","nyaec","nyaek","nyaet","nyaep","nyaeh","neo","neog","neogg","neogs","neon","neonj","neonh","neod","neol","neolg","neolm","neolb","neols","neolt","neolp","neolh","neom","neob","neobs","neos","neoss","neong","neoj","neoc","neok","neot","neop","neoh","ne","neg","negg","negs","nen","nenj","nenh","ned","nel","nelg","nelm","nelb","nels","nelt","nelp","nelh","nem","neb","nebs","nes","ness","neng","nej","nec","nek","net","nep","neh","nyeo","nyeog","nyeogg","nyeogs","nyeon","nyeonj","nyeonh","nyeod","nyeol","nyeolg","nyeolm","nyeolb","nyeols","nyeolt","nyeolp","nyeolh","nyeom","nyeob","nyeobs","nyeos","nyeoss","nyeong","nyeoj","nyeoc","nyeok","nyeot","nyeop","nyeoh","nye","nyeg","nyegg","nyegs","nyen","nyenj","nyenh","nyed","nyel","nyelg","nyelm","nyelb","nyels","nyelt","nyelp","nyelh","nyem","nyeb","nyebs","nyes","nyess","nyeng","nyej","nyec","nyek","nyet","nyep","nyeh","no","nog","nogg","nogs","non","nonj","nonh","nod","nol","nolg","nolm","nolb","nols","nolt","nolp","nolh","nom","nob","nobs","nos","noss","nong","noj","noc","nok","not","nop","noh","nwa","nwag","nwagg","nwags","nwan","nwanj","nwanh","nwad","nwal","nwalg","nwalm","nwalb","nwals","nwalt","nwalp","nwalh","nwam","nwab","nwabs","nwas","nwass","nwang","nwaj","nwac","nwak","nwat","nwap","nwah","nwae","nwaeg","nwaegg","nwaegs","nwaen","nwaenj","nwaenh","nwaed","nwael","nwaelg","nwaelm","nwaelb","nwaels","nwaelt","nwaelp","nwaelh","nwaem","nwaeb","nwaebs","nwaes","nwaess","nwaeng","nwaej","nwaec","nwaek","nwaet","nwaep","nwaeh","noe","noeg","noegg","noegs","noen","noenj","noenh","noed","noel","noelg","noelm","noelb","noels","noelt","noelp","noelh","noem","noeb","noebs","noes","noess","noeng","noej","noec","noek","noet","noep","noeh","nyo","nyog","nyogg","nyogs","nyon","nyonj","nyonh","nyod","nyol","nyolg","nyolm","nyolb","nyols","nyolt","nyolp","nyolh","nyom","nyob","nyobs","nyos","nyoss","nyong","nyoj","nyoc" ];
|
|
|
|
},{}],163:[function(require,module,exports){
|
|
module.exports = [ "nyok","nyot","nyop","nyoh","nu","nug","nugg","nugs","nun","nunj","nunh","nud","nul","nulg","nulm","nulb","nuls","nult","nulp","nulh","num","nub","nubs","nus","nuss","nung","nuj","nuc","nuk","nut","nup","nuh","nweo","nweog","nweogg","nweogs","nweon","nweonj","nweonh","nweod","nweol","nweolg","nweolm","nweolb","nweols","nweolt","nweolp","nweolh","nweom","nweob","nweobs","nweos","nweoss","nweong","nweoj","nweoc","nweok","nweot","nweop","nweoh","nwe","nweg","nwegg","nwegs","nwen","nwenj","nwenh","nwed","nwel","nwelg","nwelm","nwelb","nwels","nwelt","nwelp","nwelh","nwem","nweb","nwebs","nwes","nwess","nweng","nwej","nwec","nwek","nwet","nwep","nweh","nwi","nwig","nwigg","nwigs","nwin","nwinj","nwinh","nwid","nwil","nwilg","nwilm","nwilb","nwils","nwilt","nwilp","nwilh","nwim","nwib","nwibs","nwis","nwiss","nwing","nwij","nwic","nwik","nwit","nwip","nwih","nyu","nyug","nyugg","nyugs","nyun","nyunj","nyunh","nyud","nyul","nyulg","nyulm","nyulb","nyuls","nyult","nyulp","nyulh","nyum","nyub","nyubs","nyus","nyuss","nyung","nyuj","nyuc","nyuk","nyut","nyup","nyuh","neu","neug","neugg","neugs","neun","neunj","neunh","neud","neul","neulg","neulm","neulb","neuls","neult","neulp","neulh","neum","neub","neubs","neus","neuss","neung","neuj","neuc","neuk","neut","neup","neuh","nyi","nyig","nyigg","nyigs","nyin","nyinj","nyinh","nyid","nyil","nyilg","nyilm","nyilb","nyils","nyilt","nyilp","nyilh","nyim","nyib","nyibs","nyis","nyiss","nying","nyij","nyic","nyik","nyit","nyip","nyih","ni","nig","nigg","nigs","nin","ninj","ninh","nid","nil","nilg","nilm","nilb","nils","nilt","nilp","nilh","nim","nib","nibs","nis","niss","ning","nij","nic","nik","nit","nip","nih","da","dag","dagg","dags","dan","danj","danh","dad","dal","dalg","dalm","dalb","dals","dalt","dalp","dalh","dam","dab","dabs","das","dass","dang","daj","dac","dak","dat","dap","dah" ];
|
|
|
|
},{}],164:[function(require,module,exports){
|
|
module.exports = [ "dae","daeg","daegg","daegs","daen","daenj","daenh","daed","dael","daelg","daelm","daelb","daels","daelt","daelp","daelh","daem","daeb","daebs","daes","daess","daeng","daej","daec","daek","daet","daep","daeh","dya","dyag","dyagg","dyags","dyan","dyanj","dyanh","dyad","dyal","dyalg","dyalm","dyalb","dyals","dyalt","dyalp","dyalh","dyam","dyab","dyabs","dyas","dyass","dyang","dyaj","dyac","dyak","dyat","dyap","dyah","dyae","dyaeg","dyaegg","dyaegs","dyaen","dyaenj","dyaenh","dyaed","dyael","dyaelg","dyaelm","dyaelb","dyaels","dyaelt","dyaelp","dyaelh","dyaem","dyaeb","dyaebs","dyaes","dyaess","dyaeng","dyaej","dyaec","dyaek","dyaet","dyaep","dyaeh","deo","deog","deogg","deogs","deon","deonj","deonh","deod","deol","deolg","deolm","deolb","deols","deolt","deolp","deolh","deom","deob","deobs","deos","deoss","deong","deoj","deoc","deok","deot","deop","deoh","de","deg","degg","degs","den","denj","denh","ded","del","delg","delm","delb","dels","delt","delp","delh","dem","deb","debs","des","dess","deng","dej","dec","dek","det","dep","deh","dyeo","dyeog","dyeogg","dyeogs","dyeon","dyeonj","dyeonh","dyeod","dyeol","dyeolg","dyeolm","dyeolb","dyeols","dyeolt","dyeolp","dyeolh","dyeom","dyeob","dyeobs","dyeos","dyeoss","dyeong","dyeoj","dyeoc","dyeok","dyeot","dyeop","dyeoh","dye","dyeg","dyegg","dyegs","dyen","dyenj","dyenh","dyed","dyel","dyelg","dyelm","dyelb","dyels","dyelt","dyelp","dyelh","dyem","dyeb","dyebs","dyes","dyess","dyeng","dyej","dyec","dyek","dyet","dyep","dyeh","do","dog","dogg","dogs","don","donj","donh","dod","dol","dolg","dolm","dolb","dols","dolt","dolp","dolh","dom","dob","dobs","dos","doss","dong","doj","doc","dok","dot","dop","doh","dwa","dwag","dwagg","dwags","dwan","dwanj","dwanh","dwad","dwal","dwalg","dwalm","dwalb","dwals","dwalt","dwalp","dwalh","dwam","dwab","dwabs","dwas","dwass","dwang","dwaj","dwac","dwak","dwat","dwap","dwah","dwae","dwaeg","dwaegg","dwaegs" ];
|
|
|
|
},{}],165:[function(require,module,exports){
|
|
module.exports = [ "dwaen","dwaenj","dwaenh","dwaed","dwael","dwaelg","dwaelm","dwaelb","dwaels","dwaelt","dwaelp","dwaelh","dwaem","dwaeb","dwaebs","dwaes","dwaess","dwaeng","dwaej","dwaec","dwaek","dwaet","dwaep","dwaeh","doe","doeg","doegg","doegs","doen","doenj","doenh","doed","doel","doelg","doelm","doelb","doels","doelt","doelp","doelh","doem","doeb","doebs","does","doess","doeng","doej","doec","doek","doet","doep","doeh","dyo","dyog","dyogg","dyogs","dyon","dyonj","dyonh","dyod","dyol","dyolg","dyolm","dyolb","dyols","dyolt","dyolp","dyolh","dyom","dyob","dyobs","dyos","dyoss","dyong","dyoj","dyoc","dyok","dyot","dyop","dyoh","du","dug","dugg","dugs","dun","dunj","dunh","dud","dul","dulg","dulm","dulb","duls","dult","dulp","dulh","dum","dub","dubs","dus","duss","dung","duj","duc","duk","dut","dup","duh","dweo","dweog","dweogg","dweogs","dweon","dweonj","dweonh","dweod","dweol","dweolg","dweolm","dweolb","dweols","dweolt","dweolp","dweolh","dweom","dweob","dweobs","dweos","dweoss","dweong","dweoj","dweoc","dweok","dweot","dweop","dweoh","dwe","dweg","dwegg","dwegs","dwen","dwenj","dwenh","dwed","dwel","dwelg","dwelm","dwelb","dwels","dwelt","dwelp","dwelh","dwem","dweb","dwebs","dwes","dwess","dweng","dwej","dwec","dwek","dwet","dwep","dweh","dwi","dwig","dwigg","dwigs","dwin","dwinj","dwinh","dwid","dwil","dwilg","dwilm","dwilb","dwils","dwilt","dwilp","dwilh","dwim","dwib","dwibs","dwis","dwiss","dwing","dwij","dwic","dwik","dwit","dwip","dwih","dyu","dyug","dyugg","dyugs","dyun","dyunj","dyunh","dyud","dyul","dyulg","dyulm","dyulb","dyuls","dyult","dyulp","dyulh","dyum","dyub","dyubs","dyus","dyuss","dyung","dyuj","dyuc","dyuk","dyut","dyup","dyuh","deu","deug","deugg","deugs","deun","deunj","deunh","deud","deul","deulg","deulm","deulb","deuls","deult","deulp","deulh","deum","deub","deubs","deus","deuss","deung","deuj","deuc","deuk","deut","deup","deuh","dyi","dyig","dyigg","dyigs","dyin","dyinj","dyinh","dyid" ];
|
|
|
|
},{}],166:[function(require,module,exports){
|
|
module.exports = [ "dyil","dyilg","dyilm","dyilb","dyils","dyilt","dyilp","dyilh","dyim","dyib","dyibs","dyis","dyiss","dying","dyij","dyic","dyik","dyit","dyip","dyih","di","dig","digg","digs","din","dinj","dinh","did","dil","dilg","dilm","dilb","dils","dilt","dilp","dilh","dim","dib","dibs","dis","diss","ding","dij","dic","dik","dit","dip","dih","dda","ddag","ddagg","ddags","ddan","ddanj","ddanh","ddad","ddal","ddalg","ddalm","ddalb","ddals","ddalt","ddalp","ddalh","ddam","ddab","ddabs","ddas","ddass","ddang","ddaj","ddac","ddak","ddat","ddap","ddah","ddae","ddaeg","ddaegg","ddaegs","ddaen","ddaenj","ddaenh","ddaed","ddael","ddaelg","ddaelm","ddaelb","ddaels","ddaelt","ddaelp","ddaelh","ddaem","ddaeb","ddaebs","ddaes","ddaess","ddaeng","ddaej","ddaec","ddaek","ddaet","ddaep","ddaeh","ddya","ddyag","ddyagg","ddyags","ddyan","ddyanj","ddyanh","ddyad","ddyal","ddyalg","ddyalm","ddyalb","ddyals","ddyalt","ddyalp","ddyalh","ddyam","ddyab","ddyabs","ddyas","ddyass","ddyang","ddyaj","ddyac","ddyak","ddyat","ddyap","ddyah","ddyae","ddyaeg","ddyaegg","ddyaegs","ddyaen","ddyaenj","ddyaenh","ddyaed","ddyael","ddyaelg","ddyaelm","ddyaelb","ddyaels","ddyaelt","ddyaelp","ddyaelh","ddyaem","ddyaeb","ddyaebs","ddyaes","ddyaess","ddyaeng","ddyaej","ddyaec","ddyaek","ddyaet","ddyaep","ddyaeh","ddeo","ddeog","ddeogg","ddeogs","ddeon","ddeonj","ddeonh","ddeod","ddeol","ddeolg","ddeolm","ddeolb","ddeols","ddeolt","ddeolp","ddeolh","ddeom","ddeob","ddeobs","ddeos","ddeoss","ddeong","ddeoj","ddeoc","ddeok","ddeot","ddeop","ddeoh","dde","ddeg","ddegg","ddegs","dden","ddenj","ddenh","dded","ddel","ddelg","ddelm","ddelb","ddels","ddelt","ddelp","ddelh","ddem","ddeb","ddebs","ddes","ddess","ddeng","ddej","ddec","ddek","ddet","ddep","ddeh","ddyeo","ddyeog","ddyeogg","ddyeogs","ddyeon","ddyeonj","ddyeonh","ddyeod","ddyeol","ddyeolg","ddyeolm","ddyeolb","ddyeols","ddyeolt","ddyeolp","ddyeolh","ddyeom","ddyeob","ddyeobs","ddyeos","ddyeoss","ddyeong","ddyeoj","ddyeoc","ddyeok","ddyeot","ddyeop","ddyeoh","ddye","ddyeg","ddyegg","ddyegs","ddyen","ddyenj","ddyenh","ddyed","ddyel","ddyelg","ddyelm","ddyelb" ];
|
|
|
|
},{}],167:[function(require,module,exports){
|
|
module.exports = [ "ddyels","ddyelt","ddyelp","ddyelh","ddyem","ddyeb","ddyebs","ddyes","ddyess","ddyeng","ddyej","ddyec","ddyek","ddyet","ddyep","ddyeh","ddo","ddog","ddogg","ddogs","ddon","ddonj","ddonh","ddod","ddol","ddolg","ddolm","ddolb","ddols","ddolt","ddolp","ddolh","ddom","ddob","ddobs","ddos","ddoss","ddong","ddoj","ddoc","ddok","ddot","ddop","ddoh","ddwa","ddwag","ddwagg","ddwags","ddwan","ddwanj","ddwanh","ddwad","ddwal","ddwalg","ddwalm","ddwalb","ddwals","ddwalt","ddwalp","ddwalh","ddwam","ddwab","ddwabs","ddwas","ddwass","ddwang","ddwaj","ddwac","ddwak","ddwat","ddwap","ddwah","ddwae","ddwaeg","ddwaegg","ddwaegs","ddwaen","ddwaenj","ddwaenh","ddwaed","ddwael","ddwaelg","ddwaelm","ddwaelb","ddwaels","ddwaelt","ddwaelp","ddwaelh","ddwaem","ddwaeb","ddwaebs","ddwaes","ddwaess","ddwaeng","ddwaej","ddwaec","ddwaek","ddwaet","ddwaep","ddwaeh","ddoe","ddoeg","ddoegg","ddoegs","ddoen","ddoenj","ddoenh","ddoed","ddoel","ddoelg","ddoelm","ddoelb","ddoels","ddoelt","ddoelp","ddoelh","ddoem","ddoeb","ddoebs","ddoes","ddoess","ddoeng","ddoej","ddoec","ddoek","ddoet","ddoep","ddoeh","ddyo","ddyog","ddyogg","ddyogs","ddyon","ddyonj","ddyonh","ddyod","ddyol","ddyolg","ddyolm","ddyolb","ddyols","ddyolt","ddyolp","ddyolh","ddyom","ddyob","ddyobs","ddyos","ddyoss","ddyong","ddyoj","ddyoc","ddyok","ddyot","ddyop","ddyoh","ddu","ddug","ddugg","ddugs","ddun","ddunj","ddunh","ddud","ddul","ddulg","ddulm","ddulb","dduls","ddult","ddulp","ddulh","ddum","ddub","ddubs","ddus","dduss","ddung","dduj","dduc","dduk","ddut","ddup","dduh","ddweo","ddweog","ddweogg","ddweogs","ddweon","ddweonj","ddweonh","ddweod","ddweol","ddweolg","ddweolm","ddweolb","ddweols","ddweolt","ddweolp","ddweolh","ddweom","ddweob","ddweobs","ddweos","ddweoss","ddweong","ddweoj","ddweoc","ddweok","ddweot","ddweop","ddweoh","ddwe","ddweg","ddwegg","ddwegs","ddwen","ddwenj","ddwenh","ddwed","ddwel","ddwelg","ddwelm","ddwelb","ddwels","ddwelt","ddwelp","ddwelh","ddwem","ddweb","ddwebs","ddwes","ddwess","ddweng","ddwej","ddwec","ddwek","ddwet","ddwep","ddweh","ddwi","ddwig","ddwigg","ddwigs","ddwin","ddwinj","ddwinh","ddwid","ddwil","ddwilg","ddwilm","ddwilb","ddwils","ddwilt","ddwilp","ddwilh" ];
|
|
|
|
},{}],168:[function(require,module,exports){
|
|
module.exports = [ "ddwim","ddwib","ddwibs","ddwis","ddwiss","ddwing","ddwij","ddwic","ddwik","ddwit","ddwip","ddwih","ddyu","ddyug","ddyugg","ddyugs","ddyun","ddyunj","ddyunh","ddyud","ddyul","ddyulg","ddyulm","ddyulb","ddyuls","ddyult","ddyulp","ddyulh","ddyum","ddyub","ddyubs","ddyus","ddyuss","ddyung","ddyuj","ddyuc","ddyuk","ddyut","ddyup","ddyuh","ddeu","ddeug","ddeugg","ddeugs","ddeun","ddeunj","ddeunh","ddeud","ddeul","ddeulg","ddeulm","ddeulb","ddeuls","ddeult","ddeulp","ddeulh","ddeum","ddeub","ddeubs","ddeus","ddeuss","ddeung","ddeuj","ddeuc","ddeuk","ddeut","ddeup","ddeuh","ddyi","ddyig","ddyigg","ddyigs","ddyin","ddyinj","ddyinh","ddyid","ddyil","ddyilg","ddyilm","ddyilb","ddyils","ddyilt","ddyilp","ddyilh","ddyim","ddyib","ddyibs","ddyis","ddyiss","ddying","ddyij","ddyic","ddyik","ddyit","ddyip","ddyih","ddi","ddig","ddigg","ddigs","ddin","ddinj","ddinh","ddid","ddil","ddilg","ddilm","ddilb","ddils","ddilt","ddilp","ddilh","ddim","ddib","ddibs","ddis","ddiss","dding","ddij","ddic","ddik","ddit","ddip","ddih","ra","rag","ragg","rags","ran","ranj","ranh","rad","ral","ralg","ralm","ralb","rals","ralt","ralp","ralh","ram","rab","rabs","ras","rass","rang","raj","rac","rak","rat","rap","rah","rae","raeg","raegg","raegs","raen","raenj","raenh","raed","rael","raelg","raelm","raelb","raels","raelt","raelp","raelh","raem","raeb","raebs","raes","raess","raeng","raej","raec","raek","raet","raep","raeh","rya","ryag","ryagg","ryags","ryan","ryanj","ryanh","ryad","ryal","ryalg","ryalm","ryalb","ryals","ryalt","ryalp","ryalh","ryam","ryab","ryabs","ryas","ryass","ryang","ryaj","ryac","ryak","ryat","ryap","ryah","ryae","ryaeg","ryaegg","ryaegs","ryaen","ryaenj","ryaenh","ryaed","ryael","ryaelg","ryaelm","ryaelb","ryaels","ryaelt","ryaelp","ryaelh","ryaem","ryaeb","ryaebs","ryaes","ryaess","ryaeng","ryaej","ryaec","ryaek","ryaet","ryaep","ryaeh","reo","reog","reogg","reogs","reon","reonj","reonh","reod","reol","reolg","reolm","reolb","reols","reolt","reolp","reolh","reom","reob","reobs","reos" ];
|
|
|
|
},{}],169:[function(require,module,exports){
|
|
module.exports = [ "reoss","reong","reoj","reoc","reok","reot","reop","reoh","re","reg","regg","regs","ren","renj","renh","red","rel","relg","relm","relb","rels","relt","relp","relh","rem","reb","rebs","res","ress","reng","rej","rec","rek","ret","rep","reh","ryeo","ryeog","ryeogg","ryeogs","ryeon","ryeonj","ryeonh","ryeod","ryeol","ryeolg","ryeolm","ryeolb","ryeols","ryeolt","ryeolp","ryeolh","ryeom","ryeob","ryeobs","ryeos","ryeoss","ryeong","ryeoj","ryeoc","ryeok","ryeot","ryeop","ryeoh","rye","ryeg","ryegg","ryegs","ryen","ryenj","ryenh","ryed","ryel","ryelg","ryelm","ryelb","ryels","ryelt","ryelp","ryelh","ryem","ryeb","ryebs","ryes","ryess","ryeng","ryej","ryec","ryek","ryet","ryep","ryeh","ro","rog","rogg","rogs","ron","ronj","ronh","rod","rol","rolg","rolm","rolb","rols","rolt","rolp","rolh","rom","rob","robs","ros","ross","rong","roj","roc","rok","rot","rop","roh","rwa","rwag","rwagg","rwags","rwan","rwanj","rwanh","rwad","rwal","rwalg","rwalm","rwalb","rwals","rwalt","rwalp","rwalh","rwam","rwab","rwabs","rwas","rwass","rwang","rwaj","rwac","rwak","rwat","rwap","rwah","rwae","rwaeg","rwaegg","rwaegs","rwaen","rwaenj","rwaenh","rwaed","rwael","rwaelg","rwaelm","rwaelb","rwaels","rwaelt","rwaelp","rwaelh","rwaem","rwaeb","rwaebs","rwaes","rwaess","rwaeng","rwaej","rwaec","rwaek","rwaet","rwaep","rwaeh","roe","roeg","roegg","roegs","roen","roenj","roenh","roed","roel","roelg","roelm","roelb","roels","roelt","roelp","roelh","roem","roeb","roebs","roes","roess","roeng","roej","roec","roek","roet","roep","roeh","ryo","ryog","ryogg","ryogs","ryon","ryonj","ryonh","ryod","ryol","ryolg","ryolm","ryolb","ryols","ryolt","ryolp","ryolh","ryom","ryob","ryobs","ryos","ryoss","ryong","ryoj","ryoc","ryok","ryot","ryop","ryoh","ru","rug","rugg","rugs","run","runj","runh","rud","rul","rulg","rulm","rulb","ruls","rult","rulp","rulh","rum","rub","rubs","rus","russ","rung","ruj","ruc" ];
|
|
|
|
},{}],170:[function(require,module,exports){
|
|
module.exports = [ "ruk","rut","rup","ruh","rweo","rweog","rweogg","rweogs","rweon","rweonj","rweonh","rweod","rweol","rweolg","rweolm","rweolb","rweols","rweolt","rweolp","rweolh","rweom","rweob","rweobs","rweos","rweoss","rweong","rweoj","rweoc","rweok","rweot","rweop","rweoh","rwe","rweg","rwegg","rwegs","rwen","rwenj","rwenh","rwed","rwel","rwelg","rwelm","rwelb","rwels","rwelt","rwelp","rwelh","rwem","rweb","rwebs","rwes","rwess","rweng","rwej","rwec","rwek","rwet","rwep","rweh","rwi","rwig","rwigg","rwigs","rwin","rwinj","rwinh","rwid","rwil","rwilg","rwilm","rwilb","rwils","rwilt","rwilp","rwilh","rwim","rwib","rwibs","rwis","rwiss","rwing","rwij","rwic","rwik","rwit","rwip","rwih","ryu","ryug","ryugg","ryugs","ryun","ryunj","ryunh","ryud","ryul","ryulg","ryulm","ryulb","ryuls","ryult","ryulp","ryulh","ryum","ryub","ryubs","ryus","ryuss","ryung","ryuj","ryuc","ryuk","ryut","ryup","ryuh","reu","reug","reugg","reugs","reun","reunj","reunh","reud","reul","reulg","reulm","reulb","reuls","reult","reulp","reulh","reum","reub","reubs","reus","reuss","reung","reuj","reuc","reuk","reut","reup","reuh","ryi","ryig","ryigg","ryigs","ryin","ryinj","ryinh","ryid","ryil","ryilg","ryilm","ryilb","ryils","ryilt","ryilp","ryilh","ryim","ryib","ryibs","ryis","ryiss","rying","ryij","ryic","ryik","ryit","ryip","ryih","ri","rig","rigg","rigs","rin","rinj","rinh","rid","ril","rilg","rilm","rilb","rils","rilt","rilp","rilh","rim","rib","ribs","ris","riss","ring","rij","ric","rik","rit","rip","rih","ma","mag","magg","mags","man","manj","manh","mad","mal","malg","malm","malb","mals","malt","malp","malh","mam","mab","mabs","mas","mass","mang","maj","mac","mak","mat","map","mah","mae","maeg","maegg","maegs","maen","maenj","maenh","maed","mael","maelg","maelm","maelb","maels","maelt","maelp","maelh","maem","maeb","maebs","maes","maess","maeng","maej","maec","maek","maet","maep","maeh" ];
|
|
|
|
},{}],171:[function(require,module,exports){
|
|
module.exports = [ "mya","myag","myagg","myags","myan","myanj","myanh","myad","myal","myalg","myalm","myalb","myals","myalt","myalp","myalh","myam","myab","myabs","myas","myass","myang","myaj","myac","myak","myat","myap","myah","myae","myaeg","myaegg","myaegs","myaen","myaenj","myaenh","myaed","myael","myaelg","myaelm","myaelb","myaels","myaelt","myaelp","myaelh","myaem","myaeb","myaebs","myaes","myaess","myaeng","myaej","myaec","myaek","myaet","myaep","myaeh","meo","meog","meogg","meogs","meon","meonj","meonh","meod","meol","meolg","meolm","meolb","meols","meolt","meolp","meolh","meom","meob","meobs","meos","meoss","meong","meoj","meoc","meok","meot","meop","meoh","me","meg","megg","megs","men","menj","menh","med","mel","melg","melm","melb","mels","melt","melp","melh","mem","meb","mebs","mes","mess","meng","mej","mec","mek","met","mep","meh","myeo","myeog","myeogg","myeogs","myeon","myeonj","myeonh","myeod","myeol","myeolg","myeolm","myeolb","myeols","myeolt","myeolp","myeolh","myeom","myeob","myeobs","myeos","myeoss","myeong","myeoj","myeoc","myeok","myeot","myeop","myeoh","mye","myeg","myegg","myegs","myen","myenj","myenh","myed","myel","myelg","myelm","myelb","myels","myelt","myelp","myelh","myem","myeb","myebs","myes","myess","myeng","myej","myec","myek","myet","myep","myeh","mo","mog","mogg","mogs","mon","monj","monh","mod","mol","molg","molm","molb","mols","molt","molp","molh","mom","mob","mobs","mos","moss","mong","moj","moc","mok","mot","mop","moh","mwa","mwag","mwagg","mwags","mwan","mwanj","mwanh","mwad","mwal","mwalg","mwalm","mwalb","mwals","mwalt","mwalp","mwalh","mwam","mwab","mwabs","mwas","mwass","mwang","mwaj","mwac","mwak","mwat","mwap","mwah","mwae","mwaeg","mwaegg","mwaegs","mwaen","mwaenj","mwaenh","mwaed","mwael","mwaelg","mwaelm","mwaelb","mwaels","mwaelt","mwaelp","mwaelh","mwaem","mwaeb","mwaebs","mwaes","mwaess","mwaeng","mwaej","mwaec","mwaek","mwaet","mwaep","mwaeh","moe","moeg","moegg","moegs" ];
|
|
|
|
},{}],172:[function(require,module,exports){
|
|
module.exports = [ "moen","moenj","moenh","moed","moel","moelg","moelm","moelb","moels","moelt","moelp","moelh","moem","moeb","moebs","moes","moess","moeng","moej","moec","moek","moet","moep","moeh","myo","myog","myogg","myogs","myon","myonj","myonh","myod","myol","myolg","myolm","myolb","myols","myolt","myolp","myolh","myom","myob","myobs","myos","myoss","myong","myoj","myoc","myok","myot","myop","myoh","mu","mug","mugg","mugs","mun","munj","munh","mud","mul","mulg","mulm","mulb","muls","mult","mulp","mulh","mum","mub","mubs","mus","muss","mung","muj","muc","muk","mut","mup","muh","mweo","mweog","mweogg","mweogs","mweon","mweonj","mweonh","mweod","mweol","mweolg","mweolm","mweolb","mweols","mweolt","mweolp","mweolh","mweom","mweob","mweobs","mweos","mweoss","mweong","mweoj","mweoc","mweok","mweot","mweop","mweoh","mwe","mweg","mwegg","mwegs","mwen","mwenj","mwenh","mwed","mwel","mwelg","mwelm","mwelb","mwels","mwelt","mwelp","mwelh","mwem","mweb","mwebs","mwes","mwess","mweng","mwej","mwec","mwek","mwet","mwep","mweh","mwi","mwig","mwigg","mwigs","mwin","mwinj","mwinh","mwid","mwil","mwilg","mwilm","mwilb","mwils","mwilt","mwilp","mwilh","mwim","mwib","mwibs","mwis","mwiss","mwing","mwij","mwic","mwik","mwit","mwip","mwih","myu","myug","myugg","myugs","myun","myunj","myunh","myud","myul","myulg","myulm","myulb","myuls","myult","myulp","myulh","myum","myub","myubs","myus","myuss","myung","myuj","myuc","myuk","myut","myup","myuh","meu","meug","meugg","meugs","meun","meunj","meunh","meud","meul","meulg","meulm","meulb","meuls","meult","meulp","meulh","meum","meub","meubs","meus","meuss","meung","meuj","meuc","meuk","meut","meup","meuh","myi","myig","myigg","myigs","myin","myinj","myinh","myid","myil","myilg","myilm","myilb","myils","myilt","myilp","myilh","myim","myib","myibs","myis","myiss","mying","myij","myic","myik","myit","myip","myih","mi","mig","migg","migs","min","minj","minh","mid" ];
|
|
|
|
},{}],173:[function(require,module,exports){
|
|
module.exports = [ "mil","milg","milm","milb","mils","milt","milp","milh","mim","mib","mibs","mis","miss","ming","mij","mic","mik","mit","mip","mih","ba","bag","bagg","bags","ban","banj","banh","bad","bal","balg","balm","balb","bals","balt","balp","balh","bam","bab","babs","bas","bass","bang","baj","bac","bak","bat","bap","bah","bae","baeg","baegg","baegs","baen","baenj","baenh","baed","bael","baelg","baelm","baelb","baels","baelt","baelp","baelh","baem","baeb","baebs","baes","baess","baeng","baej","baec","baek","baet","baep","baeh","bya","byag","byagg","byags","byan","byanj","byanh","byad","byal","byalg","byalm","byalb","byals","byalt","byalp","byalh","byam","byab","byabs","byas","byass","byang","byaj","byac","byak","byat","byap","byah","byae","byaeg","byaegg","byaegs","byaen","byaenj","byaenh","byaed","byael","byaelg","byaelm","byaelb","byaels","byaelt","byaelp","byaelh","byaem","byaeb","byaebs","byaes","byaess","byaeng","byaej","byaec","byaek","byaet","byaep","byaeh","beo","beog","beogg","beogs","beon","beonj","beonh","beod","beol","beolg","beolm","beolb","beols","beolt","beolp","beolh","beom","beob","beobs","beos","beoss","beong","beoj","beoc","beok","beot","beop","beoh","be","beg","begg","begs","ben","benj","benh","bed","bel","belg","belm","belb","bels","belt","belp","belh","bem","beb","bebs","bes","bess","beng","bej","bec","bek","bet","bep","beh","byeo","byeog","byeogg","byeogs","byeon","byeonj","byeonh","byeod","byeol","byeolg","byeolm","byeolb","byeols","byeolt","byeolp","byeolh","byeom","byeob","byeobs","byeos","byeoss","byeong","byeoj","byeoc","byeok","byeot","byeop","byeoh","bye","byeg","byegg","byegs","byen","byenj","byenh","byed","byel","byelg","byelm","byelb","byels","byelt","byelp","byelh","byem","byeb","byebs","byes","byess","byeng","byej","byec","byek","byet","byep","byeh","bo","bog","bogg","bogs","bon","bonj","bonh","bod","bol","bolg","bolm","bolb" ];
|
|
|
|
},{}],174:[function(require,module,exports){
|
|
module.exports = [ "bols","bolt","bolp","bolh","bom","bob","bobs","bos","boss","bong","boj","boc","bok","bot","bop","boh","bwa","bwag","bwagg","bwags","bwan","bwanj","bwanh","bwad","bwal","bwalg","bwalm","bwalb","bwals","bwalt","bwalp","bwalh","bwam","bwab","bwabs","bwas","bwass","bwang","bwaj","bwac","bwak","bwat","bwap","bwah","bwae","bwaeg","bwaegg","bwaegs","bwaen","bwaenj","bwaenh","bwaed","bwael","bwaelg","bwaelm","bwaelb","bwaels","bwaelt","bwaelp","bwaelh","bwaem","bwaeb","bwaebs","bwaes","bwaess","bwaeng","bwaej","bwaec","bwaek","bwaet","bwaep","bwaeh","boe","boeg","boegg","boegs","boen","boenj","boenh","boed","boel","boelg","boelm","boelb","boels","boelt","boelp","boelh","boem","boeb","boebs","boes","boess","boeng","boej","boec","boek","boet","boep","boeh","byo","byog","byogg","byogs","byon","byonj","byonh","byod","byol","byolg","byolm","byolb","byols","byolt","byolp","byolh","byom","byob","byobs","byos","byoss","byong","byoj","byoc","byok","byot","byop","byoh","bu","bug","bugg","bugs","bun","bunj","bunh","bud","bul","bulg","bulm","bulb","buls","bult","bulp","bulh","bum","bub","bubs","bus","buss","bung","buj","buc","buk","but","bup","buh","bweo","bweog","bweogg","bweogs","bweon","bweonj","bweonh","bweod","bweol","bweolg","bweolm","bweolb","bweols","bweolt","bweolp","bweolh","bweom","bweob","bweobs","bweos","bweoss","bweong","bweoj","bweoc","bweok","bweot","bweop","bweoh","bwe","bweg","bwegg","bwegs","bwen","bwenj","bwenh","bwed","bwel","bwelg","bwelm","bwelb","bwels","bwelt","bwelp","bwelh","bwem","bweb","bwebs","bwes","bwess","bweng","bwej","bwec","bwek","bwet","bwep","bweh","bwi","bwig","bwigg","bwigs","bwin","bwinj","bwinh","bwid","bwil","bwilg","bwilm","bwilb","bwils","bwilt","bwilp","bwilh","bwim","bwib","bwibs","bwis","bwiss","bwing","bwij","bwic","bwik","bwit","bwip","bwih","byu","byug","byugg","byugs","byun","byunj","byunh","byud","byul","byulg","byulm","byulb","byuls","byult","byulp","byulh" ];
|
|
|
|
},{}],175:[function(require,module,exports){
|
|
module.exports = [ "byum","byub","byubs","byus","byuss","byung","byuj","byuc","byuk","byut","byup","byuh","beu","beug","beugg","beugs","beun","beunj","beunh","beud","beul","beulg","beulm","beulb","beuls","beult","beulp","beulh","beum","beub","beubs","beus","beuss","beung","beuj","beuc","beuk","beut","beup","beuh","byi","byig","byigg","byigs","byin","byinj","byinh","byid","byil","byilg","byilm","byilb","byils","byilt","byilp","byilh","byim","byib","byibs","byis","byiss","bying","byij","byic","byik","byit","byip","byih","bi","big","bigg","bigs","bin","binj","binh","bid","bil","bilg","bilm","bilb","bils","bilt","bilp","bilh","bim","bib","bibs","bis","biss","bing","bij","bic","bik","bit","bip","bih","bba","bbag","bbagg","bbags","bban","bbanj","bbanh","bbad","bbal","bbalg","bbalm","bbalb","bbals","bbalt","bbalp","bbalh","bbam","bbab","bbabs","bbas","bbass","bbang","bbaj","bbac","bbak","bbat","bbap","bbah","bbae","bbaeg","bbaegg","bbaegs","bbaen","bbaenj","bbaenh","bbaed","bbael","bbaelg","bbaelm","bbaelb","bbaels","bbaelt","bbaelp","bbaelh","bbaem","bbaeb","bbaebs","bbaes","bbaess","bbaeng","bbaej","bbaec","bbaek","bbaet","bbaep","bbaeh","bbya","bbyag","bbyagg","bbyags","bbyan","bbyanj","bbyanh","bbyad","bbyal","bbyalg","bbyalm","bbyalb","bbyals","bbyalt","bbyalp","bbyalh","bbyam","bbyab","bbyabs","bbyas","bbyass","bbyang","bbyaj","bbyac","bbyak","bbyat","bbyap","bbyah","bbyae","bbyaeg","bbyaegg","bbyaegs","bbyaen","bbyaenj","bbyaenh","bbyaed","bbyael","bbyaelg","bbyaelm","bbyaelb","bbyaels","bbyaelt","bbyaelp","bbyaelh","bbyaem","bbyaeb","bbyaebs","bbyaes","bbyaess","bbyaeng","bbyaej","bbyaec","bbyaek","bbyaet","bbyaep","bbyaeh","bbeo","bbeog","bbeogg","bbeogs","bbeon","bbeonj","bbeonh","bbeod","bbeol","bbeolg","bbeolm","bbeolb","bbeols","bbeolt","bbeolp","bbeolh","bbeom","bbeob","bbeobs","bbeos","bbeoss","bbeong","bbeoj","bbeoc","bbeok","bbeot","bbeop","bbeoh","bbe","bbeg","bbegg","bbegs","bben","bbenj","bbenh","bbed","bbel","bbelg","bbelm","bbelb","bbels","bbelt","bbelp","bbelh","bbem","bbeb","bbebs","bbes" ];
|
|
|
|
},{}],176:[function(require,module,exports){
|
|
module.exports = [ "bbess","bbeng","bbej","bbec","bbek","bbet","bbep","bbeh","bbyeo","bbyeog","bbyeogg","bbyeogs","bbyeon","bbyeonj","bbyeonh","bbyeod","bbyeol","bbyeolg","bbyeolm","bbyeolb","bbyeols","bbyeolt","bbyeolp","bbyeolh","bbyeom","bbyeob","bbyeobs","bbyeos","bbyeoss","bbyeong","bbyeoj","bbyeoc","bbyeok","bbyeot","bbyeop","bbyeoh","bbye","bbyeg","bbyegg","bbyegs","bbyen","bbyenj","bbyenh","bbyed","bbyel","bbyelg","bbyelm","bbyelb","bbyels","bbyelt","bbyelp","bbyelh","bbyem","bbyeb","bbyebs","bbyes","bbyess","bbyeng","bbyej","bbyec","bbyek","bbyet","bbyep","bbyeh","bbo","bbog","bbogg","bbogs","bbon","bbonj","bbonh","bbod","bbol","bbolg","bbolm","bbolb","bbols","bbolt","bbolp","bbolh","bbom","bbob","bbobs","bbos","bboss","bbong","bboj","bboc","bbok","bbot","bbop","bboh","bbwa","bbwag","bbwagg","bbwags","bbwan","bbwanj","bbwanh","bbwad","bbwal","bbwalg","bbwalm","bbwalb","bbwals","bbwalt","bbwalp","bbwalh","bbwam","bbwab","bbwabs","bbwas","bbwass","bbwang","bbwaj","bbwac","bbwak","bbwat","bbwap","bbwah","bbwae","bbwaeg","bbwaegg","bbwaegs","bbwaen","bbwaenj","bbwaenh","bbwaed","bbwael","bbwaelg","bbwaelm","bbwaelb","bbwaels","bbwaelt","bbwaelp","bbwaelh","bbwaem","bbwaeb","bbwaebs","bbwaes","bbwaess","bbwaeng","bbwaej","bbwaec","bbwaek","bbwaet","bbwaep","bbwaeh","bboe","bboeg","bboegg","bboegs","bboen","bboenj","bboenh","bboed","bboel","bboelg","bboelm","bboelb","bboels","bboelt","bboelp","bboelh","bboem","bboeb","bboebs","bboes","bboess","bboeng","bboej","bboec","bboek","bboet","bboep","bboeh","bbyo","bbyog","bbyogg","bbyogs","bbyon","bbyonj","bbyonh","bbyod","bbyol","bbyolg","bbyolm","bbyolb","bbyols","bbyolt","bbyolp","bbyolh","bbyom","bbyob","bbyobs","bbyos","bbyoss","bbyong","bbyoj","bbyoc","bbyok","bbyot","bbyop","bbyoh","bbu","bbug","bbugg","bbugs","bbun","bbunj","bbunh","bbud","bbul","bbulg","bbulm","bbulb","bbuls","bbult","bbulp","bbulh","bbum","bbub","bbubs","bbus","bbuss","bbung","bbuj","bbuc","bbuk","bbut","bbup","bbuh","bbweo","bbweog","bbweogg","bbweogs","bbweon","bbweonj","bbweonh","bbweod","bbweol","bbweolg","bbweolm","bbweolb","bbweols","bbweolt","bbweolp","bbweolh","bbweom","bbweob","bbweobs","bbweos","bbweoss","bbweong","bbweoj","bbweoc" ];
|
|
|
|
},{}],177:[function(require,module,exports){
|
|
module.exports = [ "bbweok","bbweot","bbweop","bbweoh","bbwe","bbweg","bbwegg","bbwegs","bbwen","bbwenj","bbwenh","bbwed","bbwel","bbwelg","bbwelm","bbwelb","bbwels","bbwelt","bbwelp","bbwelh","bbwem","bbweb","bbwebs","bbwes","bbwess","bbweng","bbwej","bbwec","bbwek","bbwet","bbwep","bbweh","bbwi","bbwig","bbwigg","bbwigs","bbwin","bbwinj","bbwinh","bbwid","bbwil","bbwilg","bbwilm","bbwilb","bbwils","bbwilt","bbwilp","bbwilh","bbwim","bbwib","bbwibs","bbwis","bbwiss","bbwing","bbwij","bbwic","bbwik","bbwit","bbwip","bbwih","bbyu","bbyug","bbyugg","bbyugs","bbyun","bbyunj","bbyunh","bbyud","bbyul","bbyulg","bbyulm","bbyulb","bbyuls","bbyult","bbyulp","bbyulh","bbyum","bbyub","bbyubs","bbyus","bbyuss","bbyung","bbyuj","bbyuc","bbyuk","bbyut","bbyup","bbyuh","bbeu","bbeug","bbeugg","bbeugs","bbeun","bbeunj","bbeunh","bbeud","bbeul","bbeulg","bbeulm","bbeulb","bbeuls","bbeult","bbeulp","bbeulh","bbeum","bbeub","bbeubs","bbeus","bbeuss","bbeung","bbeuj","bbeuc","bbeuk","bbeut","bbeup","bbeuh","bbyi","bbyig","bbyigg","bbyigs","bbyin","bbyinj","bbyinh","bbyid","bbyil","bbyilg","bbyilm","bbyilb","bbyils","bbyilt","bbyilp","bbyilh","bbyim","bbyib","bbyibs","bbyis","bbyiss","bbying","bbyij","bbyic","bbyik","bbyit","bbyip","bbyih","bbi","bbig","bbigg","bbigs","bbin","bbinj","bbinh","bbid","bbil","bbilg","bbilm","bbilb","bbils","bbilt","bbilp","bbilh","bbim","bbib","bbibs","bbis","bbiss","bbing","bbij","bbic","bbik","bbit","bbip","bbih","sa","sag","sagg","sags","san","sanj","sanh","sad","sal","salg","salm","salb","sals","salt","salp","salh","sam","sab","sabs","sas","sass","sang","saj","sac","sak","sat","sap","sah","sae","saeg","saegg","saegs","saen","saenj","saenh","saed","sael","saelg","saelm","saelb","saels","saelt","saelp","saelh","saem","saeb","saebs","saes","saess","saeng","saej","saec","saek","saet","saep","saeh","sya","syag","syagg","syags","syan","syanj","syanh","syad","syal","syalg","syalm","syalb","syals","syalt","syalp","syalh","syam","syab","syabs","syas","syass","syang","syaj","syac","syak","syat","syap","syah" ];
|
|
|
|
},{}],178:[function(require,module,exports){
|
|
module.exports = [ "syae","syaeg","syaegg","syaegs","syaen","syaenj","syaenh","syaed","syael","syaelg","syaelm","syaelb","syaels","syaelt","syaelp","syaelh","syaem","syaeb","syaebs","syaes","syaess","syaeng","syaej","syaec","syaek","syaet","syaep","syaeh","seo","seog","seogg","seogs","seon","seonj","seonh","seod","seol","seolg","seolm","seolb","seols","seolt","seolp","seolh","seom","seob","seobs","seos","seoss","seong","seoj","seoc","seok","seot","seop","seoh","se","seg","segg","segs","sen","senj","senh","sed","sel","selg","selm","selb","sels","selt","selp","selh","sem","seb","sebs","ses","sess","seng","sej","sec","sek","set","sep","seh","syeo","syeog","syeogg","syeogs","syeon","syeonj","syeonh","syeod","syeol","syeolg","syeolm","syeolb","syeols","syeolt","syeolp","syeolh","syeom","syeob","syeobs","syeos","syeoss","syeong","syeoj","syeoc","syeok","syeot","syeop","syeoh","sye","syeg","syegg","syegs","syen","syenj","syenh","syed","syel","syelg","syelm","syelb","syels","syelt","syelp","syelh","syem","syeb","syebs","syes","syess","syeng","syej","syec","syek","syet","syep","syeh","so","sog","sogg","sogs","son","sonj","sonh","sod","sol","solg","solm","solb","sols","solt","solp","solh","som","sob","sobs","sos","soss","song","soj","soc","sok","sot","sop","soh","swa","swag","swagg","swags","swan","swanj","swanh","swad","swal","swalg","swalm","swalb","swals","swalt","swalp","swalh","swam","swab","swabs","swas","swass","swang","swaj","swac","swak","swat","swap","swah","swae","swaeg","swaegg","swaegs","swaen","swaenj","swaenh","swaed","swael","swaelg","swaelm","swaelb","swaels","swaelt","swaelp","swaelh","swaem","swaeb","swaebs","swaes","swaess","swaeng","swaej","swaec","swaek","swaet","swaep","swaeh","soe","soeg","soegg","soegs","soen","soenj","soenh","soed","soel","soelg","soelm","soelb","soels","soelt","soelp","soelh","soem","soeb","soebs","soes","soess","soeng","soej","soec","soek","soet","soep","soeh","syo","syog","syogg","syogs" ];
|
|
|
|
},{}],179:[function(require,module,exports){
|
|
module.exports = [ "syon","syonj","syonh","syod","syol","syolg","syolm","syolb","syols","syolt","syolp","syolh","syom","syob","syobs","syos","syoss","syong","syoj","syoc","syok","syot","syop","syoh","su","sug","sugg","sugs","sun","sunj","sunh","sud","sul","sulg","sulm","sulb","suls","sult","sulp","sulh","sum","sub","subs","sus","suss","sung","suj","suc","suk","sut","sup","suh","sweo","sweog","sweogg","sweogs","sweon","sweonj","sweonh","sweod","sweol","sweolg","sweolm","sweolb","sweols","sweolt","sweolp","sweolh","sweom","sweob","sweobs","sweos","sweoss","sweong","sweoj","sweoc","sweok","sweot","sweop","sweoh","swe","sweg","swegg","swegs","swen","swenj","swenh","swed","swel","swelg","swelm","swelb","swels","swelt","swelp","swelh","swem","sweb","swebs","swes","swess","sweng","swej","swec","swek","swet","swep","sweh","swi","swig","swigg","swigs","swin","swinj","swinh","swid","swil","swilg","swilm","swilb","swils","swilt","swilp","swilh","swim","swib","swibs","swis","swiss","swing","swij","swic","swik","swit","swip","swih","syu","syug","syugg","syugs","syun","syunj","syunh","syud","syul","syulg","syulm","syulb","syuls","syult","syulp","syulh","syum","syub","syubs","syus","syuss","syung","syuj","syuc","syuk","syut","syup","syuh","seu","seug","seugg","seugs","seun","seunj","seunh","seud","seul","seulg","seulm","seulb","seuls","seult","seulp","seulh","seum","seub","seubs","seus","seuss","seung","seuj","seuc","seuk","seut","seup","seuh","syi","syig","syigg","syigs","syin","syinj","syinh","syid","syil","syilg","syilm","syilb","syils","syilt","syilp","syilh","syim","syib","syibs","syis","syiss","sying","syij","syic","syik","syit","syip","syih","si","sig","sigg","sigs","sin","sinj","sinh","sid","sil","silg","silm","silb","sils","silt","silp","silh","sim","sib","sibs","sis","siss","sing","sij","sic","sik","sit","sip","sih","ssa","ssag","ssagg","ssags","ssan","ssanj","ssanh","ssad" ];
|
|
|
|
},{}],180:[function(require,module,exports){
|
|
module.exports = [ "ssal","ssalg","ssalm","ssalb","ssals","ssalt","ssalp","ssalh","ssam","ssab","ssabs","ssas","ssass","ssang","ssaj","ssac","ssak","ssat","ssap","ssah","ssae","ssaeg","ssaegg","ssaegs","ssaen","ssaenj","ssaenh","ssaed","ssael","ssaelg","ssaelm","ssaelb","ssaels","ssaelt","ssaelp","ssaelh","ssaem","ssaeb","ssaebs","ssaes","ssaess","ssaeng","ssaej","ssaec","ssaek","ssaet","ssaep","ssaeh","ssya","ssyag","ssyagg","ssyags","ssyan","ssyanj","ssyanh","ssyad","ssyal","ssyalg","ssyalm","ssyalb","ssyals","ssyalt","ssyalp","ssyalh","ssyam","ssyab","ssyabs","ssyas","ssyass","ssyang","ssyaj","ssyac","ssyak","ssyat","ssyap","ssyah","ssyae","ssyaeg","ssyaegg","ssyaegs","ssyaen","ssyaenj","ssyaenh","ssyaed","ssyael","ssyaelg","ssyaelm","ssyaelb","ssyaels","ssyaelt","ssyaelp","ssyaelh","ssyaem","ssyaeb","ssyaebs","ssyaes","ssyaess","ssyaeng","ssyaej","ssyaec","ssyaek","ssyaet","ssyaep","ssyaeh","sseo","sseog","sseogg","sseogs","sseon","sseonj","sseonh","sseod","sseol","sseolg","sseolm","sseolb","sseols","sseolt","sseolp","sseolh","sseom","sseob","sseobs","sseos","sseoss","sseong","sseoj","sseoc","sseok","sseot","sseop","sseoh","sse","sseg","ssegg","ssegs","ssen","ssenj","ssenh","ssed","ssel","sselg","sselm","sselb","ssels","sselt","sselp","sselh","ssem","sseb","ssebs","sses","ssess","sseng","ssej","ssec","ssek","sset","ssep","sseh","ssyeo","ssyeog","ssyeogg","ssyeogs","ssyeon","ssyeonj","ssyeonh","ssyeod","ssyeol","ssyeolg","ssyeolm","ssyeolb","ssyeols","ssyeolt","ssyeolp","ssyeolh","ssyeom","ssyeob","ssyeobs","ssyeos","ssyeoss","ssyeong","ssyeoj","ssyeoc","ssyeok","ssyeot","ssyeop","ssyeoh","ssye","ssyeg","ssyegg","ssyegs","ssyen","ssyenj","ssyenh","ssyed","ssyel","ssyelg","ssyelm","ssyelb","ssyels","ssyelt","ssyelp","ssyelh","ssyem","ssyeb","ssyebs","ssyes","ssyess","ssyeng","ssyej","ssyec","ssyek","ssyet","ssyep","ssyeh","sso","ssog","ssogg","ssogs","sson","ssonj","ssonh","ssod","ssol","ssolg","ssolm","ssolb","ssols","ssolt","ssolp","ssolh","ssom","ssob","ssobs","ssos","ssoss","ssong","ssoj","ssoc","ssok","ssot","ssop","ssoh","sswa","sswag","sswagg","sswags","sswan","sswanj","sswanh","sswad","sswal","sswalg","sswalm","sswalb" ];
|
|
|
|
},{}],181:[function(require,module,exports){
|
|
module.exports = [ "sswals","sswalt","sswalp","sswalh","sswam","sswab","sswabs","sswas","sswass","sswang","sswaj","sswac","sswak","sswat","sswap","sswah","sswae","sswaeg","sswaegg","sswaegs","sswaen","sswaenj","sswaenh","sswaed","sswael","sswaelg","sswaelm","sswaelb","sswaels","sswaelt","sswaelp","sswaelh","sswaem","sswaeb","sswaebs","sswaes","sswaess","sswaeng","sswaej","sswaec","sswaek","sswaet","sswaep","sswaeh","ssoe","ssoeg","ssoegg","ssoegs","ssoen","ssoenj","ssoenh","ssoed","ssoel","ssoelg","ssoelm","ssoelb","ssoels","ssoelt","ssoelp","ssoelh","ssoem","ssoeb","ssoebs","ssoes","ssoess","ssoeng","ssoej","ssoec","ssoek","ssoet","ssoep","ssoeh","ssyo","ssyog","ssyogg","ssyogs","ssyon","ssyonj","ssyonh","ssyod","ssyol","ssyolg","ssyolm","ssyolb","ssyols","ssyolt","ssyolp","ssyolh","ssyom","ssyob","ssyobs","ssyos","ssyoss","ssyong","ssyoj","ssyoc","ssyok","ssyot","ssyop","ssyoh","ssu","ssug","ssugg","ssugs","ssun","ssunj","ssunh","ssud","ssul","ssulg","ssulm","ssulb","ssuls","ssult","ssulp","ssulh","ssum","ssub","ssubs","ssus","ssuss","ssung","ssuj","ssuc","ssuk","ssut","ssup","ssuh","ssweo","ssweog","ssweogg","ssweogs","ssweon","ssweonj","ssweonh","ssweod","ssweol","ssweolg","ssweolm","ssweolb","ssweols","ssweolt","ssweolp","ssweolh","ssweom","ssweob","ssweobs","ssweos","ssweoss","ssweong","ssweoj","ssweoc","ssweok","ssweot","ssweop","ssweoh","sswe","ssweg","sswegg","sswegs","sswen","sswenj","sswenh","sswed","sswel","sswelg","sswelm","sswelb","sswels","sswelt","sswelp","sswelh","sswem","ssweb","sswebs","sswes","sswess","ssweng","sswej","sswec","sswek","sswet","sswep","ssweh","sswi","sswig","sswigg","sswigs","sswin","sswinj","sswinh","sswid","sswil","sswilg","sswilm","sswilb","sswils","sswilt","sswilp","sswilh","sswim","sswib","sswibs","sswis","sswiss","sswing","sswij","sswic","sswik","sswit","sswip","sswih","ssyu","ssyug","ssyugg","ssyugs","ssyun","ssyunj","ssyunh","ssyud","ssyul","ssyulg","ssyulm","ssyulb","ssyuls","ssyult","ssyulp","ssyulh","ssyum","ssyub","ssyubs","ssyus","ssyuss","ssyung","ssyuj","ssyuc","ssyuk","ssyut","ssyup","ssyuh","sseu","sseug","sseugg","sseugs","sseun","sseunj","sseunh","sseud","sseul","sseulg","sseulm","sseulb","sseuls","sseult","sseulp","sseulh" ];
|
|
|
|
},{}],182:[function(require,module,exports){
|
|
module.exports = [ "sseum","sseub","sseubs","sseus","sseuss","sseung","sseuj","sseuc","sseuk","sseut","sseup","sseuh","ssyi","ssyig","ssyigg","ssyigs","ssyin","ssyinj","ssyinh","ssyid","ssyil","ssyilg","ssyilm","ssyilb","ssyils","ssyilt","ssyilp","ssyilh","ssyim","ssyib","ssyibs","ssyis","ssyiss","ssying","ssyij","ssyic","ssyik","ssyit","ssyip","ssyih","ssi","ssig","ssigg","ssigs","ssin","ssinj","ssinh","ssid","ssil","ssilg","ssilm","ssilb","ssils","ssilt","ssilp","ssilh","ssim","ssib","ssibs","ssis","ssiss","ssing","ssij","ssic","ssik","ssit","ssip","ssih","a","ag","agg","ags","an","anj","anh","ad","al","alg","alm","alb","als","alt","alp","alh","am","ab","abs","as","ass","ang","aj","ac","ak","at","ap","ah","ae","aeg","aegg","aegs","aen","aenj","aenh","aed","ael","aelg","aelm","aelb","aels","aelt","aelp","aelh","aem","aeb","aebs","aes","aess","aeng","aej","aec","aek","aet","aep","aeh","ya","yag","yagg","yags","yan","yanj","yanh","yad","yal","yalg","yalm","yalb","yals","yalt","yalp","yalh","yam","yab","yabs","yas","yass","yang","yaj","yac","yak","yat","yap","yah","yae","yaeg","yaegg","yaegs","yaen","yaenj","yaenh","yaed","yael","yaelg","yaelm","yaelb","yaels","yaelt","yaelp","yaelh","yaem","yaeb","yaebs","yaes","yaess","yaeng","yaej","yaec","yaek","yaet","yaep","yaeh","eo","eog","eogg","eogs","eon","eonj","eonh","eod","eol","eolg","eolm","eolb","eols","eolt","eolp","eolh","eom","eob","eobs","eos","eoss","eong","eoj","eoc","eok","eot","eop","eoh","e","eg","egg","egs","en","enj","enh","ed","el","elg","elm","elb","els","elt","elp","elh","em","eb","ebs","es","ess","eng","ej","ec","ek","et","ep","eh","yeo","yeog","yeogg","yeogs","yeon","yeonj","yeonh","yeod","yeol","yeolg","yeolm","yeolb","yeols","yeolt","yeolp","yeolh","yeom","yeob","yeobs","yeos" ];
|
|
|
|
},{}],183:[function(require,module,exports){
|
|
module.exports = [ "yeoss","yeong","yeoj","yeoc","yeok","yeot","yeop","yeoh","ye","yeg","yegg","yegs","yen","yenj","yenh","yed","yel","yelg","yelm","yelb","yels","yelt","yelp","yelh","yem","yeb","yebs","yes","yess","yeng","yej","yec","yek","yet","yep","yeh","o","og","ogg","ogs","on","onj","onh","od","ol","olg","olm","olb","ols","olt","olp","olh","om","ob","obs","os","oss","ong","oj","oc","ok","ot","op","oh","wa","wag","wagg","wags","wan","wanj","wanh","wad","wal","walg","walm","walb","wals","walt","walp","walh","wam","wab","wabs","was","wass","wang","waj","wac","wak","wat","wap","wah","wae","waeg","waegg","waegs","waen","waenj","waenh","waed","wael","waelg","waelm","waelb","waels","waelt","waelp","waelh","waem","waeb","waebs","waes","waess","waeng","waej","waec","waek","waet","waep","waeh","oe","oeg","oegg","oegs","oen","oenj","oenh","oed","oel","oelg","oelm","oelb","oels","oelt","oelp","oelh","oem","oeb","oebs","oes","oess","oeng","oej","oec","oek","oet","oep","oeh","yo","yog","yogg","yogs","yon","yonj","yonh","yod","yol","yolg","yolm","yolb","yols","yolt","yolp","yolh","yom","yob","yobs","yos","yoss","yong","yoj","yoc","yok","yot","yop","yoh","u","ug","ugg","ugs","un","unj","unh","ud","ul","ulg","ulm","ulb","uls","ult","ulp","ulh","um","ub","ubs","us","uss","ung","uj","uc","uk","ut","up","uh","weo","weog","weogg","weogs","weon","weonj","weonh","weod","weol","weolg","weolm","weolb","weols","weolt","weolp","weolh","weom","weob","weobs","weos","weoss","weong","weoj","weoc","weok","weot","weop","weoh","we","weg","wegg","wegs","wen","wenj","wenh","wed","wel","welg","welm","welb","wels","welt","welp","welh","wem","web","webs","wes","wess","weng","wej","wec" ];
|
|
|
|
},{}],184:[function(require,module,exports){
|
|
module.exports = [ "wek","wet","wep","weh","wi","wig","wigg","wigs","win","winj","winh","wid","wil","wilg","wilm","wilb","wils","wilt","wilp","wilh","wim","wib","wibs","wis","wiss","wing","wij","wic","wik","wit","wip","wih","yu","yug","yugg","yugs","yun","yunj","yunh","yud","yul","yulg","yulm","yulb","yuls","yult","yulp","yulh","yum","yub","yubs","yus","yuss","yung","yuj","yuc","yuk","yut","yup","yuh","eu","eug","eugg","eugs","eun","eunj","eunh","eud","eul","eulg","eulm","eulb","euls","eult","eulp","eulh","eum","eub","eubs","eus","euss","eung","euj","euc","euk","eut","eup","euh","yi","yig","yigg","yigs","yin","yinj","yinh","yid","yil","yilg","yilm","yilb","yils","yilt","yilp","yilh","yim","yib","yibs","yis","yiss","ying","yij","yic","yik","yit","yip","yih","i","ig","igg","igs","in","inj","inh","id","il","ilg","ilm","ilb","ils","ilt","ilp","ilh","im","ib","ibs","is","iss","ing","ij","ic","ik","it","ip","ih","ja","jag","jagg","jags","jan","janj","janh","jad","jal","jalg","jalm","jalb","jals","jalt","jalp","jalh","jam","jab","jabs","jas","jass","jang","jaj","jac","jak","jat","jap","jah","jae","jaeg","jaegg","jaegs","jaen","jaenj","jaenh","jaed","jael","jaelg","jaelm","jaelb","jaels","jaelt","jaelp","jaelh","jaem","jaeb","jaebs","jaes","jaess","jaeng","jaej","jaec","jaek","jaet","jaep","jaeh","jya","jyag","jyagg","jyags","jyan","jyanj","jyanh","jyad","jyal","jyalg","jyalm","jyalb","jyals","jyalt","jyalp","jyalh","jyam","jyab","jyabs","jyas","jyass","jyang","jyaj","jyac","jyak","jyat","jyap","jyah","jyae","jyaeg","jyaegg","jyaegs","jyaen","jyaenj","jyaenh","jyaed","jyael","jyaelg","jyaelm","jyaelb","jyaels","jyaelt","jyaelp","jyaelh","jyaem","jyaeb","jyaebs","jyaes","jyaess","jyaeng","jyaej","jyaec","jyaek","jyaet","jyaep","jyaeh" ];
|
|
|
|
},{}],185:[function(require,module,exports){
|
|
module.exports = [ "jeo","jeog","jeogg","jeogs","jeon","jeonj","jeonh","jeod","jeol","jeolg","jeolm","jeolb","jeols","jeolt","jeolp","jeolh","jeom","jeob","jeobs","jeos","jeoss","jeong","jeoj","jeoc","jeok","jeot","jeop","jeoh","je","jeg","jegg","jegs","jen","jenj","jenh","jed","jel","jelg","jelm","jelb","jels","jelt","jelp","jelh","jem","jeb","jebs","jes","jess","jeng","jej","jec","jek","jet","jep","jeh","jyeo","jyeog","jyeogg","jyeogs","jyeon","jyeonj","jyeonh","jyeod","jyeol","jyeolg","jyeolm","jyeolb","jyeols","jyeolt","jyeolp","jyeolh","jyeom","jyeob","jyeobs","jyeos","jyeoss","jyeong","jyeoj","jyeoc","jyeok","jyeot","jyeop","jyeoh","jye","jyeg","jyegg","jyegs","jyen","jyenj","jyenh","jyed","jyel","jyelg","jyelm","jyelb","jyels","jyelt","jyelp","jyelh","jyem","jyeb","jyebs","jyes","jyess","jyeng","jyej","jyec","jyek","jyet","jyep","jyeh","jo","jog","jogg","jogs","jon","jonj","jonh","jod","jol","jolg","jolm","jolb","jols","jolt","jolp","jolh","jom","job","jobs","jos","joss","jong","joj","joc","jok","jot","jop","joh","jwa","jwag","jwagg","jwags","jwan","jwanj","jwanh","jwad","jwal","jwalg","jwalm","jwalb","jwals","jwalt","jwalp","jwalh","jwam","jwab","jwabs","jwas","jwass","jwang","jwaj","jwac","jwak","jwat","jwap","jwah","jwae","jwaeg","jwaegg","jwaegs","jwaen","jwaenj","jwaenh","jwaed","jwael","jwaelg","jwaelm","jwaelb","jwaels","jwaelt","jwaelp","jwaelh","jwaem","jwaeb","jwaebs","jwaes","jwaess","jwaeng","jwaej","jwaec","jwaek","jwaet","jwaep","jwaeh","joe","joeg","joegg","joegs","joen","joenj","joenh","joed","joel","joelg","joelm","joelb","joels","joelt","joelp","joelh","joem","joeb","joebs","joes","joess","joeng","joej","joec","joek","joet","joep","joeh","jyo","jyog","jyogg","jyogs","jyon","jyonj","jyonh","jyod","jyol","jyolg","jyolm","jyolb","jyols","jyolt","jyolp","jyolh","jyom","jyob","jyobs","jyos","jyoss","jyong","jyoj","jyoc","jyok","jyot","jyop","jyoh","ju","jug","jugg","jugs" ];
|
|
|
|
},{}],186:[function(require,module,exports){
|
|
module.exports = [ "jun","junj","junh","jud","jul","julg","julm","julb","juls","jult","julp","julh","jum","jub","jubs","jus","juss","jung","juj","juc","juk","jut","jup","juh","jweo","jweog","jweogg","jweogs","jweon","jweonj","jweonh","jweod","jweol","jweolg","jweolm","jweolb","jweols","jweolt","jweolp","jweolh","jweom","jweob","jweobs","jweos","jweoss","jweong","jweoj","jweoc","jweok","jweot","jweop","jweoh","jwe","jweg","jwegg","jwegs","jwen","jwenj","jwenh","jwed","jwel","jwelg","jwelm","jwelb","jwels","jwelt","jwelp","jwelh","jwem","jweb","jwebs","jwes","jwess","jweng","jwej","jwec","jwek","jwet","jwep","jweh","jwi","jwig","jwigg","jwigs","jwin","jwinj","jwinh","jwid","jwil","jwilg","jwilm","jwilb","jwils","jwilt","jwilp","jwilh","jwim","jwib","jwibs","jwis","jwiss","jwing","jwij","jwic","jwik","jwit","jwip","jwih","jyu","jyug","jyugg","jyugs","jyun","jyunj","jyunh","jyud","jyul","jyulg","jyulm","jyulb","jyuls","jyult","jyulp","jyulh","jyum","jyub","jyubs","jyus","jyuss","jyung","jyuj","jyuc","jyuk","jyut","jyup","jyuh","jeu","jeug","jeugg","jeugs","jeun","jeunj","jeunh","jeud","jeul","jeulg","jeulm","jeulb","jeuls","jeult","jeulp","jeulh","jeum","jeub","jeubs","jeus","jeuss","jeung","jeuj","jeuc","jeuk","jeut","jeup","jeuh","jyi","jyig","jyigg","jyigs","jyin","jyinj","jyinh","jyid","jyil","jyilg","jyilm","jyilb","jyils","jyilt","jyilp","jyilh","jyim","jyib","jyibs","jyis","jyiss","jying","jyij","jyic","jyik","jyit","jyip","jyih","ji","jig","jigg","jigs","jin","jinj","jinh","jid","jil","jilg","jilm","jilb","jils","jilt","jilp","jilh","jim","jib","jibs","jis","jiss","jing","jij","jic","jik","jit","jip","jih","jja","jjag","jjagg","jjags","jjan","jjanj","jjanh","jjad","jjal","jjalg","jjalm","jjalb","jjals","jjalt","jjalp","jjalh","jjam","jjab","jjabs","jjas","jjass","jjang","jjaj","jjac","jjak","jjat","jjap","jjah","jjae","jjaeg","jjaegg","jjaegs","jjaen","jjaenj","jjaenh","jjaed" ];
|
|
|
|
},{}],187:[function(require,module,exports){
|
|
module.exports = [ "jjael","jjaelg","jjaelm","jjaelb","jjaels","jjaelt","jjaelp","jjaelh","jjaem","jjaeb","jjaebs","jjaes","jjaess","jjaeng","jjaej","jjaec","jjaek","jjaet","jjaep","jjaeh","jjya","jjyag","jjyagg","jjyags","jjyan","jjyanj","jjyanh","jjyad","jjyal","jjyalg","jjyalm","jjyalb","jjyals","jjyalt","jjyalp","jjyalh","jjyam","jjyab","jjyabs","jjyas","jjyass","jjyang","jjyaj","jjyac","jjyak","jjyat","jjyap","jjyah","jjyae","jjyaeg","jjyaegg","jjyaegs","jjyaen","jjyaenj","jjyaenh","jjyaed","jjyael","jjyaelg","jjyaelm","jjyaelb","jjyaels","jjyaelt","jjyaelp","jjyaelh","jjyaem","jjyaeb","jjyaebs","jjyaes","jjyaess","jjyaeng","jjyaej","jjyaec","jjyaek","jjyaet","jjyaep","jjyaeh","jjeo","jjeog","jjeogg","jjeogs","jjeon","jjeonj","jjeonh","jjeod","jjeol","jjeolg","jjeolm","jjeolb","jjeols","jjeolt","jjeolp","jjeolh","jjeom","jjeob","jjeobs","jjeos","jjeoss","jjeong","jjeoj","jjeoc","jjeok","jjeot","jjeop","jjeoh","jje","jjeg","jjegg","jjegs","jjen","jjenj","jjenh","jjed","jjel","jjelg","jjelm","jjelb","jjels","jjelt","jjelp","jjelh","jjem","jjeb","jjebs","jjes","jjess","jjeng","jjej","jjec","jjek","jjet","jjep","jjeh","jjyeo","jjyeog","jjyeogg","jjyeogs","jjyeon","jjyeonj","jjyeonh","jjyeod","jjyeol","jjyeolg","jjyeolm","jjyeolb","jjyeols","jjyeolt","jjyeolp","jjyeolh","jjyeom","jjyeob","jjyeobs","jjyeos","jjyeoss","jjyeong","jjyeoj","jjyeoc","jjyeok","jjyeot","jjyeop","jjyeoh","jjye","jjyeg","jjyegg","jjyegs","jjyen","jjyenj","jjyenh","jjyed","jjyel","jjyelg","jjyelm","jjyelb","jjyels","jjyelt","jjyelp","jjyelh","jjyem","jjyeb","jjyebs","jjyes","jjyess","jjyeng","jjyej","jjyec","jjyek","jjyet","jjyep","jjyeh","jjo","jjog","jjogg","jjogs","jjon","jjonj","jjonh","jjod","jjol","jjolg","jjolm","jjolb","jjols","jjolt","jjolp","jjolh","jjom","jjob","jjobs","jjos","jjoss","jjong","jjoj","jjoc","jjok","jjot","jjop","jjoh","jjwa","jjwag","jjwagg","jjwags","jjwan","jjwanj","jjwanh","jjwad","jjwal","jjwalg","jjwalm","jjwalb","jjwals","jjwalt","jjwalp","jjwalh","jjwam","jjwab","jjwabs","jjwas","jjwass","jjwang","jjwaj","jjwac","jjwak","jjwat","jjwap","jjwah","jjwae","jjwaeg","jjwaegg","jjwaegs","jjwaen","jjwaenj","jjwaenh","jjwaed","jjwael","jjwaelg","jjwaelm","jjwaelb" ];
|
|
|
|
},{}],188:[function(require,module,exports){
|
|
module.exports = [ "jjwaels","jjwaelt","jjwaelp","jjwaelh","jjwaem","jjwaeb","jjwaebs","jjwaes","jjwaess","jjwaeng","jjwaej","jjwaec","jjwaek","jjwaet","jjwaep","jjwaeh","jjoe","jjoeg","jjoegg","jjoegs","jjoen","jjoenj","jjoenh","jjoed","jjoel","jjoelg","jjoelm","jjoelb","jjoels","jjoelt","jjoelp","jjoelh","jjoem","jjoeb","jjoebs","jjoes","jjoess","jjoeng","jjoej","jjoec","jjoek","jjoet","jjoep","jjoeh","jjyo","jjyog","jjyogg","jjyogs","jjyon","jjyonj","jjyonh","jjyod","jjyol","jjyolg","jjyolm","jjyolb","jjyols","jjyolt","jjyolp","jjyolh","jjyom","jjyob","jjyobs","jjyos","jjyoss","jjyong","jjyoj","jjyoc","jjyok","jjyot","jjyop","jjyoh","jju","jjug","jjugg","jjugs","jjun","jjunj","jjunh","jjud","jjul","jjulg","jjulm","jjulb","jjuls","jjult","jjulp","jjulh","jjum","jjub","jjubs","jjus","jjuss","jjung","jjuj","jjuc","jjuk","jjut","jjup","jjuh","jjweo","jjweog","jjweogg","jjweogs","jjweon","jjweonj","jjweonh","jjweod","jjweol","jjweolg","jjweolm","jjweolb","jjweols","jjweolt","jjweolp","jjweolh","jjweom","jjweob","jjweobs","jjweos","jjweoss","jjweong","jjweoj","jjweoc","jjweok","jjweot","jjweop","jjweoh","jjwe","jjweg","jjwegg","jjwegs","jjwen","jjwenj","jjwenh","jjwed","jjwel","jjwelg","jjwelm","jjwelb","jjwels","jjwelt","jjwelp","jjwelh","jjwem","jjweb","jjwebs","jjwes","jjwess","jjweng","jjwej","jjwec","jjwek","jjwet","jjwep","jjweh","jjwi","jjwig","jjwigg","jjwigs","jjwin","jjwinj","jjwinh","jjwid","jjwil","jjwilg","jjwilm","jjwilb","jjwils","jjwilt","jjwilp","jjwilh","jjwim","jjwib","jjwibs","jjwis","jjwiss","jjwing","jjwij","jjwic","jjwik","jjwit","jjwip","jjwih","jjyu","jjyug","jjyugg","jjyugs","jjyun","jjyunj","jjyunh","jjyud","jjyul","jjyulg","jjyulm","jjyulb","jjyuls","jjyult","jjyulp","jjyulh","jjyum","jjyub","jjyubs","jjyus","jjyuss","jjyung","jjyuj","jjyuc","jjyuk","jjyut","jjyup","jjyuh","jjeu","jjeug","jjeugg","jjeugs","jjeun","jjeunj","jjeunh","jjeud","jjeul","jjeulg","jjeulm","jjeulb","jjeuls","jjeult","jjeulp","jjeulh","jjeum","jjeub","jjeubs","jjeus","jjeuss","jjeung","jjeuj","jjeuc","jjeuk","jjeut","jjeup","jjeuh","jjyi","jjyig","jjyigg","jjyigs","jjyin","jjyinj","jjyinh","jjyid","jjyil","jjyilg","jjyilm","jjyilb","jjyils","jjyilt","jjyilp","jjyilh" ];
|
|
|
|
},{}],189:[function(require,module,exports){
|
|
module.exports = [ "jjyim","jjyib","jjyibs","jjyis","jjyiss","jjying","jjyij","jjyic","jjyik","jjyit","jjyip","jjyih","jji","jjig","jjigg","jjigs","jjin","jjinj","jjinh","jjid","jjil","jjilg","jjilm","jjilb","jjils","jjilt","jjilp","jjilh","jjim","jjib","jjibs","jjis","jjiss","jjing","jjij","jjic","jjik","jjit","jjip","jjih","ca","cag","cagg","cags","can","canj","canh","cad","cal","calg","calm","calb","cals","calt","calp","calh","cam","cab","cabs","cas","cass","cang","caj","cac","cak","cat","cap","cah","cae","caeg","caegg","caegs","caen","caenj","caenh","caed","cael","caelg","caelm","caelb","caels","caelt","caelp","caelh","caem","caeb","caebs","caes","caess","caeng","caej","caec","caek","caet","caep","caeh","cya","cyag","cyagg","cyags","cyan","cyanj","cyanh","cyad","cyal","cyalg","cyalm","cyalb","cyals","cyalt","cyalp","cyalh","cyam","cyab","cyabs","cyas","cyass","cyang","cyaj","cyac","cyak","cyat","cyap","cyah","cyae","cyaeg","cyaegg","cyaegs","cyaen","cyaenj","cyaenh","cyaed","cyael","cyaelg","cyaelm","cyaelb","cyaels","cyaelt","cyaelp","cyaelh","cyaem","cyaeb","cyaebs","cyaes","cyaess","cyaeng","cyaej","cyaec","cyaek","cyaet","cyaep","cyaeh","ceo","ceog","ceogg","ceogs","ceon","ceonj","ceonh","ceod","ceol","ceolg","ceolm","ceolb","ceols","ceolt","ceolp","ceolh","ceom","ceob","ceobs","ceos","ceoss","ceong","ceoj","ceoc","ceok","ceot","ceop","ceoh","ce","ceg","cegg","cegs","cen","cenj","cenh","ced","cel","celg","celm","celb","cels","celt","celp","celh","cem","ceb","cebs","ces","cess","ceng","cej","cec","cek","cet","cep","ceh","cyeo","cyeog","cyeogg","cyeogs","cyeon","cyeonj","cyeonh","cyeod","cyeol","cyeolg","cyeolm","cyeolb","cyeols","cyeolt","cyeolp","cyeolh","cyeom","cyeob","cyeobs","cyeos","cyeoss","cyeong","cyeoj","cyeoc","cyeok","cyeot","cyeop","cyeoh","cye","cyeg","cyegg","cyegs","cyen","cyenj","cyenh","cyed","cyel","cyelg","cyelm","cyelb","cyels","cyelt","cyelp","cyelh","cyem","cyeb","cyebs","cyes" ];
|
|
|
|
},{}],190:[function(require,module,exports){
|
|
module.exports = [ "cyess","cyeng","cyej","cyec","cyek","cyet","cyep","cyeh","co","cog","cogg","cogs","con","conj","conh","cod","col","colg","colm","colb","cols","colt","colp","colh","com","cob","cobs","cos","coss","cong","coj","coc","cok","cot","cop","coh","cwa","cwag","cwagg","cwags","cwan","cwanj","cwanh","cwad","cwal","cwalg","cwalm","cwalb","cwals","cwalt","cwalp","cwalh","cwam","cwab","cwabs","cwas","cwass","cwang","cwaj","cwac","cwak","cwat","cwap","cwah","cwae","cwaeg","cwaegg","cwaegs","cwaen","cwaenj","cwaenh","cwaed","cwael","cwaelg","cwaelm","cwaelb","cwaels","cwaelt","cwaelp","cwaelh","cwaem","cwaeb","cwaebs","cwaes","cwaess","cwaeng","cwaej","cwaec","cwaek","cwaet","cwaep","cwaeh","coe","coeg","coegg","coegs","coen","coenj","coenh","coed","coel","coelg","coelm","coelb","coels","coelt","coelp","coelh","coem","coeb","coebs","coes","coess","coeng","coej","coec","coek","coet","coep","coeh","cyo","cyog","cyogg","cyogs","cyon","cyonj","cyonh","cyod","cyol","cyolg","cyolm","cyolb","cyols","cyolt","cyolp","cyolh","cyom","cyob","cyobs","cyos","cyoss","cyong","cyoj","cyoc","cyok","cyot","cyop","cyoh","cu","cug","cugg","cugs","cun","cunj","cunh","cud","cul","culg","culm","culb","culs","cult","culp","culh","cum","cub","cubs","cus","cuss","cung","cuj","cuc","cuk","cut","cup","cuh","cweo","cweog","cweogg","cweogs","cweon","cweonj","cweonh","cweod","cweol","cweolg","cweolm","cweolb","cweols","cweolt","cweolp","cweolh","cweom","cweob","cweobs","cweos","cweoss","cweong","cweoj","cweoc","cweok","cweot","cweop","cweoh","cwe","cweg","cwegg","cwegs","cwen","cwenj","cwenh","cwed","cwel","cwelg","cwelm","cwelb","cwels","cwelt","cwelp","cwelh","cwem","cweb","cwebs","cwes","cwess","cweng","cwej","cwec","cwek","cwet","cwep","cweh","cwi","cwig","cwigg","cwigs","cwin","cwinj","cwinh","cwid","cwil","cwilg","cwilm","cwilb","cwils","cwilt","cwilp","cwilh","cwim","cwib","cwibs","cwis","cwiss","cwing","cwij","cwic" ];
|
|
|
|
},{}],191:[function(require,module,exports){
|
|
module.exports = [ "cwik","cwit","cwip","cwih","cyu","cyug","cyugg","cyugs","cyun","cyunj","cyunh","cyud","cyul","cyulg","cyulm","cyulb","cyuls","cyult","cyulp","cyulh","cyum","cyub","cyubs","cyus","cyuss","cyung","cyuj","cyuc","cyuk","cyut","cyup","cyuh","ceu","ceug","ceugg","ceugs","ceun","ceunj","ceunh","ceud","ceul","ceulg","ceulm","ceulb","ceuls","ceult","ceulp","ceulh","ceum","ceub","ceubs","ceus","ceuss","ceung","ceuj","ceuc","ceuk","ceut","ceup","ceuh","cyi","cyig","cyigg","cyigs","cyin","cyinj","cyinh","cyid","cyil","cyilg","cyilm","cyilb","cyils","cyilt","cyilp","cyilh","cyim","cyib","cyibs","cyis","cyiss","cying","cyij","cyic","cyik","cyit","cyip","cyih","ci","cig","cigg","cigs","cin","cinj","cinh","cid","cil","cilg","cilm","cilb","cils","cilt","cilp","cilh","cim","cib","cibs","cis","ciss","cing","cij","cic","cik","cit","cip","cih","ka","kag","kagg","kags","kan","kanj","kanh","kad","kal","kalg","kalm","kalb","kals","kalt","kalp","kalh","kam","kab","kabs","kas","kass","kang","kaj","kac","kak","kat","kap","kah","kae","kaeg","kaegg","kaegs","kaen","kaenj","kaenh","kaed","kael","kaelg","kaelm","kaelb","kaels","kaelt","kaelp","kaelh","kaem","kaeb","kaebs","kaes","kaess","kaeng","kaej","kaec","kaek","kaet","kaep","kaeh","kya","kyag","kyagg","kyags","kyan","kyanj","kyanh","kyad","kyal","kyalg","kyalm","kyalb","kyals","kyalt","kyalp","kyalh","kyam","kyab","kyabs","kyas","kyass","kyang","kyaj","kyac","kyak","kyat","kyap","kyah","kyae","kyaeg","kyaegg","kyaegs","kyaen","kyaenj","kyaenh","kyaed","kyael","kyaelg","kyaelm","kyaelb","kyaels","kyaelt","kyaelp","kyaelh","kyaem","kyaeb","kyaebs","kyaes","kyaess","kyaeng","kyaej","kyaec","kyaek","kyaet","kyaep","kyaeh","keo","keog","keogg","keogs","keon","keonj","keonh","keod","keol","keolg","keolm","keolb","keols","keolt","keolp","keolh","keom","keob","keobs","keos","keoss","keong","keoj","keoc","keok","keot","keop","keoh" ];
|
|
|
|
},{}],192:[function(require,module,exports){
|
|
module.exports = [ "ke","keg","kegg","kegs","ken","kenj","kenh","ked","kel","kelg","kelm","kelb","kels","kelt","kelp","kelh","kem","keb","kebs","kes","kess","keng","kej","kec","kek","ket","kep","keh","kyeo","kyeog","kyeogg","kyeogs","kyeon","kyeonj","kyeonh","kyeod","kyeol","kyeolg","kyeolm","kyeolb","kyeols","kyeolt","kyeolp","kyeolh","kyeom","kyeob","kyeobs","kyeos","kyeoss","kyeong","kyeoj","kyeoc","kyeok","kyeot","kyeop","kyeoh","kye","kyeg","kyegg","kyegs","kyen","kyenj","kyenh","kyed","kyel","kyelg","kyelm","kyelb","kyels","kyelt","kyelp","kyelh","kyem","kyeb","kyebs","kyes","kyess","kyeng","kyej","kyec","kyek","kyet","kyep","kyeh","ko","kog","kogg","kogs","kon","konj","konh","kod","kol","kolg","kolm","kolb","kols","kolt","kolp","kolh","kom","kob","kobs","kos","koss","kong","koj","koc","kok","kot","kop","koh","kwa","kwag","kwagg","kwags","kwan","kwanj","kwanh","kwad","kwal","kwalg","kwalm","kwalb","kwals","kwalt","kwalp","kwalh","kwam","kwab","kwabs","kwas","kwass","kwang","kwaj","kwac","kwak","kwat","kwap","kwah","kwae","kwaeg","kwaegg","kwaegs","kwaen","kwaenj","kwaenh","kwaed","kwael","kwaelg","kwaelm","kwaelb","kwaels","kwaelt","kwaelp","kwaelh","kwaem","kwaeb","kwaebs","kwaes","kwaess","kwaeng","kwaej","kwaec","kwaek","kwaet","kwaep","kwaeh","koe","koeg","koegg","koegs","koen","koenj","koenh","koed","koel","koelg","koelm","koelb","koels","koelt","koelp","koelh","koem","koeb","koebs","koes","koess","koeng","koej","koec","koek","koet","koep","koeh","kyo","kyog","kyogg","kyogs","kyon","kyonj","kyonh","kyod","kyol","kyolg","kyolm","kyolb","kyols","kyolt","kyolp","kyolh","kyom","kyob","kyobs","kyos","kyoss","kyong","kyoj","kyoc","kyok","kyot","kyop","kyoh","ku","kug","kugg","kugs","kun","kunj","kunh","kud","kul","kulg","kulm","kulb","kuls","kult","kulp","kulh","kum","kub","kubs","kus","kuss","kung","kuj","kuc","kuk","kut","kup","kuh","kweo","kweog","kweogg","kweogs" ];
|
|
|
|
},{}],193:[function(require,module,exports){
|
|
module.exports = [ "kweon","kweonj","kweonh","kweod","kweol","kweolg","kweolm","kweolb","kweols","kweolt","kweolp","kweolh","kweom","kweob","kweobs","kweos","kweoss","kweong","kweoj","kweoc","kweok","kweot","kweop","kweoh","kwe","kweg","kwegg","kwegs","kwen","kwenj","kwenh","kwed","kwel","kwelg","kwelm","kwelb","kwels","kwelt","kwelp","kwelh","kwem","kweb","kwebs","kwes","kwess","kweng","kwej","kwec","kwek","kwet","kwep","kweh","kwi","kwig","kwigg","kwigs","kwin","kwinj","kwinh","kwid","kwil","kwilg","kwilm","kwilb","kwils","kwilt","kwilp","kwilh","kwim","kwib","kwibs","kwis","kwiss","kwing","kwij","kwic","kwik","kwit","kwip","kwih","kyu","kyug","kyugg","kyugs","kyun","kyunj","kyunh","kyud","kyul","kyulg","kyulm","kyulb","kyuls","kyult","kyulp","kyulh","kyum","kyub","kyubs","kyus","kyuss","kyung","kyuj","kyuc","kyuk","kyut","kyup","kyuh","keu","keug","keugg","keugs","keun","keunj","keunh","keud","keul","keulg","keulm","keulb","keuls","keult","keulp","keulh","keum","keub","keubs","keus","keuss","keung","keuj","keuc","keuk","keut","keup","keuh","kyi","kyig","kyigg","kyigs","kyin","kyinj","kyinh","kyid","kyil","kyilg","kyilm","kyilb","kyils","kyilt","kyilp","kyilh","kyim","kyib","kyibs","kyis","kyiss","kying","kyij","kyic","kyik","kyit","kyip","kyih","ki","kig","kigg","kigs","kin","kinj","kinh","kid","kil","kilg","kilm","kilb","kils","kilt","kilp","kilh","kim","kib","kibs","kis","kiss","king","kij","kic","kik","kit","kip","kih","ta","tag","tagg","tags","tan","tanj","tanh","tad","tal","talg","talm","talb","tals","talt","talp","talh","tam","tab","tabs","tas","tass","tang","taj","tac","tak","tat","tap","tah","tae","taeg","taegg","taegs","taen","taenj","taenh","taed","tael","taelg","taelm","taelb","taels","taelt","taelp","taelh","taem","taeb","taebs","taes","taess","taeng","taej","taec","taek","taet","taep","taeh","tya","tyag","tyagg","tyags","tyan","tyanj","tyanh","tyad" ];
|
|
|
|
},{}],194:[function(require,module,exports){
|
|
module.exports = [ "tyal","tyalg","tyalm","tyalb","tyals","tyalt","tyalp","tyalh","tyam","tyab","tyabs","tyas","tyass","tyang","tyaj","tyac","tyak","tyat","tyap","tyah","tyae","tyaeg","tyaegg","tyaegs","tyaen","tyaenj","tyaenh","tyaed","tyael","tyaelg","tyaelm","tyaelb","tyaels","tyaelt","tyaelp","tyaelh","tyaem","tyaeb","tyaebs","tyaes","tyaess","tyaeng","tyaej","tyaec","tyaek","tyaet","tyaep","tyaeh","teo","teog","teogg","teogs","teon","teonj","teonh","teod","teol","teolg","teolm","teolb","teols","teolt","teolp","teolh","teom","teob","teobs","teos","teoss","teong","teoj","teoc","teok","teot","teop","teoh","te","teg","tegg","tegs","ten","tenj","tenh","ted","tel","telg","telm","telb","tels","telt","telp","telh","tem","teb","tebs","tes","tess","teng","tej","tec","tek","tet","tep","teh","tyeo","tyeog","tyeogg","tyeogs","tyeon","tyeonj","tyeonh","tyeod","tyeol","tyeolg","tyeolm","tyeolb","tyeols","tyeolt","tyeolp","tyeolh","tyeom","tyeob","tyeobs","tyeos","tyeoss","tyeong","tyeoj","tyeoc","tyeok","tyeot","tyeop","tyeoh","tye","tyeg","tyegg","tyegs","tyen","tyenj","tyenh","tyed","tyel","tyelg","tyelm","tyelb","tyels","tyelt","tyelp","tyelh","tyem","tyeb","tyebs","tyes","tyess","tyeng","tyej","tyec","tyek","tyet","tyep","tyeh","to","tog","togg","togs","ton","tonj","tonh","tod","tol","tolg","tolm","tolb","tols","tolt","tolp","tolh","tom","tob","tobs","tos","toss","tong","toj","toc","tok","tot","top","toh","twa","twag","twagg","twags","twan","twanj","twanh","twad","twal","twalg","twalm","twalb","twals","twalt","twalp","twalh","twam","twab","twabs","twas","twass","twang","twaj","twac","twak","twat","twap","twah","twae","twaeg","twaegg","twaegs","twaen","twaenj","twaenh","twaed","twael","twaelg","twaelm","twaelb","twaels","twaelt","twaelp","twaelh","twaem","twaeb","twaebs","twaes","twaess","twaeng","twaej","twaec","twaek","twaet","twaep","twaeh","toe","toeg","toegg","toegs","toen","toenj","toenh","toed","toel","toelg","toelm","toelb" ];
|
|
|
|
},{}],195:[function(require,module,exports){
|
|
module.exports = [ "toels","toelt","toelp","toelh","toem","toeb","toebs","toes","toess","toeng","toej","toec","toek","toet","toep","toeh","tyo","tyog","tyogg","tyogs","tyon","tyonj","tyonh","tyod","tyol","tyolg","tyolm","tyolb","tyols","tyolt","tyolp","tyolh","tyom","tyob","tyobs","tyos","tyoss","tyong","tyoj","tyoc","tyok","tyot","tyop","tyoh","tu","tug","tugg","tugs","tun","tunj","tunh","tud","tul","tulg","tulm","tulb","tuls","tult","tulp","tulh","tum","tub","tubs","tus","tuss","tung","tuj","tuc","tuk","tut","tup","tuh","tweo","tweog","tweogg","tweogs","tweon","tweonj","tweonh","tweod","tweol","tweolg","tweolm","tweolb","tweols","tweolt","tweolp","tweolh","tweom","tweob","tweobs","tweos","tweoss","tweong","tweoj","tweoc","tweok","tweot","tweop","tweoh","twe","tweg","twegg","twegs","twen","twenj","twenh","twed","twel","twelg","twelm","twelb","twels","twelt","twelp","twelh","twem","tweb","twebs","twes","twess","tweng","twej","twec","twek","twet","twep","tweh","twi","twig","twigg","twigs","twin","twinj","twinh","twid","twil","twilg","twilm","twilb","twils","twilt","twilp","twilh","twim","twib","twibs","twis","twiss","twing","twij","twic","twik","twit","twip","twih","tyu","tyug","tyugg","tyugs","tyun","tyunj","tyunh","tyud","tyul","tyulg","tyulm","tyulb","tyuls","tyult","tyulp","tyulh","tyum","tyub","tyubs","tyus","tyuss","tyung","tyuj","tyuc","tyuk","tyut","tyup","tyuh","teu","teug","teugg","teugs","teun","teunj","teunh","teud","teul","teulg","teulm","teulb","teuls","teult","teulp","teulh","teum","teub","teubs","teus","teuss","teung","teuj","teuc","teuk","teut","teup","teuh","tyi","tyig","tyigg","tyigs","tyin","tyinj","tyinh","tyid","tyil","tyilg","tyilm","tyilb","tyils","tyilt","tyilp","tyilh","tyim","tyib","tyibs","tyis","tyiss","tying","tyij","tyic","tyik","tyit","tyip","tyih","ti","tig","tigg","tigs","tin","tinj","tinh","tid","til","tilg","tilm","tilb","tils","tilt","tilp","tilh" ];
|
|
|
|
},{}],196:[function(require,module,exports){
|
|
module.exports = [ "tim","tib","tibs","tis","tiss","ting","tij","tic","tik","tit","tip","tih","pa","pag","pagg","pags","pan","panj","panh","pad","pal","palg","palm","palb","pals","palt","palp","palh","pam","pab","pabs","pas","pass","pang","paj","pac","pak","pat","pap","pah","pae","paeg","paegg","paegs","paen","paenj","paenh","paed","pael","paelg","paelm","paelb","paels","paelt","paelp","paelh","paem","paeb","paebs","paes","paess","paeng","paej","paec","paek","paet","paep","paeh","pya","pyag","pyagg","pyags","pyan","pyanj","pyanh","pyad","pyal","pyalg","pyalm","pyalb","pyals","pyalt","pyalp","pyalh","pyam","pyab","pyabs","pyas","pyass","pyang","pyaj","pyac","pyak","pyat","pyap","pyah","pyae","pyaeg","pyaegg","pyaegs","pyaen","pyaenj","pyaenh","pyaed","pyael","pyaelg","pyaelm","pyaelb","pyaels","pyaelt","pyaelp","pyaelh","pyaem","pyaeb","pyaebs","pyaes","pyaess","pyaeng","pyaej","pyaec","pyaek","pyaet","pyaep","pyaeh","peo","peog","peogg","peogs","peon","peonj","peonh","peod","peol","peolg","peolm","peolb","peols","peolt","peolp","peolh","peom","peob","peobs","peos","peoss","peong","peoj","peoc","peok","peot","peop","peoh","pe","peg","pegg","pegs","pen","penj","penh","ped","pel","pelg","pelm","pelb","pels","pelt","pelp","pelh","pem","peb","pebs","pes","pess","peng","pej","pec","pek","pet","pep","peh","pyeo","pyeog","pyeogg","pyeogs","pyeon","pyeonj","pyeonh","pyeod","pyeol","pyeolg","pyeolm","pyeolb","pyeols","pyeolt","pyeolp","pyeolh","pyeom","pyeob","pyeobs","pyeos","pyeoss","pyeong","pyeoj","pyeoc","pyeok","pyeot","pyeop","pyeoh","pye","pyeg","pyegg","pyegs","pyen","pyenj","pyenh","pyed","pyel","pyelg","pyelm","pyelb","pyels","pyelt","pyelp","pyelh","pyem","pyeb","pyebs","pyes","pyess","pyeng","pyej","pyec","pyek","pyet","pyep","pyeh","po","pog","pogg","pogs","pon","ponj","ponh","pod","pol","polg","polm","polb","pols","polt","polp","polh","pom","pob","pobs","pos" ];
|
|
|
|
},{}],197:[function(require,module,exports){
|
|
module.exports = [ "poss","pong","poj","poc","pok","pot","pop","poh","pwa","pwag","pwagg","pwags","pwan","pwanj","pwanh","pwad","pwal","pwalg","pwalm","pwalb","pwals","pwalt","pwalp","pwalh","pwam","pwab","pwabs","pwas","pwass","pwang","pwaj","pwac","pwak","pwat","pwap","pwah","pwae","pwaeg","pwaegg","pwaegs","pwaen","pwaenj","pwaenh","pwaed","pwael","pwaelg","pwaelm","pwaelb","pwaels","pwaelt","pwaelp","pwaelh","pwaem","pwaeb","pwaebs","pwaes","pwaess","pwaeng","pwaej","pwaec","pwaek","pwaet","pwaep","pwaeh","poe","poeg","poegg","poegs","poen","poenj","poenh","poed","poel","poelg","poelm","poelb","poels","poelt","poelp","poelh","poem","poeb","poebs","poes","poess","poeng","poej","poec","poek","poet","poep","poeh","pyo","pyog","pyogg","pyogs","pyon","pyonj","pyonh","pyod","pyol","pyolg","pyolm","pyolb","pyols","pyolt","pyolp","pyolh","pyom","pyob","pyobs","pyos","pyoss","pyong","pyoj","pyoc","pyok","pyot","pyop","pyoh","pu","pug","pugg","pugs","pun","punj","punh","pud","pul","pulg","pulm","pulb","puls","pult","pulp","pulh","pum","pub","pubs","pus","puss","pung","puj","puc","puk","put","pup","puh","pweo","pweog","pweogg","pweogs","pweon","pweonj","pweonh","pweod","pweol","pweolg","pweolm","pweolb","pweols","pweolt","pweolp","pweolh","pweom","pweob","pweobs","pweos","pweoss","pweong","pweoj","pweoc","pweok","pweot","pweop","pweoh","pwe","pweg","pwegg","pwegs","pwen","pwenj","pwenh","pwed","pwel","pwelg","pwelm","pwelb","pwels","pwelt","pwelp","pwelh","pwem","pweb","pwebs","pwes","pwess","pweng","pwej","pwec","pwek","pwet","pwep","pweh","pwi","pwig","pwigg","pwigs","pwin","pwinj","pwinh","pwid","pwil","pwilg","pwilm","pwilb","pwils","pwilt","pwilp","pwilh","pwim","pwib","pwibs","pwis","pwiss","pwing","pwij","pwic","pwik","pwit","pwip","pwih","pyu","pyug","pyugg","pyugs","pyun","pyunj","pyunh","pyud","pyul","pyulg","pyulm","pyulb","pyuls","pyult","pyulp","pyulh","pyum","pyub","pyubs","pyus","pyuss","pyung","pyuj","pyuc" ];
|
|
|
|
},{}],198:[function(require,module,exports){
|
|
module.exports = [ "pyuk","pyut","pyup","pyuh","peu","peug","peugg","peugs","peun","peunj","peunh","peud","peul","peulg","peulm","peulb","peuls","peult","peulp","peulh","peum","peub","peubs","peus","peuss","peung","peuj","peuc","peuk","peut","peup","peuh","pyi","pyig","pyigg","pyigs","pyin","pyinj","pyinh","pyid","pyil","pyilg","pyilm","pyilb","pyils","pyilt","pyilp","pyilh","pyim","pyib","pyibs","pyis","pyiss","pying","pyij","pyic","pyik","pyit","pyip","pyih","pi","pig","pigg","pigs","pin","pinj","pinh","pid","pil","pilg","pilm","pilb","pils","pilt","pilp","pilh","pim","pib","pibs","pis","piss","ping","pij","pic","pik","pit","pip","pih","ha","hag","hagg","hags","han","hanj","hanh","had","hal","halg","halm","halb","hals","halt","halp","halh","ham","hab","habs","has","hass","hang","haj","hac","hak","hat","hap","hah","hae","haeg","haegg","haegs","haen","haenj","haenh","haed","hael","haelg","haelm","haelb","haels","haelt","haelp","haelh","haem","haeb","haebs","haes","haess","haeng","haej","haec","haek","haet","haep","haeh","hya","hyag","hyagg","hyags","hyan","hyanj","hyanh","hyad","hyal","hyalg","hyalm","hyalb","hyals","hyalt","hyalp","hyalh","hyam","hyab","hyabs","hyas","hyass","hyang","hyaj","hyac","hyak","hyat","hyap","hyah","hyae","hyaeg","hyaegg","hyaegs","hyaen","hyaenj","hyaenh","hyaed","hyael","hyaelg","hyaelm","hyaelb","hyaels","hyaelt","hyaelp","hyaelh","hyaem","hyaeb","hyaebs","hyaes","hyaess","hyaeng","hyaej","hyaec","hyaek","hyaet","hyaep","hyaeh","heo","heog","heogg","heogs","heon","heonj","heonh","heod","heol","heolg","heolm","heolb","heols","heolt","heolp","heolh","heom","heob","heobs","heos","heoss","heong","heoj","heoc","heok","heot","heop","heoh","he","heg","hegg","hegs","hen","henj","henh","hed","hel","helg","helm","helb","hels","helt","help","helh","hem","heb","hebs","hes","hess","heng","hej","hec","hek","het","hep","heh" ];
|
|
|
|
},{}],199:[function(require,module,exports){
|
|
module.exports = [ "hyeo","hyeog","hyeogg","hyeogs","hyeon","hyeonj","hyeonh","hyeod","hyeol","hyeolg","hyeolm","hyeolb","hyeols","hyeolt","hyeolp","hyeolh","hyeom","hyeob","hyeobs","hyeos","hyeoss","hyeong","hyeoj","hyeoc","hyeok","hyeot","hyeop","hyeoh","hye","hyeg","hyegg","hyegs","hyen","hyenj","hyenh","hyed","hyel","hyelg","hyelm","hyelb","hyels","hyelt","hyelp","hyelh","hyem","hyeb","hyebs","hyes","hyess","hyeng","hyej","hyec","hyek","hyet","hyep","hyeh","ho","hog","hogg","hogs","hon","honj","honh","hod","hol","holg","holm","holb","hols","holt","holp","holh","hom","hob","hobs","hos","hoss","hong","hoj","hoc","hok","hot","hop","hoh","hwa","hwag","hwagg","hwags","hwan","hwanj","hwanh","hwad","hwal","hwalg","hwalm","hwalb","hwals","hwalt","hwalp","hwalh","hwam","hwab","hwabs","hwas","hwass","hwang","hwaj","hwac","hwak","hwat","hwap","hwah","hwae","hwaeg","hwaegg","hwaegs","hwaen","hwaenj","hwaenh","hwaed","hwael","hwaelg","hwaelm","hwaelb","hwaels","hwaelt","hwaelp","hwaelh","hwaem","hwaeb","hwaebs","hwaes","hwaess","hwaeng","hwaej","hwaec","hwaek","hwaet","hwaep","hwaeh","hoe","hoeg","hoegg","hoegs","hoen","hoenj","hoenh","hoed","hoel","hoelg","hoelm","hoelb","hoels","hoelt","hoelp","hoelh","hoem","hoeb","hoebs","hoes","hoess","hoeng","hoej","hoec","hoek","hoet","hoep","hoeh","hyo","hyog","hyogg","hyogs","hyon","hyonj","hyonh","hyod","hyol","hyolg","hyolm","hyolb","hyols","hyolt","hyolp","hyolh","hyom","hyob","hyobs","hyos","hyoss","hyong","hyoj","hyoc","hyok","hyot","hyop","hyoh","hu","hug","hugg","hugs","hun","hunj","hunh","hud","hul","hulg","hulm","hulb","huls","hult","hulp","hulh","hum","hub","hubs","hus","huss","hung","huj","huc","huk","hut","hup","huh","hweo","hweog","hweogg","hweogs","hweon","hweonj","hweonh","hweod","hweol","hweolg","hweolm","hweolb","hweols","hweolt","hweolp","hweolh","hweom","hweob","hweobs","hweos","hweoss","hweong","hweoj","hweoc","hweok","hweot","hweop","hweoh","hwe","hweg","hwegg","hwegs" ];
|
|
|
|
},{}],200:[function(require,module,exports){
|
|
module.exports = [ "hwen","hwenj","hwenh","hwed","hwel","hwelg","hwelm","hwelb","hwels","hwelt","hwelp","hwelh","hwem","hweb","hwebs","hwes","hwess","hweng","hwej","hwec","hwek","hwet","hwep","hweh","hwi","hwig","hwigg","hwigs","hwin","hwinj","hwinh","hwid","hwil","hwilg","hwilm","hwilb","hwils","hwilt","hwilp","hwilh","hwim","hwib","hwibs","hwis","hwiss","hwing","hwij","hwic","hwik","hwit","hwip","hwih","hyu","hyug","hyugg","hyugs","hyun","hyunj","hyunh","hyud","hyul","hyulg","hyulm","hyulb","hyuls","hyult","hyulp","hyulh","hyum","hyub","hyubs","hyus","hyuss","hyung","hyuj","hyuc","hyuk","hyut","hyup","hyuh","heu","heug","heugg","heugs","heun","heunj","heunh","heud","heul","heulg","heulm","heulb","heuls","heult","heulp","heulh","heum","heub","heubs","heus","heuss","heung","heuj","heuc","heuk","heut","heup","heuh","hyi","hyig","hyigg","hyigs","hyin","hyinj","hyinh","hyid","hyil","hyilg","hyilm","hyilb","hyils","hyilt","hyilp","hyilh","hyim","hyib","hyibs","hyis","hyiss","hying","hyij","hyic","hyik","hyit","hyip","hyih","hi","hig","higg","higs","hin","hinj","hinh","hid","hil","hilg","hilm","hilb","hils","hilt","hilp","hilh","him","hib","hibs","his","hiss","hing","hij","hic","hik","hit","hip","hih","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]" ];
|
|
|
|
},{}],201:[function(require,module,exports){
|
|
module.exports = [ "Kay ","Kayng ","Ke ","Ko ","Kol ","Koc ","Kwi ","Kwi ","Kyun ","Kul ","Kum ","Na ","Na ","Na ","La ","Na ","Na ","Na ","Na ","Na ","Nak ","Nak ","Nak ","Nak ","Nak ","Nak ","Nak ","Nan ","Nan ","Nan ","Nan ","Nan ","Nan ","Nam ","Nam ","Nam ","Nam ","Nap ","Nap ","Nap ","Nang ","Nang ","Nang ","Nang ","Nang ","Nay ","Nayng ","No ","No ","No ","No ","No ","No ","No ","No ","No ","No ","No ","No ","Nok ","Nok ","Nok ","Nok ","Nok ","Nok ","Non ","Nong ","Nong ","Nong ","Nong ","Noy ","Noy ","Noy ","Noy ","Nwu ","Nwu ","Nwu ","Nwu ","Nwu ","Nwu ","Nwu ","Nwu ","Nuk ","Nuk ","Num ","Nung ","Nung ","Nung ","Nung ","Nung ","Twu ","La ","Lak ","Lak ","Lan ","Lyeng ","Lo ","Lyul ","Li ","Pey ","Pen ","Pyen ","Pwu ","Pwul ","Pi ","Sak ","Sak ","Sam ","Sayk ","Sayng ","Sep ","Sey ","Sway ","Sin ","Sim ","Sip ","Ya ","Yak ","Yak ","Yang ","Yang ","Yang ","Yang ","Yang ","Yang ","Yang ","Yang ","Ye ","Ye ","Ye ","Ye ","Ye ","Ye ","Ye ","Ye ","Ye ","Ye ","Ye ","Yek ","Yek ","Yek ","Yek ","Yen ","Yen ","Yen ","Yen ","Yen ","Yen ","Yen ","Yen ","Yen ","Yen ","Yen ","Yen ","Yen ","Yen ","Yel ","Yel ","Yel ","Yel ","Yel ","Yel ","Yem ","Yem ","Yem ","Yem ","Yem ","Yep ","Yeng ","Yeng ","Yeng ","Yeng ","Yeng ","Yeng ","Yeng ","Yeng ","Yeng ","Yeng ","Yeng ","Yeng ","Yeng ","Yey ","Yey ","Yey ","Yey ","O ","Yo ","Yo ","Yo ","Yo ","Yo ","Yo ","Yo ","Yo ","Yo ","Yo ","Yong ","Wun ","Wen ","Yu ","Yu ","Yu ","Yu ","Yu ","Yu ","Yu ","Yu ","Yu ","Yu ","Yuk ","Yuk ","Yuk ","Yun ","Yun ","Yun ","Yun ","Yul ","Yul ","Yul ","Yul ","Yung ","I ","I ","I ","I ","I ","I ","I ","I ","I ","I ","I ","I ","I ","I ","Ik ","Ik ","In ","In ","In ","In ","In ","In ","In ","Im ","Im ","Im ","Ip ","Ip ","Ip ","Cang ","Cek ","Ci ","Cip ","Cha ","Chek " ];
|
|
|
|
},{}],202:[function(require,module,exports){
|
|
module.exports = [ "Chey ","Thak ","Thak ","Thang ","Thayk ","Thong ","Pho ","Phok ","Hang ","Hang ","Hyen ","Hwak ","Wu ","Huo ","[?] ","[?] ","Zhong ","[?] ","Qing ","[?] ","[?] ","Xi ","Zhu ","Yi ","Li ","Shen ","Xiang ","Fu ","Jing ","Jing ","Yu ","[?] ","Hagi ","[?] ","Zhu ","[?] ","[?] ","Yi ","Du ","[?] ","[?] ","[?] ","Fan ","Si ","Guan ","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]" ];
|
|
|
|
},{}],203:[function(require,module,exports){
|
|
module.exports = [ "ff","fi","fl","ffi","ffl","st","st","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","mn","me","mi","vn","mkh","[?]","[?]","[?]","[?]","[?]","yi","","ay","`","","d","h","k","l","m","m","t","+","sh","s","sh","s","a","a","","b","g","d","h","v","z","[?]","t","y","k","k","l","[?]","l","[?]","n","n","[?]","p","p","[?]","ts","ts","r","sh","t","vo","b","k","p","l","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","" ];
|
|
|
|
},{}],204:[function(require,module,exports){
|
|
module.exports = [ "","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","" ];
|
|
|
|
},{}],205:[function(require,module,exports){
|
|
module.exports = [ "","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","[?]","[?]","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","","","","","","","","","","","","","[?]","[?]","[?]" ];
|
|
|
|
},{}],206:[function(require,module,exports){
|
|
module.exports = [ "[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","","","","~","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","..","--","-","_","_","(",") ","{","} ","[","] ","[(",")] ","<<",">> ","<","> ","[","] ","{","}","[?]","[?]","[?]","[?]","","","","","","","",",",",",".","",";",":","?","!","-","(",")","{","}","{","}","#","&","*","+","-","<",">","=","","\\","$","%","@","[?]","[?]","[?]","[?]","","","","[?]","","[?]","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","[?]","[?]","" ];
|
|
|
|
},{}],207:[function(require,module,exports){
|
|
module.exports = [ "[?]","!","\"","#","$","%","&","'","(",")","*","+",",","-",".","/","0","1","2","3","4","5","6","7","8","9",":",";","<","=",">","?","@","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","[","\\","]","^","_","`","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","{","|","}","~","[?]","[?]",".","[","]",",","*","wo","a","i","u","e","o","ya","yu","yo","tu","+","a","i","u","e","o","ka","ki","ku","ke","ko","sa","si","su","se","so","ta","ti","tu","te","to","na","ni","nu","ne","no","ha","hi","hu","he","ho","ma","mi","mu","me","mo","ya","yu","yo","ra","ri","ru","re","ro","wa","n",":",";","","g","gg","gs","n","nj","nh","d","dd","r","lg","lm","lb","ls","lt","lp","rh","m","b","bb","bs","s","ss","","j","jj","c","k","t","p","h","[?]","[?]","[?]","a","ae","ya","yae","eo","e","[?]","[?]","yeo","ye","o","wa","wae","oe","[?]","[?]","yo","u","weo","we","wi","yu","[?]","[?]","eu","yi","i","[?]","[?]","[?]","/C","PS","!","-","|","Y=","W=","[?]","|","-","|","-","|","#","O","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","[?]","{","|","}","","","","" ];
|
|
|
|
},{}],208:[function(require,module,exports){
|
|
/**
|
|
* Unidecode takes UTF-8 data and tries to represent it in US-ASCII characters (i.e., the universally displayable characters between 0x00 and 0x7F).
|
|
* The representation is almost always an attempt at transliteration -- i.e., conveying, in Roman letters, the pronunciation expressed by the text in
|
|
* some other writing system.
|
|
*
|
|
* The tables used (in data) are converted from the tables provided in the perl library Text::Unidecode (http://search.cpan.org/dist/Text-Unidecode/lib/Text/Unidecode.pm)
|
|
* and are distributed under the perl license
|
|
*
|
|
* @author Francois-Guillaume Ribreau
|
|
*
|
|
* Based on the port of unidecode for php
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
var tr = {};
|
|
var utf8_rx = /(?![\x00-\x7F]|[\xC0-\xDF][\x80-\xBF]|[\xE0-\xEF][\x80-\xBF]{2}|[\xF0-\xF7][\x80-\xBF]{3})./g;
|
|
|
|
module.exports = function (str) {
|
|
return str.replace(utf8_rx, unidecode_internal_replace);
|
|
};
|
|
|
|
function unidecode_internal_replace(match) {
|
|
var utf16 = utf8_to_utf16(match);
|
|
|
|
if (utf16 > 0xFFFF) {
|
|
return '_';
|
|
} else {
|
|
|
|
var h = utf16 >> 8;
|
|
var l = utf16 & 0xFF;
|
|
|
|
// (18) 18 > h < 1e (30)
|
|
if (h > 24 && h < 30) return '';
|
|
|
|
//(d7) 215 > h < 249 (f9) no supported
|
|
if (h > 215 && h < 249) return '';
|
|
|
|
if (!tr[h]) {
|
|
switch (dec2hex(h)) {
|
|
case '00':
|
|
tr[h] = require('./data/x00');
|
|
break;
|
|
case '01':
|
|
tr[h] = require('./data/x01');
|
|
break;
|
|
case '02':
|
|
tr[h] = require('./data/x02');
|
|
break;
|
|
case '03':
|
|
tr[h] = require('./data/x03');
|
|
break;
|
|
case '04':
|
|
tr[h] = require('./data/x04');
|
|
break;
|
|
case '05':
|
|
tr[h] = require('./data/x05');
|
|
break;
|
|
case '06':
|
|
tr[h] = require('./data/x06');
|
|
break;
|
|
case '07':
|
|
tr[h] = require('./data/x07');
|
|
break;
|
|
case '09':
|
|
tr[h] = require('./data/x09');
|
|
break;
|
|
case '0a':
|
|
tr[h] = require('./data/x0a');
|
|
break;
|
|
case '0b':
|
|
tr[h] = require('./data/x0b');
|
|
break;
|
|
case '0c':
|
|
tr[h] = require('./data/x0c');
|
|
break;
|
|
case '0d':
|
|
tr[h] = require('./data/x0d');
|
|
break;
|
|
case '0e':
|
|
tr[h] = require('./data/x0e');
|
|
break;
|
|
case '0f':
|
|
tr[h] = require('./data/x0f');
|
|
break;
|
|
case '10':
|
|
tr[h] = require('./data/x10');
|
|
break;
|
|
case '11':
|
|
tr[h] = require('./data/x11');
|
|
break;
|
|
case '12':
|
|
tr[h] = require('./data/x12');
|
|
break;
|
|
case '13':
|
|
tr[h] = require('./data/x13');
|
|
break;
|
|
case '14':
|
|
tr[h] = require('./data/x14');
|
|
break;
|
|
case '15':
|
|
tr[h] = require('./data/x15');
|
|
break;
|
|
case '16':
|
|
tr[h] = require('./data/x16');
|
|
break;
|
|
case '17':
|
|
tr[h] = require('./data/x17');
|
|
break;
|
|
case '18':
|
|
tr[h] = require('./data/x18');
|
|
break;
|
|
case '1e':
|
|
tr[h] = require('./data/x1e');
|
|
break;
|
|
case '1f':
|
|
tr[h] = require('./data/x1f');
|
|
break;
|
|
case '20':
|
|
tr[h] = require('./data/x20');
|
|
break;
|
|
case '21':
|
|
tr[h] = require('./data/x21');
|
|
break;
|
|
case '22':
|
|
tr[h] = require('./data/x22');
|
|
break;
|
|
case '23':
|
|
tr[h] = require('./data/x23');
|
|
break;
|
|
case '24':
|
|
tr[h] = require('./data/x24');
|
|
break;
|
|
case '25':
|
|
tr[h] = require('./data/x25');
|
|
break;
|
|
case '26':
|
|
tr[h] = require('./data/x26');
|
|
break;
|
|
case '27':
|
|
tr[h] = require('./data/x27');
|
|
break;
|
|
case '28':
|
|
tr[h] = require('./data/x28');
|
|
break;
|
|
case '2e':
|
|
tr[h] = require('./data/x2e');
|
|
break;
|
|
case '2f':
|
|
tr[h] = require('./data/x2f');
|
|
break;
|
|
case '30':
|
|
tr[h] = require('./data/x30');
|
|
break;
|
|
case '31':
|
|
tr[h] = require('./data/x31');
|
|
break;
|
|
case '32':
|
|
tr[h] = require('./data/x32');
|
|
break;
|
|
case '33':
|
|
tr[h] = require('./data/x33');
|
|
break;
|
|
case '4d':
|
|
tr[h] = require('./data/x4d');
|
|
break;
|
|
case '4e':
|
|
tr[h] = require('./data/x4e');
|
|
break;
|
|
case '4f':
|
|
tr[h] = require('./data/x4f');
|
|
break;
|
|
case '50':
|
|
tr[h] = require('./data/x50');
|
|
break;
|
|
case '51':
|
|
tr[h] = require('./data/x51');
|
|
break;
|
|
case '52':
|
|
tr[h] = require('./data/x52');
|
|
break;
|
|
case '53':
|
|
tr[h] = require('./data/x53');
|
|
break;
|
|
case '54':
|
|
tr[h] = require('./data/x54');
|
|
break;
|
|
case '55':
|
|
tr[h] = require('./data/x55');
|
|
break;
|
|
case '56':
|
|
tr[h] = require('./data/x56');
|
|
break;
|
|
case '57':
|
|
tr[h] = require('./data/x57');
|
|
break;
|
|
case '58':
|
|
tr[h] = require('./data/x58');
|
|
break;
|
|
case '59':
|
|
tr[h] = require('./data/x59');
|
|
break;
|
|
case '5a':
|
|
tr[h] = require('./data/x5a');
|
|
break;
|
|
case '5b':
|
|
tr[h] = require('./data/x5b');
|
|
break;
|
|
case '5c':
|
|
tr[h] = require('./data/x5c');
|
|
break;
|
|
case '5d':
|
|
tr[h] = require('./data/x5d');
|
|
break;
|
|
case '5e':
|
|
tr[h] = require('./data/x5e');
|
|
break;
|
|
case '5f':
|
|
tr[h] = require('./data/x5f');
|
|
break;
|
|
case '60':
|
|
tr[h] = require('./data/x60');
|
|
break;
|
|
case '61':
|
|
tr[h] = require('./data/x61');
|
|
break;
|
|
case '62':
|
|
tr[h] = require('./data/x62');
|
|
break;
|
|
case '63':
|
|
tr[h] = require('./data/x63');
|
|
break;
|
|
case '64':
|
|
tr[h] = require('./data/x64');
|
|
break;
|
|
case '65':
|
|
tr[h] = require('./data/x65');
|
|
break;
|
|
case '66':
|
|
tr[h] = require('./data/x66');
|
|
break;
|
|
case '67':
|
|
tr[h] = require('./data/x67');
|
|
break;
|
|
case '68':
|
|
tr[h] = require('./data/x68');
|
|
break;
|
|
case '69':
|
|
tr[h] = require('./data/x69');
|
|
break;
|
|
case '6a':
|
|
tr[h] = require('./data/x6a');
|
|
break;
|
|
case '6b':
|
|
tr[h] = require('./data/x6b');
|
|
break;
|
|
case '6c':
|
|
tr[h] = require('./data/x6c');
|
|
break;
|
|
case '6d':
|
|
tr[h] = require('./data/x6d');
|
|
break;
|
|
case '6e':
|
|
tr[h] = require('./data/x6e');
|
|
break;
|
|
case '6f':
|
|
tr[h] = require('./data/x6f');
|
|
break;
|
|
case '70':
|
|
tr[h] = require('./data/x70');
|
|
break;
|
|
case '71':
|
|
tr[h] = require('./data/x71');
|
|
break;
|
|
case '72':
|
|
tr[h] = require('./data/x72');
|
|
break;
|
|
case '73':
|
|
tr[h] = require('./data/x73');
|
|
break;
|
|
case '74':
|
|
tr[h] = require('./data/x74');
|
|
break;
|
|
case '75':
|
|
tr[h] = require('./data/x75');
|
|
break;
|
|
case '76':
|
|
tr[h] = require('./data/x76');
|
|
break;
|
|
case '77':
|
|
tr[h] = require('./data/x77');
|
|
break;
|
|
case '78':
|
|
tr[h] = require('./data/x78');
|
|
break;
|
|
case '79':
|
|
tr[h] = require('./data/x79');
|
|
break;
|
|
case '7a':
|
|
tr[h] = require('./data/x7a');
|
|
break;
|
|
case '7b':
|
|
tr[h] = require('./data/x7b');
|
|
break;
|
|
case '7c':
|
|
tr[h] = require('./data/x7c');
|
|
break;
|
|
case '7d':
|
|
tr[h] = require('./data/x7d');
|
|
break;
|
|
case '7e':
|
|
tr[h] = require('./data/x7e');
|
|
break;
|
|
case '7f':
|
|
tr[h] = require('./data/x7f');
|
|
break;
|
|
case '80':
|
|
tr[h] = require('./data/x80');
|
|
break;
|
|
case '81':
|
|
tr[h] = require('./data/x81');
|
|
break;
|
|
case '82':
|
|
tr[h] = require('./data/x82');
|
|
break;
|
|
case '83':
|
|
tr[h] = require('./data/x83');
|
|
break;
|
|
case '84':
|
|
tr[h] = require('./data/x84');
|
|
break;
|
|
case '85':
|
|
tr[h] = require('./data/x85');
|
|
break;
|
|
case '86':
|
|
tr[h] = require('./data/x86');
|
|
break;
|
|
case '87':
|
|
tr[h] = require('./data/x87');
|
|
break;
|
|
case '88':
|
|
tr[h] = require('./data/x88');
|
|
break;
|
|
case '89':
|
|
tr[h] = require('./data/x89');
|
|
break;
|
|
case '8a':
|
|
tr[h] = require('./data/x8a');
|
|
break;
|
|
case '8b':
|
|
tr[h] = require('./data/x8b');
|
|
break;
|
|
case '8c':
|
|
tr[h] = require('./data/x8c');
|
|
break;
|
|
case '8d':
|
|
tr[h] = require('./data/x8d');
|
|
break;
|
|
case '8e':
|
|
tr[h] = require('./data/x8e');
|
|
break;
|
|
case '8f':
|
|
tr[h] = require('./data/x8f');
|
|
break;
|
|
case '90':
|
|
tr[h] = require('./data/x90');
|
|
break;
|
|
case '91':
|
|
tr[h] = require('./data/x91');
|
|
break;
|
|
case '92':
|
|
tr[h] = require('./data/x92');
|
|
break;
|
|
case '93':
|
|
tr[h] = require('./data/x93');
|
|
break;
|
|
case '94':
|
|
tr[h] = require('./data/x94');
|
|
break;
|
|
case '95':
|
|
tr[h] = require('./data/x95');
|
|
break;
|
|
case '96':
|
|
tr[h] = require('./data/x96');
|
|
break;
|
|
case '97':
|
|
tr[h] = require('./data/x97');
|
|
break;
|
|
case '98':
|
|
tr[h] = require('./data/x98');
|
|
break;
|
|
case '99':
|
|
tr[h] = require('./data/x99');
|
|
break;
|
|
case '9a':
|
|
tr[h] = require('./data/x9a');
|
|
break;
|
|
case '9b':
|
|
tr[h] = require('./data/x9b');
|
|
break;
|
|
case '9c':
|
|
tr[h] = require('./data/x9c');
|
|
break;
|
|
case '9d':
|
|
tr[h] = require('./data/x9d');
|
|
break;
|
|
case '9e':
|
|
tr[h] = require('./data/x9e');
|
|
break;
|
|
case '9f':
|
|
tr[h] = require('./data/x9f');
|
|
break;
|
|
case 'a0':
|
|
tr[h] = require('./data/xa0');
|
|
break;
|
|
case 'a1':
|
|
tr[h] = require('./data/xa1');
|
|
break;
|
|
case 'a2':
|
|
tr[h] = require('./data/xa2');
|
|
break;
|
|
case 'a3':
|
|
tr[h] = require('./data/xa3');
|
|
break;
|
|
case 'a4':
|
|
tr[h] = require('./data/xa4');
|
|
break;
|
|
case 'ac':
|
|
tr[h] = require('./data/xac');
|
|
break;
|
|
case 'ad':
|
|
tr[h] = require('./data/xad');
|
|
break;
|
|
case 'ae':
|
|
tr[h] = require('./data/xae');
|
|
break;
|
|
case 'af':
|
|
tr[h] = require('./data/xaf');
|
|
break;
|
|
case 'b0':
|
|
tr[h] = require('./data/xb0');
|
|
break;
|
|
case 'b1':
|
|
tr[h] = require('./data/xb1');
|
|
break;
|
|
case 'b2':
|
|
tr[h] = require('./data/xb2');
|
|
break;
|
|
case 'b3':
|
|
tr[h] = require('./data/xb3');
|
|
break;
|
|
case 'b4':
|
|
tr[h] = require('./data/xb4');
|
|
break;
|
|
case 'b5':
|
|
tr[h] = require('./data/xb5');
|
|
break;
|
|
case 'b6':
|
|
tr[h] = require('./data/xb6');
|
|
break;
|
|
case 'b7':
|
|
tr[h] = require('./data/xb7');
|
|
break;
|
|
case 'b8':
|
|
tr[h] = require('./data/xb8');
|
|
break;
|
|
case 'b9':
|
|
tr[h] = require('./data/xb9');
|
|
break;
|
|
case 'ba':
|
|
tr[h] = require('./data/xba');
|
|
break;
|
|
case 'bb':
|
|
tr[h] = require('./data/xbb');
|
|
break;
|
|
case 'bc':
|
|
tr[h] = require('./data/xbc');
|
|
break;
|
|
case 'bd':
|
|
tr[h] = require('./data/xbd');
|
|
break;
|
|
case 'be':
|
|
tr[h] = require('./data/xbe');
|
|
break;
|
|
case 'bf':
|
|
tr[h] = require('./data/xbf');
|
|
break;
|
|
case 'c0':
|
|
tr[h] = require('./data/xc0');
|
|
break;
|
|
case 'c1':
|
|
tr[h] = require('./data/xc1');
|
|
break;
|
|
case 'c2':
|
|
tr[h] = require('./data/xc2');
|
|
break;
|
|
case 'c3':
|
|
tr[h] = require('./data/xc3');
|
|
break;
|
|
case 'c4':
|
|
tr[h] = require('./data/xc4');
|
|
break;
|
|
case 'c5':
|
|
tr[h] = require('./data/xc5');
|
|
break;
|
|
case 'c6':
|
|
tr[h] = require('./data/xc6');
|
|
break;
|
|
case 'c7':
|
|
tr[h] = require('./data/xc7');
|
|
break;
|
|
case 'c8':
|
|
tr[h] = require('./data/xc8');
|
|
break;
|
|
case 'c9':
|
|
tr[h] = require('./data/xc9');
|
|
break;
|
|
case 'ca':
|
|
tr[h] = require('./data/xca');
|
|
break;
|
|
case 'cb':
|
|
tr[h] = require('./data/xcb');
|
|
break;
|
|
case 'cc':
|
|
tr[h] = require('./data/xcc');
|
|
break;
|
|
case 'cd':
|
|
tr[h] = require('./data/xcd');
|
|
break;
|
|
case 'ce':
|
|
tr[h] = require('./data/xce');
|
|
break;
|
|
case 'cf':
|
|
tr[h] = require('./data/xcf');
|
|
break;
|
|
case 'd0':
|
|
tr[h] = require('./data/xd0');
|
|
break;
|
|
case 'd1':
|
|
tr[h] = require('./data/xd1');
|
|
break;
|
|
case 'd2':
|
|
tr[h] = require('./data/xd2');
|
|
break;
|
|
case 'd3':
|
|
tr[h] = require('./data/xd3');
|
|
break;
|
|
case 'd4':
|
|
tr[h] = require('./data/xd4');
|
|
break;
|
|
case 'd5':
|
|
tr[h] = require('./data/xd5');
|
|
break;
|
|
case 'd6':
|
|
tr[h] = require('./data/xd6');
|
|
break;
|
|
case 'd7':
|
|
tr[h] = require('./data/xd7');
|
|
break;
|
|
case 'f9':
|
|
tr[h] = require('./data/xf9');
|
|
break;
|
|
case 'fa':
|
|
tr[h] = require('./data/xfa');
|
|
break;
|
|
case 'fb':
|
|
tr[h] = require('./data/xfb');
|
|
break;
|
|
case 'fc':
|
|
tr[h] = require('./data/xfc');
|
|
break;
|
|
case 'fd':
|
|
tr[h] = require('./data/xfd');
|
|
break;
|
|
case 'fe':
|
|
tr[h] = require('./data/xfe');
|
|
break;
|
|
case 'ff':
|
|
tr[h] = require('./data/xff');
|
|
break;
|
|
default:
|
|
// console.error("Unidecode file not found for h=", h);
|
|
return '';
|
|
}
|
|
}
|
|
|
|
return tr[h][l];
|
|
}
|
|
}
|
|
|
|
function dec2hex(i) {
|
|
return (i + 0x100).toString(16).substr(-2);
|
|
}
|
|
|
|
function utf8_to_utf16(raw) {
|
|
var b1, b2, b3, b4,
|
|
x, y, z;
|
|
|
|
while (Array.isArray(raw)) raw = raw[0];
|
|
|
|
switch (raw.length) {
|
|
case 1:
|
|
return ord(raw);
|
|
|
|
// http://en.wikipedia.org/wiki/UTF-8
|
|
case 2:
|
|
b1 = ord(raw.substr(0, 1));
|
|
b2 = ord(raw.substr(1, 1));
|
|
|
|
x = ((b1 & 0x03) << 6) | (b2 & 0x3F);
|
|
y = (b1 & 0x1C) >> 2;
|
|
|
|
return (y << 8) | x;
|
|
|
|
case 3:
|
|
b1 = ord(raw.substr(0, 1));
|
|
b2 = ord(raw.substr(1, 1));
|
|
b3 = ord(raw.substr(2, 1));
|
|
|
|
x = ((b2 & 0x03) << 6) | (b3 & 0x3F);
|
|
y = ((b1 & 0x0F) << 4) | ((b2 & 0x3C) >> 2);
|
|
|
|
return (y << 8) | x;
|
|
|
|
default:
|
|
b1 = ord(raw.substr(0, 1));
|
|
b2 = ord(raw.substr(1, 1));
|
|
b3 = ord(raw.substr(2, 1));
|
|
b4 = ord(raw.substr(3, 1));
|
|
|
|
x = ((b3 & 0x03) << 6) | (b4 & 0x3F);
|
|
y = ((b2 & 0x0F) << 4) | ((b3 & 0x3C) >> 2);
|
|
z = ((b1 & 0x07) << 5) | ((b2 & 0x30) >> 4);
|
|
|
|
return (z << 16) | (y << 8) | x;
|
|
}
|
|
}
|
|
|
|
/* From php.js */
|
|
|
|
function ord(string) {
|
|
// Returns the codepoint value of a character
|
|
//
|
|
// version: 1109.2015
|
|
// discuss at: http://phpjs.org/functions/ord
|
|
// + original by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
|
|
// + bugfixed by: Onno Marsman
|
|
// + improved by: Brett Zamir (http://brett-zamir.me)
|
|
// + input by: incidence
|
|
// * example 1: ord('K');
|
|
// * returns 1: 75
|
|
// * example 2: ord('\uD800\uDC00'); // surrogate pair to create a single Unicode character
|
|
// * returns 2: 65536
|
|
var str = string + '',
|
|
code = str.charCodeAt(0);
|
|
if (0xD800 <= code && code <= 0xDBFF) { // High surrogate (could change last hex to 0xDB7F to treat high private surrogates as single characters)
|
|
var hi = code;
|
|
if (str.length === 1) {
|
|
return code; // This is just a high surrogate with no following low surrogate, so we return its value;
|
|
// we could also throw an error as it is not a complete character, but someone may want to know
|
|
}
|
|
var low = str.charCodeAt(1);
|
|
return ((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000;
|
|
}
|
|
if (0xDC00 <= code && code <= 0xDFFF) { // Low surrogate
|
|
return code; // This is just a low surrogate with no preceding high surrogate, so we return its value;
|
|
// we could also throw an error as it is not a complete character, but someone may want to know
|
|
}
|
|
return code;
|
|
}
|
|
|
|
},{"./data/x00":28,"./data/x01":29,"./data/x02":30,"./data/x03":31,"./data/x04":32,"./data/x05":33,"./data/x06":34,"./data/x07":35,"./data/x09":36,"./data/x0a":37,"./data/x0b":38,"./data/x0c":39,"./data/x0d":40,"./data/x0e":41,"./data/x0f":42,"./data/x10":43,"./data/x11":44,"./data/x12":45,"./data/x13":46,"./data/x14":47,"./data/x15":48,"./data/x16":49,"./data/x17":50,"./data/x18":51,"./data/x1e":52,"./data/x1f":53,"./data/x20":54,"./data/x21":55,"./data/x22":56,"./data/x23":57,"./data/x24":58,"./data/x25":59,"./data/x26":60,"./data/x27":61,"./data/x28":62,"./data/x2e":63,"./data/x2f":64,"./data/x30":65,"./data/x31":66,"./data/x32":67,"./data/x33":68,"./data/x4d":69,"./data/x4e":70,"./data/x4f":71,"./data/x50":72,"./data/x51":73,"./data/x52":74,"./data/x53":75,"./data/x54":76,"./data/x55":77,"./data/x56":78,"./data/x57":79,"./data/x58":80,"./data/x59":81,"./data/x5a":82,"./data/x5b":83,"./data/x5c":84,"./data/x5d":85,"./data/x5e":86,"./data/x5f":87,"./data/x60":88,"./data/x61":89,"./data/x62":90,"./data/x63":91,"./data/x64":92,"./data/x65":93,"./data/x66":94,"./data/x67":95,"./data/x68":96,"./data/x69":97,"./data/x6a":98,"./data/x6b":99,"./data/x6c":100,"./data/x6d":101,"./data/x6e":102,"./data/x6f":103,"./data/x70":104,"./data/x71":105,"./data/x72":106,"./data/x73":107,"./data/x74":108,"./data/x75":109,"./data/x76":110,"./data/x77":111,"./data/x78":112,"./data/x79":113,"./data/x7a":114,"./data/x7b":115,"./data/x7c":116,"./data/x7d":117,"./data/x7e":118,"./data/x7f":119,"./data/x80":120,"./data/x81":121,"./data/x82":122,"./data/x83":123,"./data/x84":124,"./data/x85":125,"./data/x86":126,"./data/x87":127,"./data/x88":128,"./data/x89":129,"./data/x8a":130,"./data/x8b":131,"./data/x8c":132,"./data/x8d":133,"./data/x8e":134,"./data/x8f":135,"./data/x90":136,"./data/x91":137,"./data/x92":138,"./data/x93":139,"./data/x94":140,"./data/x95":141,"./data/x96":142,"./data/x97":143,"./data/x98":144,"./data/x99":145,"./data/x9a":146,"./data/x9b":147,"./data/x9c":148,"./data/x9d":149,"./data/x9e":150,"./data/x9f":151,"./data/xa0":152,"./data/xa1":153,"./data/xa2":154,"./data/xa3":155,"./data/xa4":156,"./data/xac":157,"./data/xad":158,"./data/xae":159,"./data/xaf":160,"./data/xb0":161,"./data/xb1":162,"./data/xb2":163,"./data/xb3":164,"./data/xb4":165,"./data/xb5":166,"./data/xb6":167,"./data/xb7":168,"./data/xb8":169,"./data/xb9":170,"./data/xba":171,"./data/xbb":172,"./data/xbc":173,"./data/xbd":174,"./data/xbe":175,"./data/xbf":176,"./data/xc0":177,"./data/xc1":178,"./data/xc2":179,"./data/xc3":180,"./data/xc4":181,"./data/xc5":182,"./data/xc6":183,"./data/xc7":184,"./data/xc8":185,"./data/xc9":186,"./data/xca":187,"./data/xcb":188,"./data/xcc":189,"./data/xcd":190,"./data/xce":191,"./data/xcf":192,"./data/xd0":193,"./data/xd1":194,"./data/xd2":195,"./data/xd3":196,"./data/xd4":197,"./data/xd5":198,"./data/xd6":199,"./data/xd7":200,"./data/xf9":201,"./data/xfa":202,"./data/xfb":203,"./data/xfc":204,"./data/xfd":205,"./data/xfe":206,"./data/xff":207}],209:[function(require,module,exports){
|
|
const { SimpleCache } = require('acebase-core');
|
|
const { AceBase, AceBaseLocalSettings } = require('./acebase-local');
|
|
const { CustomStorageSettings, CustomStorageTransaction, CustomStorageHelpers, ICustomStorageNode, ICustomStorageNodeMetaData } = require('./storage-custom');
|
|
|
|
/**
|
|
* @typedef {Object} IIndexedDBNodeData
|
|
* @property {string} path
|
|
* @property {ICustomStorageNodeMetaData} metadata
|
|
*/
|
|
|
|
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 {
|
|
/**
|
|
* Constructor that is used in browser context
|
|
* @param {string} name database name
|
|
* @param {AceBaseLocalSettings} settings settings
|
|
*/
|
|
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 {string} dbname Name of the database
|
|
* @param {object} [settings] optional settings
|
|
* @param {string} [settings.logLevel='error'] what level to use for logging to the console
|
|
* @param {boolean} [settings.removeVoidProperties=false] Whether to remove undefined property values of objects being stored, instead of throwing an error
|
|
* @param {number} [settings.maxInlineValueSize=50] Maximum size of binary data/strings to store in parent object records. Larger values are stored in their own records. Recommended to keep this at the default setting
|
|
* @param {boolean} [settings.multipleTabs=false] Whether to enable cross-tab synchronization
|
|
* @param {number} [settings.cacheSeconds=60] How many seconds to keep node info in memory, to speed up IndexedDB performance.
|
|
* @param {number} [settings.lockTimeout=120] timeout setting for read and write locks in seconds. Operations taking longer than this will be aborted. Default is 120 seconds.
|
|
* @param {boolean} [settings.sponsor=false] You can turn this on if you are a sponsor
|
|
*/
|
|
static WithIndexedDB(dbname, settings) {
|
|
|
|
settings = settings || {};
|
|
if (!settings.logLevel) { settings.logLevel = 'error'; }
|
|
|
|
// We'll create an IndexedDB with name "dbname.acebase"
|
|
const IndexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB; // browser prefixes not really needed, see https://caniuse.com/#feat=indexeddb
|
|
let request = IndexedDB.open(`${dbname}.acebase`, 1);
|
|
|
|
let readyResolve, readyReject, readyPromise = new Promise((rs,rj) => { readyResolve = rs; readyReject = rj; });
|
|
|
|
request.onupgradeneeded = (e) => {
|
|
// create datastore
|
|
let 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 db;
|
|
request.onsuccess = e => {
|
|
db = request.result;
|
|
readyResolve();
|
|
};
|
|
request.onerror = e => {
|
|
readyReject(e);
|
|
};
|
|
|
|
const cache = new SimpleCache(typeof settings.cacheSeconds === 'number' ? settings.cacheSeconds : 60); // 60 second node cache by default
|
|
// cache.enabled = false;
|
|
|
|
const storageSettings = new CustomStorageSettings({
|
|
name: 'IndexedDB',
|
|
locking: true, // IndexedDB transactions are short-lived, so we'll use AceBase's path based locking
|
|
removeVoidProperties: settings.removeVoidProperties,
|
|
maxInlineValueSize: settings.maxInlineValueSize,
|
|
lockTimeout: settings.lockTimeout,
|
|
ready() {
|
|
return readyPromise;
|
|
},
|
|
async getTransaction(target) {
|
|
await readyPromise;
|
|
const context = {
|
|
debug: false,
|
|
db,
|
|
cache,
|
|
ipc
|
|
}
|
|
return new IndexedDBStorageTransaction(context, target);
|
|
}
|
|
});
|
|
const acebase = new BrowserAceBase(dbname, { multipleTabs: settings.multipleTabs, logLevel: settings.logLevel, storage: storageSettings, sponsor: settings.sponsor });
|
|
const ipc = acebase.api.storage.ipc;
|
|
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 (let path of message.paths) {
|
|
cache.remove(path);
|
|
}
|
|
}
|
|
});
|
|
return acebase;
|
|
}
|
|
}
|
|
|
|
function _requestToPromise(request) {
|
|
return new Promise((resolve, reject) => {
|
|
request.onsuccess = event => {
|
|
return resolve(request.result || null);
|
|
}
|
|
request.onerror = reject;
|
|
});
|
|
}
|
|
|
|
class IndexedDBStorageTransaction extends 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.
|
|
* @param {{debug: boolean, db: IDBDatabase, cache: SimpleCache<string, ICustomStorageNode> }} context
|
|
* @param {{path: string, write: boolean}} target
|
|
*/
|
|
constructor(context, target) {
|
|
super(target);
|
|
this.production = true; // Improves performance, only set when all works well
|
|
/** @type {{debug: boolean, db: IDBDatabase, cache: SimpleCache<string, ICustomStorageNode> }} */
|
|
this.context = context;
|
|
this._pending = [];
|
|
}
|
|
|
|
/** @returns {IDBTransaction} */
|
|
_createTransaction(write = false) {
|
|
const tx = this.context.db.transaction(['nodes', 'content'], write ? 'readwrite' : 'readonly');
|
|
return tx;
|
|
}
|
|
|
|
_splitMetadata(node) {
|
|
/** @type {ICustomStorageNode} */
|
|
const copy = {};
|
|
const value = node.value;
|
|
Object.assign(copy, node);
|
|
delete copy.value;
|
|
/** @type {ICustomStorageNodeMetaData} */
|
|
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.sendMessage({ type: 'notification', data: { 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);
|
|
/** @type {IIndexedDBNodeData} */
|
|
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;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
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();
|
|
/** @type {IIndexedDBNodeData} */
|
|
const info = results[0];
|
|
if (!info) {
|
|
// Node doesn't exist
|
|
this.context.cache.set(path, null);
|
|
return null;
|
|
}
|
|
/** @type {ICustomStorageNode} */
|
|
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 });
|
|
}
|
|
|
|
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}" ****`);
|
|
include.descendants = false;
|
|
return this._getChildrenOf(path, include, checkCallback, addCallback);
|
|
}
|
|
|
|
descendantsOf(path, include, checkCallback, addCallback) {
|
|
// console.log(`*** descendantsOf "${path}" ****`);
|
|
include.descendants = true;
|
|
return this._getChildrenOf(path, include, checkCallback, addCallback);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {string} path
|
|
* @param {object} include
|
|
* @param {boolean} include.descendants
|
|
* @param {boolean} include.metadata
|
|
* @param {boolean} include.value
|
|
* @param {(path: string, metadata?: ICustomStorageNodeMetaData) => boolean} checkCallback
|
|
* @param {(path: string, node: any) => boolean} addCallback
|
|
*/
|
|
_getChildrenOf(path, include, checkCallback, addCallback) {
|
|
// Use cursor to loop from path on
|
|
return new Promise((resolve, reject) => {
|
|
const pathInfo = CustomStorageHelpers.PathInfo.get(path);
|
|
const tx = this._createTransaction(false);
|
|
const store = tx.objectStore('nodes');
|
|
const query = IDBKeyRange.lowerBound(path, true);
|
|
/** @type {IDBRequest<IDBCursorWithValue>|IDBRequest<IDBCursor>} */
|
|
const cursor = include.metadata ? store.openCursor(query) : store.openKeyCursor(query);
|
|
cursor.onerror = e => {
|
|
tx.abort && tx.abort();
|
|
reject(e);
|
|
}
|
|
cursor.onsuccess = async e => {
|
|
/** @type {string} */
|
|
const otherPath = cursor.result ? cursor.result.key : null;
|
|
let keepGoing = true;
|
|
if (otherPath === null) {
|
|
// No more results
|
|
keepGoing = false;
|
|
}
|
|
else if (!pathInfo.isAncestorOf(otherPath)) {
|
|
// Paths are sorted, no more children or ancestors to be expected!
|
|
keepGoing = false;
|
|
}
|
|
else if (include.descendants || pathInfo.isParentOf(otherPath)) {
|
|
|
|
/** @type {ICustomStorageNode|ICustomStorageNodeMetaData} */
|
|
let node;
|
|
if (include.metadata) {
|
|
/** @type {IDBRequest<IDBCursorWithValue>} */
|
|
const valueCursor = cursor;
|
|
/** @type {IIndexedDBNodeData} */
|
|
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) {
|
|
tx.commit && tx.commit();
|
|
resolve();
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
}
|
|
|
|
module.exports = { BrowserAceBase };
|
|
},{"./acebase-local":210,"./storage-custom":250,"acebase-core":12}],210:[function(require,module,exports){
|
|
const { AceBaseBase, AceBaseBaseSettings } = require('acebase-core');
|
|
const { LocalApi } = require('./api-local');
|
|
const { CustomStorageSettings, CustomStorageTransaction, CustomStorageHelpers } = require('./storage-custom');
|
|
|
|
class AceBaseLocalSettings extends AceBaseBaseSettings {
|
|
/**
|
|
*
|
|
* @param {AceBaseBaseSettings & { storage: import('./storage').StorageSettings, ipc: import('./storage').IPCClientSettings, transactions: import('..').TransactionLogSettings }} options
|
|
*/
|
|
constructor(options) {
|
|
super(options);
|
|
if (!options) { options = {}; }
|
|
this.storage = options.storage || {};
|
|
|
|
// Copy IPC and transaction settings to storage settings
|
|
if (typeof options.ipc === 'object') {
|
|
this.storage.ipc = options.ipc;
|
|
}
|
|
if (typeof options.transactions === 'object') {
|
|
this.storage.transactions = options.transactions;
|
|
}
|
|
}
|
|
}
|
|
|
|
class AceBase extends AceBaseBase {
|
|
|
|
/**
|
|
*
|
|
* @param {string} dbname Name of the database to open or create
|
|
* @param {AceBaseLocalSettings} options
|
|
*/
|
|
constructor(dbname, options) {
|
|
options = new AceBaseLocalSettings(options);
|
|
options.info = options.info || 'realtime database';
|
|
super(dbname, options);
|
|
const apiSettings = {
|
|
db: this,
|
|
storage: options.storage,
|
|
logLevel: options.logLevel
|
|
};
|
|
this.api = new LocalApi(dbname, apiSettings, () => {
|
|
this.emit("ready");
|
|
});
|
|
}
|
|
|
|
close() {
|
|
// Close the database by calling exit on the ipc channel, which will emit an 'exit' event when the database can be safely closed.
|
|
return 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; }
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 {string} dbname Name of the database
|
|
* @param {object} [settings] optional settings
|
|
* @param {string} [settings.logLevel] what level to use for logging to the console
|
|
* @param {boolean} [settings.temp] whether to use sessionStorage instead of localStorage
|
|
* @param {any} [settings.provider] Alternate localStorage provider for running in non-browser environments. Eg using 'node-localstorage'
|
|
* @param {boolean} [settings.removeVoidProperties=false] Whether to remove undefined property values of objects being stored, instead of throwing an error
|
|
* @param {number} [settings.maxInlineValueSize=50] Maximum size of binary data/strings to store in parent object records. Larger values are stored in their own records. Recommended to keep this at the default setting
|
|
* @param {boolean} [settings.multipleTabs=false] Whether to enable cross-tab synchronization
|
|
* @param {boolean} [settings.sponsor=false] You can turn this on if you are a sponsor
|
|
*/
|
|
static WithLocalStorage(dbname, settings) {
|
|
|
|
settings = settings || {};
|
|
if (!settings.logLevel) { settings.logLevel = 'error'; }
|
|
|
|
// Determine whether to use localStorage or sessionStorage
|
|
const localStorage = settings.provider ? settings.provider : settings.temp ? window.localStorage : window.sessionStorage;
|
|
|
|
// Setup our CustomStorageSettings
|
|
const storageSettings = new CustomStorageSettings({
|
|
name: 'LocalStorage',
|
|
locking: true,
|
|
removeVoidProperties: settings.removeVoidProperties,
|
|
maxInlineValueSize: settings.maxInlineValueSize,
|
|
ready() {
|
|
// LocalStorage is always ready
|
|
return Promise.resolve();
|
|
},
|
|
getTransaction(target) {
|
|
// Create an instance of our transaction class
|
|
const context = {
|
|
debug: true,
|
|
dbname,
|
|
localStorage
|
|
}
|
|
const transaction = new LocalStorageTransaction(context, target);
|
|
return Promise.resolve(transaction);
|
|
}
|
|
});
|
|
const db = new AceBase(dbname, { logLevel: settings.logLevel, storage: storageSettings, sponsor: settings.sponsor });
|
|
db.settings.ipcEvents = settings.multipleTabs === true;
|
|
|
|
return db;
|
|
}
|
|
|
|
}
|
|
|
|
// Setup CustomStorageTransaction for browser's LocalStorage
|
|
class LocalStorageTransaction extends CustomStorageTransaction {
|
|
|
|
/**
|
|
* @param {{debug: boolean, dbname: string, localStorage: typeof window.localStorage}} context
|
|
* @param {{path: string, write: boolean}} target
|
|
*/
|
|
constructor(context, target) {
|
|
super(target);
|
|
this.context = context;
|
|
this._storageKeysPrefix = `${this.context.dbname}.acebase::`;
|
|
}
|
|
|
|
async commit() {
|
|
// All changes have already been committed. TODO: use same approach as IndexedDB
|
|
}
|
|
|
|
async rollback(err) {
|
|
// Not able to rollback changes, because we did not keep track
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
async set(path, val) {
|
|
// Sets value in localStorage, wrapped in Promise
|
|
const json = JSON.stringify(val);
|
|
this.context.localStorage.setItem(this.getStorageKeyForPath(path), json);
|
|
}
|
|
|
|
async remove(path) {
|
|
// Removes a value from localStorage, wrapped in Promise
|
|
this.context.localStorage.removeItem(this.getStorageKeyForPath(path));
|
|
}
|
|
|
|
async childrenOf(path, include, checkCallback, addCallback) {
|
|
// Streams all child paths
|
|
// Cannot query localStorage, so loop through all stored keys to find children
|
|
const pathInfo = CustomStorageHelpers.PathInfo.get(path);
|
|
for (let i = 0; i < this.context.localStorage.length; i++) {
|
|
const key = this.context.localStorage.key(i);
|
|
if (!key.startsWith(this._storageKeysPrefix)) { continue; }
|
|
let otherPath = this.getPathFromStorageKey(key);
|
|
if (pathInfo.isParentOf(otherPath) && checkCallback(otherPath)) {
|
|
let node;
|
|
if (include.metadata || include.value) {
|
|
const json = this.context.localStorage.getItem(key);
|
|
node = JSON.parse(json);
|
|
}
|
|
const keepGoing = addCallback(otherPath, node);
|
|
if (!keepGoing) { break; }
|
|
}
|
|
}
|
|
}
|
|
|
|
async descendantsOf(path, include, checkCallback, addCallback) {
|
|
// Streams all descendant paths
|
|
// Cannot query localStorage, so loop through all stored keys to find descendants
|
|
const pathInfo = CustomStorageHelpers.PathInfo.get(path);
|
|
for (let i = 0; i < this.context.localStorage.length; i++) {
|
|
const key = this.context.localStorage.key(i);
|
|
if (!key.startsWith(this._storageKeysPrefix)) { continue; }
|
|
let otherPath = this.getPathFromStorageKey(key);
|
|
if (pathInfo.isAncestorOf(otherPath) && checkCallback(otherPath)) {
|
|
let node;
|
|
if (include.metadata || include.value) {
|
|
const json = this.context.localStorage.getItem(key);
|
|
node = JSON.parse(json);
|
|
}
|
|
const keepGoing = addCallback(otherPath, node);
|
|
if (!keepGoing) { break; }
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper function to get the path from a localStorage key
|
|
* @param {string} key
|
|
*/
|
|
getPathFromStorageKey(key) {
|
|
return key.slice(this._storageKeysPrefix.length);
|
|
}
|
|
|
|
/**
|
|
* Helper function to get the localStorage key for a path
|
|
* @param {string} path
|
|
*/
|
|
getStorageKeyForPath(path) {
|
|
return `${this._storageKeysPrefix}${path}`;
|
|
}
|
|
}
|
|
|
|
module.exports = { AceBase, AceBaseLocalSettings };
|
|
},{"./api-local":211,"./storage-custom":250,"acebase-core":12}],211:[function(require,module,exports){
|
|
const { Api, ID, PathInfo } = require('acebase-core');
|
|
const { StorageSettings, NodeNotFoundError } = require('./storage');
|
|
const { AceBaseStorage, AceBaseStorageSettings } = require('./storage-acebase');
|
|
const { SQLiteStorage, SQLiteStorageSettings } = require('./storage-sqlite');
|
|
const { MSSQLStorage, MSSQLStorageSettings } = require('./storage-mssql');
|
|
const { CustomStorage, CustomStorageSettings } = require('./storage-custom');
|
|
const { VALUE_TYPES } = require('./node-value-types');
|
|
const { DataIndex } = require('./data-index');
|
|
const { query: executeQuery } = require('./query');
|
|
|
|
class LocalApi extends Api {
|
|
// All api methods for local database instance
|
|
|
|
/**
|
|
*
|
|
* @param {{db: AceBase, storage: StorageSettings, logLevel?: string }} settings
|
|
*/
|
|
constructor(dbname = "default", settings, readyCallback) {
|
|
super();
|
|
this.db = settings.db;
|
|
|
|
if (typeof settings.storage === 'object') {
|
|
settings.storage.logLevel = settings.logLevel;
|
|
if (SQLiteStorageSettings && (settings.storage instanceof SQLiteStorageSettings || settings.storage.type === 'sqlite')) {
|
|
this.storage = new SQLiteStorage(dbname, settings.storage);
|
|
}
|
|
else if (MSSQLStorageSettings && (settings.storage instanceof MSSQLStorageSettings || settings.storage.type === 'mssql')) {
|
|
this.storage = new MSSQLStorage(dbname, settings.storage);
|
|
}
|
|
else if (CustomStorageSettings && (settings.storage instanceof CustomStorageSettings || settings.storage.type === 'custom')) {
|
|
this.storage = new CustomStorage(dbname, settings.storage);
|
|
}
|
|
else {
|
|
const storageSettings = settings.storage instanceof AceBaseStorageSettings
|
|
? settings.storage
|
|
: new AceBaseStorageSettings(settings.storage);
|
|
this.storage = new AceBaseStorage(dbname, storageSettings);
|
|
}
|
|
}
|
|
else {
|
|
settings.storage = new AceBaseStorageSettings({ logLevel: settings.logLevel });
|
|
this.storage = new AceBaseStorage(dbname, settings.storage);
|
|
}
|
|
this.storage.on("ready", readyCallback);
|
|
}
|
|
|
|
stats(options) {
|
|
return Promise.resolve(this.storage.stats);
|
|
}
|
|
|
|
subscribe(path, event, callback) {
|
|
this.storage.subscriptions.add(path, event, callback);
|
|
}
|
|
|
|
unsubscribe(path, event = undefined, callback = undefined) {
|
|
this.storage.subscriptions.remove(path, event, callback);
|
|
}
|
|
|
|
/**
|
|
* Creates a new node or overwrites an existing node
|
|
* @param {Storage} storage
|
|
* @param {string} path
|
|
* @param {any} value Any value will do. If the value is small enough to be stored in a parent record, it will take care of it
|
|
* @param {object} [options]
|
|
* @param {boolean} [options.suppress_events=false] whether to suppress the execution of event subscriptions
|
|
* @param {any} [options.context=null] Context to be passed along with data events
|
|
* @returns {Promise<{ cursor?: string }>} 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 { cursor };
|
|
}
|
|
|
|
/**
|
|
* Updates an existing node, or creates a new node.
|
|
* @param {Storage} storage
|
|
* @param {string} path
|
|
* @param {any} updates
|
|
* @param {object} [options]
|
|
* @param {boolean} [options.suppress_events=false] whether to suppress the execution of event subscriptions
|
|
* @param {any} [options.context=null] Context to be passed along with data events
|
|
* @returns {Promise<{ cursor?: string }>} returns a promise with the new cursor (if transaction logging is enabled)
|
|
*/
|
|
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 { cursor };
|
|
}
|
|
|
|
get transactionLoggingEnabled() {
|
|
return this.storage.settings.transactions && this.storage.settings.transactions.log === true;
|
|
}
|
|
|
|
/**
|
|
* Gets the value of a node
|
|
* @param {Storage} storage
|
|
* @param {string} path
|
|
* @param {object} [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 {string[]} [options.include] keys to include
|
|
* @param {string[]} [options.exclude] keys to exclude
|
|
* @param {boolean} [options.child_objects=true] whether to include child objects
|
|
* @returns {Promise<{ value: any, context: any, cursor?: string }>}
|
|
*/
|
|
async get(path, options) {
|
|
// const context = {};
|
|
// if (this.transactionLoggingEnabled) {
|
|
// context.acebase_cursor = ID.generate();
|
|
// }
|
|
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 };
|
|
}
|
|
|
|
/**
|
|
* Performs a transaction on a Node
|
|
* @param {Storage} storage
|
|
* @param {string} path
|
|
* @param {(currentValue: any) => Promise<any>} 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.
|
|
* @param {any} [options]
|
|
* @param {boolean} [options.suppress_events=false] whether to suppress the execution of event subscriptions
|
|
* @param {any} [options.context=null]
|
|
* @returns {Promise<{ cursor?: string }>} 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 { 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!
|
|
// */
|
|
// }
|
|
|
|
/**
|
|
*
|
|
* @param {string} path
|
|
* @param {object} query
|
|
* @param {Array<{ key: string, op: string, compare: any}>} query.filters
|
|
* @param {number} query.skip number of results to skip, useful for paging
|
|
* @param {number} query.take max number of results to return
|
|
* @param {Array<{ key: string, ascending: boolean }>} query.order
|
|
* @param {object} [options]
|
|
* @param {boolean} [options.snapshots=false] whether to return matching data, or paths to matching nodes only
|
|
* @param {string[]} [options.include] when using snapshots, keys or relative paths to include in result data
|
|
* @param {string[]} [options.exclude] when using snapshots, keys or relative paths to exclude from result data
|
|
* @param {boolean} [options.child_objects] when using snapshots, whether to include child objects in result data
|
|
* @param {(event: { name: string, [key: string]: any }) => void} [options.eventHandler]
|
|
* @param {object} [options.monitor] NEW (BETA) monitor changes
|
|
* @param {boolean} [options.monitor.add=false] monitor new matches (either because they were added, or changed and now match the query)
|
|
* @param {boolean} [options.monitor.change=false] monitor changed children that still match this query
|
|
* @param {boolean} [options.monitor.remove=false] monitor children that don't match this query anymore
|
|
* @returns {Promise<{ results: object[]|string[], context: any, stop(): Promise<void> }>} returns a promise that resolves with matching data or paths in `results`
|
|
*/
|
|
query(path, query, options = { snapshots: false, include: undefined, exclude: undefined, child_objects: undefined, eventHandler: event => {} }) {
|
|
return executeQuery(this, path, query, options);
|
|
}
|
|
|
|
/**
|
|
* Creates an index on key for all child nodes at path
|
|
* @param {string} path
|
|
* @param {string} key
|
|
* @param {object} [options]
|
|
* @returns {Promise<DataIndex>}
|
|
*/
|
|
createIndex(path, key, options) {
|
|
return this.storage.indexes.create(path, key, options);
|
|
}
|
|
|
|
/**
|
|
* Gets all indexes
|
|
* @returns {Promise<DataIndex[]>}
|
|
*/
|
|
getIndexes() {
|
|
return Promise.resolve(this.storage.indexes.list());
|
|
}
|
|
|
|
/**
|
|
* Deletes an existing index from the database
|
|
* @param {string} filePath
|
|
* @returns
|
|
*/
|
|
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); }
|
|
if (['null','undefined'].includes(from)) { from = null; }
|
|
const children = [];
|
|
let n = 0, stop = false, more = false; //stop = skip + limit,
|
|
await this.storage.getChildren(path)
|
|
.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;
|
|
if (include) {
|
|
children.push({
|
|
key: typeof childInfo.key === 'string' ? childInfo.key : childInfo.index,
|
|
type: childInfo.valueTypeName,
|
|
value: childInfo.value,
|
|
// address is now only added when storage is acebase. Not when eg sqlite, mssql
|
|
address: typeof childInfo.address === 'object' && 'pageNr' in childInfo.address ? { pageNr: childInfo.address.pageNr, recordNr: childInfo.address.recordNr } : undefined
|
|
});
|
|
}
|
|
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..
|
|
});
|
|
return {
|
|
more,
|
|
list: children
|
|
};
|
|
}
|
|
switch(type) {
|
|
case "children": {
|
|
return getChildren(path, args.limit, args.skip, args.from);
|
|
}
|
|
case "info": {
|
|
const info = {
|
|
key: '',
|
|
exists: false,
|
|
type: 'unknown',
|
|
value: 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;
|
|
let isObjectOrArray = nodeInfo.exists && nodeInfo.address && [VALUE_TYPES.OBJECT, 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
export(path, stream, options = { format: 'json' }) {
|
|
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) {
|
|
return this.storage.setSchema(path, schema);
|
|
}
|
|
|
|
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
|
|
* @param {object} filter
|
|
* @param {string} [filter.path] path to get all mutations for, only used if `for` property isn't used
|
|
* @param {Array<{ path: string, events: string[] }>} [filter.for] paths and events to get relevant mutations for
|
|
* @param {string} filter.cursor cursor to use
|
|
* @param {number} filter.timestamp timestamp to use
|
|
* @returns {Promise<{ used_cursor: string, new_cursor: string, mutations: object[] }>}
|
|
*/
|
|
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
|
|
* @param {object} filter
|
|
* @param {string} [filter.path] path to get all mutations for, only used if `for` property isn't used
|
|
* @param {Array<{ path: string, events: string[] }>} [filter.for] paths and events to get relevant mutations for
|
|
* @param {string} filter.cursor cursor to use
|
|
* @param {number} filter.timestamp timestamp to use
|
|
* @returns {Promise<{ used_cursor: string, new_cursor: string, changes: object[] }>}
|
|
*/
|
|
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);
|
|
}
|
|
}
|
|
|
|
module.exports = { LocalApi };
|
|
},{"./data-index":237,"./node-value-types":244,"./query":248,"./storage":252,"./storage-acebase":246,"./storage-custom":250,"./storage-mssql":246,"./storage-sqlite":246,"acebase-core":12}],212:[function(require,module,exports){
|
|
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.Uint8ArrayBuilder = exports.readSignedOffset = exports.writeSignedOffset = exports.readSignedNumber = exports.writeSignedNumber = exports.readByteLength = exports.writeByteLength = void 0;
|
|
function writeByteLength(bytes, index, length) {
|
|
bytes[index] = (length >> 24) & 0xff;
|
|
bytes[index + 1] = (length >> 16) & 0xff;
|
|
bytes[index + 2] = (length >> 8) & 0xff;
|
|
bytes[index + 3] = length & 0xff;
|
|
return bytes;
|
|
}
|
|
exports.writeByteLength = writeByteLength;
|
|
function readByteLength(bytes, index) {
|
|
const length = (bytes[index] << 24)
|
|
| (bytes[index + 1] << 16)
|
|
| (bytes[index + 2] << 8)
|
|
| bytes[index + 3];
|
|
return length;
|
|
}
|
|
exports.readByteLength = readByteLength;
|
|
const MAX_SIGNED_NUMBER = Math.pow(2, 31) - 1;
|
|
function writeSignedNumber(bytes, index, offset) {
|
|
const negative = offset < 0;
|
|
if (negative) {
|
|
offset = -offset;
|
|
}
|
|
if (offset > MAX_SIGNED_NUMBER) {
|
|
throw new Error(`reference offset to big to store in 31 bits`);
|
|
}
|
|
bytes[index] = ((offset >> 24) & 0x7f) | (negative ? 0x80 : 0);
|
|
bytes[index + 1] = (offset >> 16) & 0xff;
|
|
bytes[index + 2] = (offset >> 8) & 0xff;
|
|
bytes[index + 3] = offset & 0xff;
|
|
return bytes;
|
|
}
|
|
exports.writeSignedNumber = writeSignedNumber;
|
|
function readSignedNumber(bytes, index) {
|
|
let nr = ((bytes[index] & 0x7f) << 24) | (bytes[index + 1] << 16) | (bytes[index + 2] << 8) | bytes[index + 3];
|
|
const isNegative = (bytes[index] & 0x80) > 0;
|
|
if (isNegative) {
|
|
nr = -nr;
|
|
}
|
|
return nr;
|
|
}
|
|
exports.readSignedNumber = readSignedNumber;
|
|
const MAX_SIGNED_OFFSET = Math.pow(2, 47) - 1;
|
|
// input: 2315765760
|
|
// expected output: [0, 0, 138, 7, 200, 0]
|
|
function writeSignedOffset(bytes, index, offset, large = false) {
|
|
if (!large) {
|
|
// throw new Error('DEV: write large offsets only! (remove error later when successfully implemented)');
|
|
return writeSignedNumber(bytes, index, offset);
|
|
}
|
|
const negative = offset < 0;
|
|
if (negative) {
|
|
offset = -offset;
|
|
}
|
|
if (offset > MAX_SIGNED_OFFSET) {
|
|
throw new Error(`reference offset to big to store in 47 bits`);
|
|
}
|
|
// Bitwise operations in javascript are 32 bits, so they cannot be used on larger numbers
|
|
// Split the large number into 6 8-bit numbers by division instead
|
|
let n = offset;
|
|
for (let i = 0; i < 6; i++) {
|
|
const b = n & 0xff;
|
|
bytes[index + 5 - i] = b;
|
|
n = n <= b ? 0 : (n - b) / 256;
|
|
}
|
|
if (negative) {
|
|
bytes[index] |= 0x80;
|
|
}
|
|
return bytes;
|
|
}
|
|
exports.writeSignedOffset = writeSignedOffset;
|
|
function readSignedOffset(bytes, index, large = false) {
|
|
if (!large) {
|
|
// throw new Error('DEV: read large offsets only! (remove error later when successfully implemented)');
|
|
return readSignedNumber(bytes, index);
|
|
}
|
|
let offset = 0;
|
|
const isNegative = (bytes[index] & 0x80) > 0;
|
|
for (let i = 0; i < 6; i++) {
|
|
let b = bytes[index + i];
|
|
if (i === 0 && isNegative) {
|
|
b ^= 0x80;
|
|
}
|
|
offset += b * Math.pow(2, (5 - i) * 8);
|
|
}
|
|
if (isNegative) {
|
|
offset = -offset;
|
|
}
|
|
return offset;
|
|
}
|
|
exports.readSignedOffset = readSignedOffset;
|
|
class Uint8ArrayBuilder {
|
|
// static get blockSize() {
|
|
// return 4096;
|
|
// }
|
|
constructor(bytes = null, bufferSize = 4096) {
|
|
/** @type {Uint8Array} */
|
|
this._data = new Uint8Array();
|
|
this._length = 0;
|
|
this._bufferSize = bufferSize;
|
|
bytes && this.append(bytes);
|
|
}
|
|
// /**
|
|
// * grows the buffer
|
|
// * @param byteCount the amount of bytes
|
|
// */
|
|
// reserve(byteCount: number) {
|
|
// const addBytes = Uint8ArrayBuilder.blockSize * Math.ceil(byteCount / Uint8ArrayBuilder.blockSize);
|
|
// const newLength = this._data.byteLength + addBytes;
|
|
// const newData = new Uint8Array(newLength);
|
|
// newData.set(this._data, 0);
|
|
// this._data = newData;
|
|
// }
|
|
append(bytes) {
|
|
if (bytes instanceof Uint8ArrayBuilder) {
|
|
bytes = bytes.data;
|
|
}
|
|
this.reserve(bytes.length);
|
|
this._data.set(bytes, this._length);
|
|
this._length += bytes.length;
|
|
return this;
|
|
}
|
|
push(...bytes) {
|
|
if (bytes.length === 0) {
|
|
console.warn('WARNING: pushing 0 bytes to Uint8ArrayBuilder!');
|
|
}
|
|
return this.append(bytes);
|
|
}
|
|
static writeUint32(positiveNumber, target, index) {
|
|
if (target) {
|
|
new DataView(target).setUint32(index, positiveNumber, false);
|
|
}
|
|
else {
|
|
const bytes = new Uint8Array(4);
|
|
const view = new DataView(bytes);
|
|
view.setUint32(index, positiveNumber);
|
|
return bytes;
|
|
}
|
|
}
|
|
reserve(length) {
|
|
const freeBytes = this._data.byteLength - this._length;
|
|
if (freeBytes < length) {
|
|
// Won't fit
|
|
const bytesShort = length - freeBytes;
|
|
const addBytes = this._bufferSize * Math.ceil((bytesShort * 1.1) / this._bufferSize);
|
|
const newLength = this._data.byteLength + addBytes;
|
|
// this._data = new Uint8Array(this._data.buffer, 0, newLength);
|
|
const newData = new Uint8Array(newLength);
|
|
newData.set(this._data, 0);
|
|
this._data = newData;
|
|
}
|
|
}
|
|
get dataView() {
|
|
return new DataView(this._data.buffer, this._data.byteOffset, this._data.byteLength);
|
|
}
|
|
write(data, index) {
|
|
if (typeof index !== 'number') {
|
|
throw new Error(`no index passed to write method`);
|
|
}
|
|
const grow = index + data.byteLength - this._length;
|
|
if (grow > 0) {
|
|
this.reserve(grow);
|
|
this._length += grow;
|
|
}
|
|
this._data.set(data, index);
|
|
}
|
|
writeByte(byte, index) {
|
|
if (typeof index !== 'number') {
|
|
// Append
|
|
this.reserve(1);
|
|
index = this._length;
|
|
this._length += 1;
|
|
}
|
|
this.dataView.setUint8(index, byte);
|
|
}
|
|
writeUint16(positiveNumber, index) {
|
|
if (typeof index !== 'number') {
|
|
// Append
|
|
this.reserve(2);
|
|
index = this._length;
|
|
this._length += 2;
|
|
}
|
|
this.dataView.setUint16(index, positiveNumber, false); // Use big-endian, msb first
|
|
}
|
|
writeUint32(positiveNumber, index) {
|
|
if (typeof index !== 'number') {
|
|
// Append
|
|
this.reserve(4);
|
|
index = this._length;
|
|
this._length += 4;
|
|
}
|
|
this.dataView.setUint32(index, positiveNumber, false); // Use big-endian, msb first
|
|
}
|
|
writeUint32_old(positiveNumber, index) {
|
|
const bytes = writeByteLength([], 0, positiveNumber);
|
|
if (index >= 0) {
|
|
this._data.set(bytes, index);
|
|
return this;
|
|
}
|
|
return this.append(bytes);
|
|
}
|
|
writeInt32(signedNumber, index) {
|
|
if (typeof index !== 'number') {
|
|
// Append
|
|
this.reserve(4);
|
|
index = this._length;
|
|
this._length += 4;
|
|
}
|
|
if (signedNumber > MAX_SIGNED_NUMBER || signedNumber < -MAX_SIGNED_NUMBER) {
|
|
throw new Error(`number to big to store in uint32`);
|
|
}
|
|
const negative = signedNumber < 0;
|
|
if (negative) {
|
|
// Old method uses "signed magnitude" method for negative numbers
|
|
// setInt32 uses 2's complement instead. So, for negative numbers we have
|
|
// to do something else to be backward compatible with old code
|
|
const nr = -signedNumber; // Make positive
|
|
const view = this.dataView;
|
|
view.setInt8(index, ((nr >> 24) & 0x7f) | (negative ? 0x80 : 0));
|
|
view.setInt8(index + 1, (nr >> 16) & 0xff);
|
|
view.setInt8(index + 2, (nr >> 8) & 0xff);
|
|
view.setInt8(index + 3, nr & 0xff);
|
|
}
|
|
else {
|
|
this.dataView.setInt32(index, signedNumber, false); // Use big-endian, msb first
|
|
}
|
|
return this;
|
|
}
|
|
writeInt32_old(signedNumber, index) {
|
|
const bytes = writeSignedNumber([], 0, signedNumber);
|
|
if (index >= 0) {
|
|
this._data.set(bytes, index);
|
|
return this;
|
|
}
|
|
return this.append(bytes);
|
|
}
|
|
writeInt48(signedNumber, index) {
|
|
if (typeof index !== 'number') {
|
|
// Append
|
|
this.reserve(6);
|
|
index = this._length;
|
|
this._length += 6;
|
|
}
|
|
if (signedNumber > MAX_SIGNED_OFFSET || signedNumber < -MAX_SIGNED_OFFSET) {
|
|
throw new Error(`number to big to store in int48`);
|
|
}
|
|
const negative = signedNumber < 0;
|
|
// Write ourselves
|
|
let n = negative ? -signedNumber : signedNumber;
|
|
// let view = this.dataView;
|
|
for (let i = 0; i < 6; i++) {
|
|
let b = n & 0xff;
|
|
if (negative && i === 5) {
|
|
b |= 0x80;
|
|
}
|
|
this.data[index + 5 - i] = b; //view.setUint8(index + 5 - i, b);
|
|
n = n <= b ? 0 : (n - b) / 256;
|
|
}
|
|
// else {
|
|
// // No way to write an Uint48 natively, so we'll write a BigInt64 and chop off 2 bytes
|
|
// let uint64 = new Uint8Array(8);
|
|
// new DataView(uint64.buffer).setBigUint64(0, signedNumber, false);
|
|
// this._data.set(uint64.slice(2), index);
|
|
// }
|
|
return this;
|
|
}
|
|
writeInt48_old(signedNumber, index) {
|
|
const bytes = writeSignedOffset([], 0, signedNumber, true);
|
|
if (index >= 0) {
|
|
this._data.set(bytes, index);
|
|
return this;
|
|
}
|
|
return this.append(bytes);
|
|
}
|
|
get data() {
|
|
return this._data.subarray(0, this._length);
|
|
}
|
|
get length() {
|
|
return this._length;
|
|
}
|
|
slice(begin, end) {
|
|
if (begin < 0) {
|
|
return this._data.subarray(this._length + begin, this._length);
|
|
}
|
|
else {
|
|
return this._data.subarray(begin, end || this._length);
|
|
}
|
|
}
|
|
splice(index, remove) {
|
|
if (typeof remove !== 'number') {
|
|
remove = this.length - index;
|
|
}
|
|
const removed = this._data.slice(index, index + remove);
|
|
if (index + remove >= this.length) {
|
|
this._length = index;
|
|
}
|
|
else {
|
|
this._data.copyWithin(index, index + remove, this._length);
|
|
this._length -= remove;
|
|
}
|
|
return removed;
|
|
}
|
|
}
|
|
exports.Uint8ArrayBuilder = Uint8ArrayBuilder;
|
|
|
|
},{}],213:[function(require,module,exports){
|
|
/**
|
|
________________________________________________________________________________
|
|
|
|
___ ______
|
|
/ _ \ | ___ \
|
|
/ /_\ \ ___ ___| |_/ / __ _ ___ ___
|
|
| _ |/ __/ _ \ ___ \/ _` / __|/ _ \
|
|
| | | | (_| __/ |_/ / (_| \__ \ __/
|
|
\_| |_/\___\___\____/ \__,_|___/\___|
|
|
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 { DataReference, DataSnapshot, EventSubscription, PathReference, TypeMappings, ID, proxyAccess } = require('acebase-core');
|
|
const { AceBaseLocalSettings } = require('./acebase-local');
|
|
const { BrowserAceBase } = require('./acebase-browser');
|
|
const { CustomStorageSettings, CustomStorageTransaction, CustomStorageHelpers } = require('./storage-custom');
|
|
|
|
const acebase = {
|
|
AceBase: BrowserAceBase,
|
|
AceBaseLocalSettings,
|
|
DataReference,
|
|
DataSnapshot,
|
|
EventSubscription,
|
|
PathReference,
|
|
TypeMappings,
|
|
CustomStorageSettings,
|
|
CustomStorageTransaction,
|
|
CustomStorageHelpers,
|
|
ID,
|
|
proxyAccess
|
|
};
|
|
|
|
// Expose classes to window.acebase:
|
|
window.acebase = acebase;
|
|
// Expose BrowserAceBase class as window.AceBase:
|
|
window.AceBase = BrowserAceBase;
|
|
// Expose classes for module imports:
|
|
module.exports = acebase;
|
|
},{"./acebase-browser":209,"./acebase-local":210,"./storage-custom":250,"acebase-core":12}],214:[function(require,module,exports){
|
|
(function (Buffer){(function (){
|
|
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.BinaryReader = void 0;
|
|
const acebase_core_1 = require("acebase-core");
|
|
const binary_1 = require("../binary");
|
|
const detailed_error_1 = require("../detailed-error");
|
|
const promise_fs_1 = require("../promise-fs");
|
|
const tree_1 = require("./tree");
|
|
const { bytesToNumber } = acebase_core_1.Utils;
|
|
class BinaryReader {
|
|
/**
|
|
* BinaryReader is a helper class to make reading binary data easier and faster
|
|
* @param file file name, file descriptor, or an open file, or read function that returns a promise
|
|
* @param chunkSize how many bytes per read. default is 4KB
|
|
*/
|
|
constructor(file, chunkSize = 4096) {
|
|
this.chunkSize = chunkSize;
|
|
this.data = null;
|
|
/**
|
|
* offset of loaded data (start index of current chunk in data source)
|
|
*/
|
|
this.offset = 0;
|
|
/**
|
|
* current chunk reading index ("cursor" in currently loaded chunk)
|
|
*/
|
|
this.index = 0;
|
|
this.chunkSize = chunkSize;
|
|
if (typeof file === 'function') {
|
|
// Use the passed function for reads
|
|
this.read = file;
|
|
}
|
|
else {
|
|
let fd;
|
|
if (typeof file === 'number') {
|
|
// Use the passed file descriptor
|
|
fd = file;
|
|
}
|
|
else if (typeof file === 'string') {
|
|
// Read from passed file name
|
|
// Override this.init to open the file first
|
|
const init = this.init.bind(this);
|
|
this.init = async () => {
|
|
fd = await promise_fs_1.pfs.open(file, 'r'); // Open file now
|
|
return init(); // Run original this.init
|
|
};
|
|
this.close = async () => {
|
|
return promise_fs_1.pfs.close(fd);
|
|
};
|
|
}
|
|
else {
|
|
throw new detailed_error_1.DetailedError('invalid-file-argument', 'invalid file argument');
|
|
}
|
|
this.read = async (index, length) => {
|
|
const buffer = Buffer.alloc(length);
|
|
const { bytesRead } = await promise_fs_1.pfs.read(fd, buffer, 0, length, index);
|
|
if (bytesRead < length) {
|
|
return buffer.slice(0, bytesRead);
|
|
}
|
|
return buffer;
|
|
};
|
|
}
|
|
}
|
|
async init() {
|
|
const chunk = await this.read(0, this.chunkSize);
|
|
console.assert(chunk instanceof Buffer, 'read function must return a Buffer');
|
|
this.data = chunk;
|
|
this.offset = 0;
|
|
this.index = 0;
|
|
}
|
|
clone() {
|
|
const clone = Object.assign(new BinaryReader(this.read, this.chunkSize), this);
|
|
clone.offset = 0;
|
|
clone.index = 0;
|
|
clone.data = Buffer.alloc(0);
|
|
return clone;
|
|
}
|
|
async get(byteCount) {
|
|
await this.assert(byteCount);
|
|
// const bytes = this.data.slice(this.index, this.index + byteCount);
|
|
const slice = this.data.slice(this.index, this.index + byteCount); // Buffer.from(this.data.buffer, this.index, byteCount);
|
|
if (slice.byteLength !== byteCount) {
|
|
throw new detailed_error_1.DetailedError('invalid_byte_length', `Expected to read ${byteCount} bytes from tree, got ${slice.byteLength}`);
|
|
}
|
|
this.index += byteCount;
|
|
return slice;
|
|
}
|
|
async getInt32() {
|
|
const buffer = await this.get(4);
|
|
return (0, binary_1.readSignedNumber)(buffer, 0);
|
|
}
|
|
async getUint32() {
|
|
const buffer = await this.get(4);
|
|
return (0, binary_1.readByteLength)(buffer, 0);
|
|
}
|
|
async getValue() {
|
|
const header = await this.get(2);
|
|
const length = header[1];
|
|
await this.seek(-2);
|
|
const buffer = await this.get(length + 2);
|
|
return BinaryReader.readValue(buffer, 0).value;
|
|
// // TODO: Refactor not to convert buffer to array and back to buffer
|
|
// let b = await this.get(2);
|
|
// let bytes = Array.from(b);
|
|
// b = await this.get(bytes[1]);
|
|
// _appendToArray(bytes, Array.from(b));
|
|
// return BinaryReader.readValue(Buffer.from(bytes), 0).value;
|
|
}
|
|
async more(chunks = 1) {
|
|
const length = chunks * this.chunkSize;
|
|
const nextChunk = await this.read(this.offset + this.data.length, length);
|
|
console.assert(nextChunk instanceof Buffer, 'read function must return a Buffer');
|
|
// Let go of old data before current index:
|
|
this.data = this.data.slice(this.index);
|
|
this.offset += this.index;
|
|
this.index = 0;
|
|
// Append new data
|
|
const newData = Buffer.alloc(this.data.length + nextChunk.length);
|
|
newData.set(this.data, 0);
|
|
newData.set(nextChunk, this.data.length);
|
|
this.data = newData;
|
|
}
|
|
async seek(offset) {
|
|
if (this.index + offset < this.data.length) {
|
|
this.index += offset;
|
|
}
|
|
else {
|
|
const dataIndex = this.offset + this.index + offset;
|
|
const newChunk = await this.read(dataIndex, this.chunkSize);
|
|
this.data = newChunk;
|
|
this.offset = dataIndex;
|
|
this.index = 0;
|
|
}
|
|
}
|
|
async assert(byteCount) {
|
|
if (byteCount < 0) {
|
|
throw new detailed_error_1.DetailedError('invalid_byte_count', `Cannot read ${byteCount} bytes from tree`);
|
|
}
|
|
if (this.index + byteCount > this.data.byteLength) {
|
|
await this.more(Math.ceil(byteCount / this.chunkSize));
|
|
if (this.index + byteCount > this.data.byteLength) {
|
|
throw new detailed_error_1.DetailedError('EOF', 'end of file');
|
|
}
|
|
}
|
|
}
|
|
skip(byteCount) {
|
|
this.index += byteCount;
|
|
}
|
|
rewind(byteCount) {
|
|
this.index -= byteCount;
|
|
}
|
|
async go(index) {
|
|
if (this.offset <= index && this.offset + this.data.byteLength > index) {
|
|
this.index = index - this.offset;
|
|
}
|
|
else {
|
|
const chunk = await this.read(index, this.chunkSize);
|
|
this.data = chunk;
|
|
this.offset = index;
|
|
this.index = 0;
|
|
}
|
|
}
|
|
savePosition(offsetCorrection = 0) {
|
|
const savedIndex = this.offset + this.index + offsetCorrection;
|
|
const go = (offset = 0) => {
|
|
const index = savedIndex + offset;
|
|
return this.go(index);
|
|
};
|
|
return {
|
|
go,
|
|
index: savedIndex,
|
|
};
|
|
}
|
|
get sourceIndex() {
|
|
return this.offset + this.index;
|
|
}
|
|
static readValue(buffer, index) {
|
|
const arr = buffer; // Hack, getKeyFromBinary will work with a Buffer too
|
|
const val = tree_1.BPlusTree.getKeyFromBinary(arr, index);
|
|
return { value: val.key, byteLength: val.byteLength };
|
|
}
|
|
static bytesToNumber(buffer) {
|
|
const arr = buffer; // Hack, bytesToNumber will work with a Buffer too
|
|
return bytesToNumber(arr);
|
|
}
|
|
static readUint32(buffer, index) {
|
|
return (0, binary_1.readSignedNumber)(buffer, index);
|
|
}
|
|
static readInt32(buffer, index) {
|
|
return (0, binary_1.readByteLength)(buffer, index);
|
|
}
|
|
}
|
|
exports.BinaryReader = BinaryReader;
|
|
|
|
}).call(this)}).call(this,require("buffer").Buffer)
|
|
},{"../binary":212,"../detailed-error":238,"../promise-fs":247,"./tree":233,"acebase-core":12,"buffer":27}],215:[function(require,module,exports){
|
|
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.BinaryBPlusTreeBuilder = exports.FLAGS = exports.KEY_TYPE = void 0;
|
|
const binary_1 = require("../binary");
|
|
const detailed_error_1 = require("../detailed-error");
|
|
const config_1 = require("./config");
|
|
const tree_1 = require("./tree");
|
|
const acebase_core_1 = require("acebase-core");
|
|
const { bigintToBytes, encodeString, numberToBytes } = acebase_core_1.Utils;
|
|
exports.KEY_TYPE = {
|
|
UNDEFINED: 0,
|
|
STRING: 1,
|
|
NUMBER: 2,
|
|
BOOLEAN: 3,
|
|
DATE: 4,
|
|
BIGINT: 5,
|
|
};
|
|
exports.FLAGS = {
|
|
UNIQUE_KEYS: 1,
|
|
HAS_METADATA: 2,
|
|
HAS_FREE_SPACE: 4,
|
|
HAS_FILL_FACTOR: 8,
|
|
HAS_SMALL_LEAFS: 16,
|
|
HAS_LARGE_PTRS: 32,
|
|
ENTRY_HAS_EXT_DATA: 128,
|
|
IS_LEAF: 1,
|
|
LEAF_HAS_EXT_DATA: 2,
|
|
};
|
|
class BinaryBPlusTreeBuilder {
|
|
constructor(options = { uniqueKeys: true, smallLeafs: config_1.WRITE_SMALL_LEAFS, maxEntriesPerNode: 3, fillFactor: 95, metadataKeys: [], byteLength: 0, freeBytes: 0 }) {
|
|
this.uniqueKeys = options.uniqueKeys;
|
|
this.maxEntriesPerNode = options.maxEntriesPerNode;
|
|
this.metadataKeys = options.metadataKeys;
|
|
this.byteLength = options.byteLength;
|
|
this.freeBytes = options.freeBytes;
|
|
this.smallLeafs = options.smallLeafs;
|
|
this.fillFactor = options.fillFactor;
|
|
}
|
|
getHeader() {
|
|
const indexTypeFlags = (this.uniqueKeys ? exports.FLAGS.UNIQUE_KEYS : 0)
|
|
| (this.metadataKeys.length > 0 ? exports.FLAGS.HAS_METADATA : 0)
|
|
| (this.freeBytes > 0 ? exports.FLAGS.HAS_FREE_SPACE : 0)
|
|
| (typeof this.fillFactor === 'number' && this.fillFactor > 0 && this.fillFactor <= 100 ? exports.FLAGS.HAS_FILL_FACTOR : 0)
|
|
| (this.smallLeafs === true ? exports.FLAGS.HAS_SMALL_LEAFS : 0)
|
|
| exports.FLAGS.HAS_LARGE_PTRS;
|
|
const bytes = [
|
|
// byte_length:
|
|
0, 0, 0, 0,
|
|
// index_type:
|
|
indexTypeFlags,
|
|
// max_node_entries:
|
|
this.maxEntriesPerNode,
|
|
];
|
|
// update byte_length:
|
|
(0, binary_1.writeByteLength)(bytes, 0, this.byteLength);
|
|
if (this.fillFactor > 0 && this.fillFactor <= 100) {
|
|
// fill_factor:
|
|
bytes.push(this.fillFactor);
|
|
}
|
|
if (this.freeBytes > 0) {
|
|
// free_byte_length:
|
|
(0, binary_1.writeByteLength)(bytes, bytes.length, this.freeBytes);
|
|
}
|
|
if (this.metadataKeys.length > 0) {
|
|
// metadata_keys:
|
|
const index = bytes.length;
|
|
bytes.push(0, 0, 0, 0); // metadata_length
|
|
// metadata_key_count:
|
|
bytes.push(this.metadataKeys.length);
|
|
this.metadataKeys.forEach(key => {
|
|
// metadata_key:
|
|
bytes.push(key.length); // metadata_key_length
|
|
// metadata_key_name:
|
|
for (let i = 0; i < key.length; i++) {
|
|
bytes.push(key.charCodeAt(i));
|
|
}
|
|
});
|
|
// update metadata_length:
|
|
const length = bytes.length - index - 4;
|
|
(0, binary_1.writeByteLength)(bytes, index, length);
|
|
}
|
|
return bytes;
|
|
}
|
|
createNode(info, options = { addFreeSpace: true, maxLength: 0 }) {
|
|
console.assert(info.entries.length > 0, 'node has no entries!');
|
|
const bytes = [
|
|
// byte_length:
|
|
0, 0, 0, 0,
|
|
0,
|
|
// free_byte_length:
|
|
0, 0, 0, 0,
|
|
];
|
|
// entries_length:
|
|
bytes.push(info.entries.length);
|
|
// entries:
|
|
info.entries.forEach(entry => {
|
|
const keyBytes = BinaryBPlusTreeBuilder.getKeyBytes(entry.key);
|
|
bytes.push(...keyBytes);
|
|
// lt_child_ptr: recalculate offset
|
|
console.assert(entry.ltIndex >= 0, `node entry "${entry.key}" has ltIndex < 0: ${entry.ltIndex}`);
|
|
const ltChildOffset = entry.ltIndex === 0 ? 0 : entry.ltIndex - 5 - (info.index + bytes.length);
|
|
console.assert(options.allowMissingChildIndexes || ltChildOffset !== 0, 'A node entry\'s ltChildOffset must ALWAYS be set!');
|
|
(0, binary_1.writeSignedOffset)(bytes, bytes.length, ltChildOffset, true);
|
|
});
|
|
// gt_child_ptr: calculate offset
|
|
const gtChildOffset = info.gtIndex === 0 ? 0 : info.gtIndex - 5 - (info.index + bytes.length);
|
|
console.assert(options.allowMissingChildIndexes || gtChildOffset !== 0, 'A node\'s gtChildOffset must ALWAYS be set!');
|
|
(0, binary_1.writeSignedOffset)(bytes, bytes.length, gtChildOffset, true);
|
|
let byteLength = bytes.length;
|
|
if (options.maxLength > 0 && byteLength > options.maxLength) {
|
|
throw new detailed_error_1.DetailedError('max-node-size-reached', `Node byte size (${byteLength}) grew above maximum of ${options.maxLength}`);
|
|
}
|
|
if (options.addFreeSpace) {
|
|
let freeSpace = 0;
|
|
if (options.maxLength > 0) {
|
|
freeSpace = options.maxLength - byteLength;
|
|
byteLength = options.maxLength;
|
|
}
|
|
else {
|
|
const freeEntries = this.maxEntriesPerNode - info.entries.length;
|
|
const avgEntrySize = Math.ceil((byteLength - 14) / info.entries.length);
|
|
// freeSpace = freeEntries * avgEntrySize;
|
|
freeSpace = Math.ceil(freeEntries * avgEntrySize * 1.1); // + 10%
|
|
byteLength += freeSpace;
|
|
}
|
|
// Add free space zero bytes
|
|
for (let i = 0; i < freeSpace; i++) {
|
|
bytes.push(0);
|
|
}
|
|
// update free_byte_length:
|
|
(0, binary_1.writeByteLength)(bytes, 5, freeSpace);
|
|
}
|
|
// update byte_length:
|
|
(0, binary_1.writeByteLength)(bytes, 0, byteLength);
|
|
return bytes;
|
|
}
|
|
/**
|
|
*
|
|
* @param {} info
|
|
* @param {} options
|
|
* @returns {Uint8Array} bytes
|
|
*/
|
|
createLeaf(info, options = { addFreeSpace: true }) {
|
|
// console.log(`Creating leaf for entries "${info.entries[0].key}" to "${info.entries.slice(-1)[0].key}" (${info.entries.length} entries, ${info.entries.reduce((total, entry) => total + entry.values.length, 0)} values)`);
|
|
// const tree = new BPlusTree(this.maxEntriesPerNode, this.uniqueKeys, this.metadataKeys);
|
|
// const leaf = new BPlusTreeLeaf(tree);
|
|
// info.entries.forEach(entry => {
|
|
// const leafEntry = new BPlusTreeLeafEntry(leaf, entry.key);
|
|
// leafEntry.values = entry.values;
|
|
// leaf.entries.push(leafEntry);
|
|
// // leaf.entries.push(entry); // // Changed to code above during TS port
|
|
// });
|
|
let hasExtData = typeof info.extData === 'object' && info.extData.length > 0;
|
|
const bytes = new binary_1.Uint8ArrayBuilder([
|
|
0, 0, 0, 0,
|
|
exports.FLAGS.IS_LEAF | (hasExtData ? exports.FLAGS.LEAF_HAS_EXT_DATA : 0),
|
|
0, 0, 0, 0, // free_byte_length
|
|
]);
|
|
const leafFlagsIndex = 4;
|
|
// prev_leaf_ptr:
|
|
const prevLeafOffset = info.prevIndex === 0 ? 0 : info.prevIndex - (info.index + 9);
|
|
bytes.writeInt48(prevLeafOffset);
|
|
// next_leaf_ptr:
|
|
let nextLeafOffset = info.nextIndex === 0 ? 0 : info.nextIndex === 'adjacent' ? 0 : info.nextIndex - (info.index + 15);
|
|
bytes.writeInt48(nextLeafOffset);
|
|
const extDataHeaderIndex = bytes.length;
|
|
bytes.push(0, 0, 0, 0, // ext_byte_length
|
|
0, 0, 0, 0);
|
|
// entries_length:
|
|
bytes.push(info.entries.length);
|
|
const moreDataBlocks = [];
|
|
// entries:
|
|
info.entries.forEach(entry => {
|
|
const keyBytes = BinaryBPlusTreeBuilder.getKeyBytes(entry.key);
|
|
bytes.push(...keyBytes);
|
|
// val_length:
|
|
const valLengthIndex = bytes.length;
|
|
if (hasExtData && info.extData.rebuild && entry.extData && !entry.extData.loaded) {
|
|
throw new detailed_error_1.DetailedError('ext-data-not-loaded', 'extData cannot be rebuilt if an entry\'s extData isn\'t loaded');
|
|
}
|
|
if (hasExtData && entry.extData && !info.extData.rebuild) {
|
|
// this entry has external value data (leaf is being overwritten),
|
|
// use existing details
|
|
// val_length:
|
|
bytes.push(exports.FLAGS.ENTRY_HAS_EXT_DATA);
|
|
if (!this.uniqueKeys) {
|
|
// value_list_length:
|
|
bytes.writeUint32(entry.extData.totalValues); // _writeByteLength(bytes, bytes.length, entry.extData.totalValues);
|
|
}
|
|
// ext_data_ptr:
|
|
bytes.writeUint32(entry.extData.leafOffset); // _writeByteLength(bytes, bytes.length, entry.extData.leafOffset);
|
|
return; // next!
|
|
}
|
|
else if (this.smallLeafs) {
|
|
// val_length: (small)
|
|
bytes.push(0);
|
|
}
|
|
else {
|
|
// val_length: (large)
|
|
bytes.push(0, 0, 0, 0);
|
|
}
|
|
const valueBytes = new binary_1.Uint8ArrayBuilder([]);
|
|
const addValue = (entryValue) => {
|
|
const { recordPointer, metadata } = entryValue;
|
|
// value_length:
|
|
valueBytes.push(recordPointer.length);
|
|
// value_data:
|
|
valueBytes.append(recordPointer);
|
|
// metadata:
|
|
this.metadataKeys.forEach(key => {
|
|
const metadataValue = metadata[key];
|
|
const mdBytes = BinaryBPlusTreeBuilder.getKeyBytes(metadataValue); // metadata_value has same structure as key, so getBinaryKeyData comes in handy here
|
|
valueBytes.append(mdBytes);
|
|
});
|
|
};
|
|
if (this.uniqueKeys) {
|
|
// value:
|
|
addValue(entry.values[0]);
|
|
}
|
|
else {
|
|
entry.values.forEach(entryValue => {
|
|
// value:
|
|
addValue(entryValue);
|
|
});
|
|
}
|
|
if (this.smallLeafs && valueBytes.length > config_1.MAX_SMALL_LEAF_VALUE_LENGTH) {
|
|
// Values too big for small leafs
|
|
// Store value bytes in ext_data block
|
|
if (!this.uniqueKeys) {
|
|
// value_list_length:
|
|
bytes.writeUint32(entry.values.length); // _writeByteLength(bytes, bytes.length, entry.values.length);
|
|
}
|
|
// ext_data_ptr:
|
|
const extPointerIndex = bytes.length;
|
|
bytes.push(0, 0, 0, 0);
|
|
// update val_length:
|
|
bytes.data[valLengthIndex] = exports.FLAGS.ENTRY_HAS_EXT_DATA;
|
|
// add the data
|
|
if (hasExtData && !info.extData.rebuild) {
|
|
// adding ext_data_block to existing leaf is impossible here,
|
|
// because we don't have existing ext_data
|
|
// addExtData function must be supplied to handle writing
|
|
console.assert(typeof options.addExtData === 'function', 'to add ext_data to existing leaf, provide addExtData function to options');
|
|
const { extIndex } = options.addExtData(extPointerIndex, valueBytes.data);
|
|
bytes.writeUint32(extIndex, extPointerIndex);
|
|
}
|
|
else {
|
|
// add to in-memory block, leaf output will include ext_data
|
|
moreDataBlocks.push({
|
|
pointerIndex: extPointerIndex,
|
|
bytes: valueBytes,
|
|
});
|
|
}
|
|
}
|
|
else {
|
|
// update val_length:
|
|
const valLength = valueBytes.length + (this.uniqueKeys ? 0 : 4); // +4 to include value_list_length bytes //bytes.length - valLengthIndex - 4;
|
|
if (this.smallLeafs) {
|
|
bytes.data[valLengthIndex] = valLength;
|
|
}
|
|
else {
|
|
bytes.writeUint32(valLength, valLengthIndex); // _writeByteLength(bytes, valLengthIndex, valLength);
|
|
}
|
|
if (!this.uniqueKeys) {
|
|
// value_list_length:
|
|
bytes.writeUint32(entry.values.length); // _writeByteLength(bytes, bytes.length, entry.values.length);
|
|
}
|
|
// add value bytes:
|
|
bytes.append(valueBytes); // _appendToArray(bytes, valueBytes);
|
|
}
|
|
});
|
|
if (moreDataBlocks.length > 0) {
|
|
// additional ext_data block will be written
|
|
if (!hasExtData && typeof options.maxLength === 'number' && options.maxLength > 0) {
|
|
// Try if ext_data_block can be added to the leaf by shrinking the leaf size
|
|
// (using its free space for ext_data block)
|
|
const minExtDataLength = options.addFreeSpace
|
|
? Math.ceil(moreDataBlocks.reduce((length, block) => length + 8 + Math.ceil(block.bytes.length * 1.1), 0) * 1.1)
|
|
: moreDataBlocks.reduce((length, block) => length + 8 + block.bytes.length, 0);
|
|
const freeBytes = options.maxLength - bytes.length;
|
|
if (freeBytes < minExtDataLength) {
|
|
throw new detailed_error_1.DetailedError('leaf-too-small-for-extdata', 'leaf needs rebuild: not enough free space to extend leaf with ext_data');
|
|
}
|
|
// Move free space to ext_data:
|
|
options.maxLength -= minExtDataLength;
|
|
info.extData = {
|
|
length: minExtDataLength,
|
|
};
|
|
}
|
|
hasExtData = true;
|
|
// update leaf_flags:
|
|
bytes.data[leafFlagsIndex] |= exports.FLAGS.LEAF_HAS_EXT_DATA;
|
|
}
|
|
if (!hasExtData) {
|
|
// update leaf_flags:
|
|
bytes.data[leafFlagsIndex] &= ~exports.FLAGS.LEAF_HAS_EXT_DATA; // if ((bytes[leafFlagsIndex] & FLAGS.LEAF_HAS_EXT_DATA) > 0) { bytes[leafFlagsIndex] ^= FLAGS.LEAF_HAS_EXT_DATA }; // has_ext_data (no)
|
|
// remove ext_byte_length, ext_free_byte_length
|
|
bytes.splice(extDataHeaderIndex, 8);
|
|
}
|
|
let byteLength = bytes.length;
|
|
if (options.maxLength > 0 && byteLength > options.maxLength) {
|
|
throw new detailed_error_1.DetailedError('max-leaf-size-reached', `leaf byte size grew above maximum of ${options.maxLength}`);
|
|
}
|
|
let freeSpace = 0;
|
|
if (options.addFreeSpace) {
|
|
if (options.maxLength > 0) {
|
|
freeSpace = options.maxLength - byteLength;
|
|
byteLength = options.maxLength;
|
|
}
|
|
else {
|
|
const freeEntries = this.maxEntriesPerNode - info.entries.length;
|
|
const avgEntrySize = info.entries.length === 0 ? 1 : Math.ceil((byteLength - 18) / info.entries.length);
|
|
// freeSpace = (freeEntries * avgEntrySize) + (avgEntrySize * 2);
|
|
freeSpace = Math.ceil(freeEntries * avgEntrySize * 1.1); // + 10%
|
|
byteLength += freeSpace;
|
|
}
|
|
// Add free space zero bytes
|
|
bytes.append(new Uint8Array(freeSpace)); // Uint8Array is initialized with 0's
|
|
// update free_byte_length:
|
|
bytes.writeUint32(freeSpace, 5);
|
|
}
|
|
// update byte_length:
|
|
bytes.writeUint32(byteLength, 0);
|
|
// Now, add any ext_data blocks
|
|
if (moreDataBlocks.length > 0) {
|
|
// Can only happen when this is a new leaf, or when it's being rebuilt
|
|
const fbm = options.addFreeSpace ? 0.1 : 0; // fmb -> free bytes multiplier
|
|
const maxEntries = this.maxEntriesPerNode;
|
|
const extDataSize = {
|
|
// minimum size: all ext_data blocks with 10% free space
|
|
minimum: moreDataBlocks.reduce((total, block) => total + 8 + block.bytes.length + Math.ceil(block.bytes.length * fbm), 0),
|
|
// average size: minimum + 10% free bytes for growth
|
|
get average() { return Math.ceil(this.minimum * (1 + fbm)); },
|
|
// ideal size: minimum size + room for more entries percentagewise
|
|
get ideal() {
|
|
const avgExtBlockSize = Math.ceil(this.minimum / moreDataBlocks.length);
|
|
const extDataValueRatio = moreDataBlocks.length / info.entries.length;
|
|
// if 5 out of 200 entries have extData: ratio === 0.025 (2.5%)
|
|
// with total current extData size of 800 bytes, that means it should
|
|
// allow growth for another 2.5% of remaining entries. With a max of
|
|
// 255 entries, that means leaving room for 2.5% of 55 more entries.
|
|
// So, max_entries * ratio * avg_block_size gives us that number!
|
|
const idealSize = Math.ceil(maxEntries * extDataValueRatio) * avgExtBlockSize;
|
|
return idealSize;
|
|
},
|
|
used: 0,
|
|
};
|
|
extDataSize.used = info.extData
|
|
? info.extData.length
|
|
: extDataSize.ideal; // default
|
|
if (info.extData && info.extData.length < extDataSize.minimum) { // && info.extData.rebuild
|
|
// ext_data becomes too large
|
|
// Try to steal free bytes from leaf
|
|
let bytesShort = extDataSize.ideal - info.extData.length; // first try getting space for free ext_data bytes as well
|
|
extDataSize.used = extDataSize.ideal;
|
|
if (freeSpace < bytesShort) {
|
|
// Not enough free space for the ideal size. Try again with only 10% free bytes
|
|
bytesShort = extDataSize.average - info.extData.length;
|
|
extDataSize.used = extDataSize.average;
|
|
}
|
|
if (freeSpace < bytesShort) {
|
|
// Not enough free space to include ext_data free bytes. Try again without ext_data free bytes
|
|
bytesShort = extDataSize.minimum - info.extData.length;
|
|
extDataSize.used = extDataSize.minimum;
|
|
}
|
|
if (freeSpace >= bytesShort) {
|
|
// steal free bytes from leaf
|
|
byteLength -= bytesShort;
|
|
freeSpace -= bytesShort;
|
|
// update byte_length:
|
|
bytes.writeUint32(byteLength, 0);
|
|
// update free_byte_length:
|
|
bytes.writeUint32(freeSpace, 5);
|
|
// remove trailing free bytes from leaf buffer:
|
|
bytes.splice(bytes.length - bytesShort);
|
|
// Add bytes to ext_data
|
|
info.extData.length += bytesShort;
|
|
}
|
|
else {
|
|
throw new detailed_error_1.DetailedError('max-leaf-extdata-size-reached', `leaf extdata grows larger than the ${info.extData.length} bytes available to it`);
|
|
}
|
|
}
|
|
const leafEndIndex = bytes.length;
|
|
bytes.reserve(extDataSize.used);
|
|
// const blocksDebugging = [];
|
|
// let addedExtBytes = 0;
|
|
while (moreDataBlocks.length > 0) { // moreDataBlocks.forEach(block => {
|
|
const block = moreDataBlocks.shift();
|
|
const offset = bytes.length - leafEndIndex; // offset from leaf end index
|
|
bytes.writeUint32(offset, block.pointerIndex); // update ext_data_ptr
|
|
// Calculate 10% free space per block
|
|
const free = options.addFreeSpace ? Math.ceil(block.bytes.length * fbm) : 0;
|
|
const blockLength = block.bytes.length + free;
|
|
// blocksDebugging.push({
|
|
// index: bytes.length,
|
|
// length: blockLength,
|
|
// free: {
|
|
// length: free,
|
|
// index: bytes.length + block.bytes.data.length,
|
|
// end: bytes.length + block.bytes.data.length + free
|
|
// }
|
|
// });
|
|
// const debugStartIndex = bytes.length;
|
|
// ext_block_length:
|
|
bytes.writeUint32(blockLength);
|
|
// ext_block_free_length:
|
|
bytes.writeUint32(free);
|
|
// data:
|
|
bytes.append(block.bytes.data);
|
|
// Add free space:
|
|
bytes.append(new Uint8Array(free));
|
|
// addedExtBytes += bytes.length - debugStartIndex;
|
|
} //);
|
|
const extByteLength = bytes.length - leafEndIndex;
|
|
// console.assert(extByteLength === extDataSize.minimum, 'These must be equal by now!');
|
|
// console.assert(addedExtBytes === extByteLength, 'Why are these not the same?');
|
|
const extFreeByteLength = info.extData // && info.extData.rebuild
|
|
? info.extData.length - extByteLength
|
|
: options.addFreeSpace
|
|
? extDataSize.used - extByteLength // Math.ceil(extByteLength * 0.1)
|
|
: 0;
|
|
// // Debug free space:
|
|
// const freeSpaceStartIndex = bytes.length; // - extFreeByteLength;
|
|
// blocksDebugging.forEach(block => {
|
|
// if (block.free.end > freeSpaceStartIndex) {
|
|
// debugger; // This is the problem
|
|
// }
|
|
// });
|
|
// update extData info
|
|
hasExtData = true;
|
|
if (info.extData) {
|
|
info.extData.freeBytes = extFreeByteLength;
|
|
}
|
|
else {
|
|
info.extData = {
|
|
length: extByteLength + extFreeByteLength,
|
|
freeBytes: extFreeByteLength,
|
|
};
|
|
}
|
|
// Add free space:
|
|
bytes.append(new Uint8Array(extFreeByteLength));
|
|
// adjust byteLength
|
|
byteLength = bytes.length;
|
|
}
|
|
else if (hasExtData) {
|
|
byteLength += info.extData.length;
|
|
}
|
|
if (hasExtData) {
|
|
// update leaf_flags:
|
|
bytes.data[leafFlagsIndex] |= exports.FLAGS.LEAF_HAS_EXT_DATA; // has_ext_data (yes)
|
|
// update ext_byte_length:
|
|
bytes.writeUint32(info.extData.length, extDataHeaderIndex); // _writeByteLength(bytes, extDataHeaderIndex, info.extData.length);
|
|
// update ext_free_byte_length:
|
|
bytes.writeUint32(info.extData.freeBytes, extDataHeaderIndex + 4); // _writeByteLength(bytes, extDataHeaderIndex + 4, info.extData.freeBytes);
|
|
}
|
|
if (info.nextIndex === 'adjacent') {
|
|
// update next_leaf_ptr
|
|
nextLeafOffset = byteLength - 15;
|
|
bytes.writeInt48(nextLeafOffset, 15); //_writeSignedOffset(bytes, 15, nextLeafOffset, true);
|
|
}
|
|
// console.log(`Created leaf, ${bytes.length} bytes generated`);
|
|
return bytes.data;
|
|
}
|
|
getLeafEntryValueBytes(recordPointer, metadata) {
|
|
const bytes = [];
|
|
// value_length:
|
|
bytes.push(recordPointer.length);
|
|
// value_data:
|
|
bytes.push(...recordPointer);
|
|
// metadata:
|
|
this.metadataKeys.forEach(key => {
|
|
const metadataValue = metadata[key];
|
|
const valueBytes = tree_1.BPlusTree.getBinaryKeyData(metadataValue); // metadata_value has same structure as key, so getBinaryKeyData comes in handy here
|
|
bytes.push(...valueBytes);
|
|
});
|
|
return bytes;
|
|
}
|
|
static getKeyBytes(key) {
|
|
let keyBytes = [];
|
|
let keyType = exports.KEY_TYPE.UNDEFINED;
|
|
switch (typeof key) {
|
|
case 'undefined': {
|
|
keyType = exports.KEY_TYPE.UNDEFINED;
|
|
break;
|
|
}
|
|
case 'string': {
|
|
keyType = exports.KEY_TYPE.STRING;
|
|
keyBytes = Array.from(encodeString(key)); // textEncoder.encode(key)
|
|
console.assert(keyBytes.length < 256, `key byte size for "${key}" is too large, max is 255`);
|
|
break;
|
|
}
|
|
case 'number': {
|
|
keyType = exports.KEY_TYPE.NUMBER;
|
|
keyBytes = numberToBytes(key);
|
|
// Remove trailing 0's to reduce size for smaller and integer values
|
|
while (keyBytes[keyBytes.length - 1] === 0) {
|
|
keyBytes.pop();
|
|
}
|
|
break;
|
|
}
|
|
case 'bigint': {
|
|
keyType = exports.KEY_TYPE.BIGINT;
|
|
keyBytes = bigintToBytes(key);
|
|
break;
|
|
}
|
|
case 'boolean': {
|
|
keyType = exports.KEY_TYPE.BOOLEAN;
|
|
keyBytes = [key ? 1 : 0];
|
|
break;
|
|
}
|
|
case 'object': {
|
|
if (key instanceof Date) {
|
|
keyType = exports.KEY_TYPE.DATE;
|
|
keyBytes = numberToBytes(key.getTime());
|
|
}
|
|
else if (key === null) {
|
|
keyType = exports.KEY_TYPE.UNDEFINED;
|
|
}
|
|
else {
|
|
throw new detailed_error_1.DetailedError('invalid-object-key-type', `Unsupported object key type: ${key}`);
|
|
}
|
|
break;
|
|
}
|
|
default: {
|
|
throw new detailed_error_1.DetailedError('invalid-key-type', `Unsupported key type: ${typeof key}`);
|
|
}
|
|
}
|
|
const bytes = [];
|
|
// key_type:
|
|
bytes.push(keyType);
|
|
// key_length:
|
|
bytes.push(keyBytes.length);
|
|
// key_data:
|
|
bytes.push(...keyBytes);
|
|
return bytes;
|
|
}
|
|
}
|
|
exports.BinaryBPlusTreeBuilder = BinaryBPlusTreeBuilder;
|
|
|
|
},{"../binary":212,"../detailed-error":238,"./config":225,"./tree":233,"acebase-core":12}],216:[function(require,module,exports){
|
|
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.BinaryBPlusTreeLeafEntryValue = void 0;
|
|
class BinaryBPlusTreeLeafEntryValue {
|
|
/**
|
|
*
|
|
* @param recordPointer used to be called "value", renamed to prevent confusion
|
|
* @param metadata
|
|
*/
|
|
constructor(recordPointer, metadata) {
|
|
this.recordPointer = recordPointer;
|
|
this.metadata = metadata;
|
|
}
|
|
/** @deprecated use .recordPointer instead */
|
|
get value() {
|
|
return this.recordPointer;
|
|
}
|
|
}
|
|
exports.BinaryBPlusTreeLeafEntryValue = BinaryBPlusTreeLeafEntryValue;
|
|
|
|
},{}],217:[function(require,module,exports){
|
|
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.BinaryBPlusTreeLeafEntry = void 0;
|
|
class BinaryBPlusTreeLeafEntry {
|
|
/**
|
|
* @param key
|
|
* @param values Array of binary values - NOTE if the tree has unique values, it must always wrap the single value in an Array: [value]
|
|
*/
|
|
constructor(key, values) {
|
|
this.key = key;
|
|
this.values = values;
|
|
this.key = key;
|
|
this.values = values;
|
|
}
|
|
/**
|
|
* @deprecated use .values[0] instead
|
|
*/
|
|
get value() {
|
|
return this.values[0];
|
|
}
|
|
get totalValues() {
|
|
if (typeof this._totalValues === 'number') {
|
|
return this._totalValues;
|
|
}
|
|
if (this.extData) {
|
|
return this.extData.totalValues;
|
|
}
|
|
return this.values.length;
|
|
}
|
|
set totalValues(nr) {
|
|
this._totalValues = nr;
|
|
}
|
|
/** Loads values from leaf's extData block */
|
|
async loadValues() {
|
|
throw new Error('entry.loadValues must be overridden if leaf has extData');
|
|
}
|
|
}
|
|
exports.BinaryBPlusTreeLeafEntry = BinaryBPlusTreeLeafEntry;
|
|
|
|
},{}],218:[function(require,module,exports){
|
|
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.BinaryBPlusTreeLeaf = void 0;
|
|
const detailed_error_1 = require("../detailed-error");
|
|
const binary_tree_node_info_1 = require("./binary-tree-node-info");
|
|
const typesafe_compare_1 = require("./typesafe-compare");
|
|
class BinaryBPlusTreeLeaf extends binary_tree_node_info_1.BinaryBPlusTreeNodeInfo {
|
|
constructor(nodeInfo) {
|
|
console.assert(typeof nodeInfo.hasExtData === 'boolean', 'nodeInfo.hasExtData must be specified');
|
|
super(nodeInfo);
|
|
this.prevLeafOffset = 0;
|
|
this.nextLeafOffset = 0;
|
|
this.extData = {
|
|
length: 0,
|
|
freeBytes: 0,
|
|
loaded: false,
|
|
async load() {
|
|
// Make sure all extData blocks are read. Needed when eg rebuilding
|
|
throw new detailed_error_1.DetailedError('method-not-overridden', 'BinaryBPlusTreeLeaf.extData.load must be overriden');
|
|
},
|
|
};
|
|
this.entries = [];
|
|
}
|
|
static get prevLeafPtrIndex() { return 9; }
|
|
static get nextLeafPtrIndex() { return 15; }
|
|
static getPrevLeafOffset(leafIndex, prevLeafIndex) {
|
|
return prevLeafIndex > 0
|
|
? prevLeafIndex - leafIndex - 9
|
|
: 0;
|
|
}
|
|
static getNextLeafOffset(leafIndex, nextLeafIndex) {
|
|
return nextLeafIndex > 0
|
|
? nextLeafIndex - leafIndex - 15
|
|
: 0;
|
|
}
|
|
get hasPrevious() { return typeof this.getPrevious === 'function'; }
|
|
get hasNext() { return typeof this.getNext === 'function'; }
|
|
get prevLeafIndex() {
|
|
return this.prevLeafOffset !== 0
|
|
? this.index + 9 + this.prevLeafOffset
|
|
: 0;
|
|
}
|
|
set prevLeafIndex(newIndex) {
|
|
this.prevLeafOffset = newIndex > 0
|
|
? newIndex - this.index - 9
|
|
: 0;
|
|
}
|
|
get nextLeafIndex() {
|
|
return this.nextLeafOffset !== 0
|
|
? this.index + (this.tree.info.hasLargePtrs ? 15 : 13) + this.nextLeafOffset
|
|
: 0;
|
|
}
|
|
set nextLeafIndex(newIndex) {
|
|
this.nextLeafOffset = newIndex > 0
|
|
? newIndex - this.index - (this.tree.info.hasLargePtrs ? 15 : 13)
|
|
: 0;
|
|
}
|
|
findEntryIndex(key) {
|
|
return this.entries.findIndex(entry => (0, typesafe_compare_1._isEqual)(entry.key, key));
|
|
}
|
|
findEntry(key) {
|
|
return this.entries[this.findEntryIndex(key)];
|
|
}
|
|
}
|
|
exports.BinaryBPlusTreeLeaf = BinaryBPlusTreeLeaf;
|
|
|
|
},{"../detailed-error":238,"./binary-tree-node-info":220,"./typesafe-compare":235}],219:[function(require,module,exports){
|
|
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.BinaryBPlusTreeNodeEntry = void 0;
|
|
const detailed_error_1 = require("../detailed-error");
|
|
class BinaryBPlusTreeNodeEntry {
|
|
constructor(key) {
|
|
this.key = key;
|
|
this.ltChildOffset = null;
|
|
}
|
|
async getLtChild() {
|
|
throw new detailed_error_1.DetailedError('method not overridden', 'getLtChild must be overridden');
|
|
}
|
|
}
|
|
exports.BinaryBPlusTreeNodeEntry = BinaryBPlusTreeNodeEntry;
|
|
|
|
},{"../detailed-error":238}],220:[function(require,module,exports){
|
|
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.BinaryBPlusTreeNodeInfo = void 0;
|
|
class BinaryBPlusTreeNodeInfo {
|
|
constructor(info) {
|
|
this.tree = info.tree;
|
|
this.isLeaf = info.isLeaf;
|
|
this.hasExtData = info.hasExtData || false;
|
|
this.bytes = info.bytes;
|
|
if (typeof info.sourceIndex === 'undefined') {
|
|
info.sourceIndex = info.index;
|
|
}
|
|
this.sourceIndex = info.sourceIndex;
|
|
if (typeof info.dataIndex === 'undefined') {
|
|
info.dataIndex = this.sourceIndex + 9; // node/leaf header is 9 bytes
|
|
}
|
|
this.dataIndex = info.dataIndex;
|
|
this.length = info.length;
|
|
this.free = info.free;
|
|
this.parentNode = info.parentNode;
|
|
this.parentEntry = info.parentEntry;
|
|
}
|
|
get index() {
|
|
return this.sourceIndex;
|
|
}
|
|
set index(value) {
|
|
this.sourceIndex = value;
|
|
}
|
|
}
|
|
exports.BinaryBPlusTreeNodeInfo = BinaryBPlusTreeNodeInfo;
|
|
|
|
},{}],221:[function(require,module,exports){
|
|
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.BinaryBPlusTreeNode = void 0;
|
|
const detailed_error_1 = require("../detailed-error");
|
|
const binary_tree_node_info_1 = require("./binary-tree-node-info");
|
|
class BinaryBPlusTreeNode extends binary_tree_node_info_1.BinaryBPlusTreeNodeInfo {
|
|
constructor(nodeInfo) {
|
|
super(nodeInfo);
|
|
this.entries = [];
|
|
this.gtChildOffset = null;
|
|
}
|
|
async getGtChild() {
|
|
throw new detailed_error_1.DetailedError('method-not-overridden', 'getGtChild must be overridden');
|
|
}
|
|
}
|
|
exports.BinaryBPlusTreeNode = BinaryBPlusTreeNode;
|
|
|
|
},{"../detailed-error":238,"./binary-tree-node-info":220}],222:[function(require,module,exports){
|
|
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.BinaryBPlusTreeTransactionOperation = void 0;
|
|
class BinaryBPlusTreeTransactionOperation {
|
|
constructor(operation) {
|
|
// operation.key = _normalizeKey(operation.key); // if (_isIntString(operation.key)) { operation.key = parseInt(operation.key); }
|
|
this.type = operation.type;
|
|
this.key = operation.key;
|
|
if (operation.type === 'add' || operation.type === 'remove') {
|
|
this.recordPointer = operation.recordPointer;
|
|
}
|
|
if (operation.type === 'add') {
|
|
this.metadata = operation.metadata;
|
|
}
|
|
if (operation.type === 'update') {
|
|
this.newValue = operation.newValue;
|
|
this.currentValue = operation.currentValue;
|
|
}
|
|
}
|
|
static add(key, recordPointer, metadata) {
|
|
return new BinaryBPlusTreeTransactionOperation({ type: 'add', key, recordPointer, metadata });
|
|
}
|
|
static update(key, newValue, currentValue, metadata) {
|
|
return new BinaryBPlusTreeTransactionOperation({ type: 'update', key, newValue, currentValue, metadata });
|
|
}
|
|
static remove(key, recordPointer) {
|
|
return new BinaryBPlusTreeTransactionOperation({ type: 'remove', key, recordPointer });
|
|
}
|
|
}
|
|
exports.BinaryBPlusTreeTransactionOperation = BinaryBPlusTreeTransactionOperation;
|
|
|
|
},{}],223:[function(require,module,exports){
|
|
(function (Buffer){(function (){
|
|
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.BinaryBPlusTree = exports.BlacklistingSearchOperator = void 0;
|
|
const acebase_core_1 = require("acebase-core");
|
|
const binary_1 = require("../binary");
|
|
const detailed_error_1 = require("../detailed-error");
|
|
const thread_safe_1 = require("../thread-safe");
|
|
const binary_reader_1 = require("./binary-reader");
|
|
const binary_tree_builder_1 = require("./binary-tree-builder");
|
|
const binary_tree_leaf_1 = require("./binary-tree-leaf");
|
|
const binary_tree_leaf_entry_1 = require("./binary-tree-leaf-entry");
|
|
const binary_tree_leaf_entry_value_1 = require("./binary-tree-leaf-entry-value");
|
|
const binary_tree_node_1 = require("./binary-tree-node");
|
|
const binary_tree_node_entry_1 = require("./binary-tree-node-entry");
|
|
const binary_tree_node_info_1 = require("./binary-tree-node-info");
|
|
const binary_tree_transaction_operation_1 = require("./binary-tree-transaction-operation");
|
|
const binary_writer_1 = require("./binary-writer");
|
|
const config_1 = require("./config");
|
|
const tree_1 = require("./tree");
|
|
const tree_builder_1 = require("./tree-builder");
|
|
const tree_leaf_entry_value_1 = require("./tree-leaf-entry-value");
|
|
const tx_1 = require("./tx");
|
|
const typesafe_compare_1 = require("./typesafe-compare");
|
|
const utils_1 = require("./utils");
|
|
const { bigintToBytes } = acebase_core_1.Utils;
|
|
class BlacklistingSearchOperator {
|
|
/**
|
|
* @param callback callback that runs for each entry, must return an array of the entry values to be blacklisted
|
|
*/
|
|
constructor(callback) {
|
|
this.check = callback;
|
|
}
|
|
}
|
|
exports.BlacklistingSearchOperator = BlacklistingSearchOperator;
|
|
class NoTreeInfoError extends Error {
|
|
constructor() { super('Tree info has not been read'); }
|
|
}
|
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
const noop = async () => { };
|
|
class BinaryBPlusTree {
|
|
/**
|
|
* Provides functionality to read and search in a B+tree from a binary data source
|
|
* @param readFn byte array, or function that reads from your data source, must return a promise that resolves with a byte array (the bytes read from file/memory)
|
|
* @param chunkSize numbers of bytes per chunk to read at once
|
|
* @param writeFn function that writes to your data source, must return a promise that resolves once write has completed
|
|
* @param id to edit the tree, pass a unique id to enable "thread-safe" locking
|
|
*/
|
|
constructor(readFn, chunkSize = 1024, writeFn, id) {
|
|
this.id = id;
|
|
this._chunkSize = chunkSize;
|
|
this._autoGrow = false;
|
|
this.id = id;
|
|
if (readFn instanceof Array) {
|
|
let data = readFn;
|
|
if (tree_1.BPlusTree.debugBinary) {
|
|
this.debugData = data;
|
|
data = this.debugData.map(entry => entry instanceof Array ? entry[1] : entry);
|
|
}
|
|
this._readFn = async (i, length) => {
|
|
const slice = data.slice(i, i + length);
|
|
return Buffer.from(slice);
|
|
};
|
|
}
|
|
else if (typeof readFn === 'function') {
|
|
this._readFn = readFn;
|
|
}
|
|
else {
|
|
throw new TypeError('readFn must be a byte array or function that reads from a data source');
|
|
}
|
|
if (typeof writeFn === 'function') {
|
|
this._writeFn = writeFn;
|
|
}
|
|
else if (typeof writeFn === 'undefined' && readFn instanceof Array) {
|
|
const sourceData = readFn;
|
|
this._writeFn = (data, index) => {
|
|
for (let i = 0; i < data.length; i++) {
|
|
sourceData[index + i] = data[i];
|
|
}
|
|
};
|
|
}
|
|
else {
|
|
this._writeFn = () => {
|
|
throw new Error('Cannot write data, no writeFn was supplied');
|
|
};
|
|
}
|
|
}
|
|
static async test(data) {
|
|
const tree = new BinaryBPlusTree(data);
|
|
let leaf = await tree.getFirstLeaf();
|
|
while (leaf) {
|
|
for (let i = 0; i < leaf.entries.length; i++) {
|
|
const entry = leaf.entries[i];
|
|
const found = await tree.find(entry.key);
|
|
if (found === null) {
|
|
throw new Error(`Tree entry ${entry.key} could not be found using tree.find`);
|
|
}
|
|
}
|
|
leaf = leaf.getNext ? await leaf.getNext() : null;
|
|
}
|
|
}
|
|
get autoGrow() {
|
|
return this._autoGrow;
|
|
}
|
|
set autoGrow(grow) {
|
|
this._autoGrow = grow === true;
|
|
// if (this._autoGrow) {
|
|
// console.warn('autoGrow enabled for binary tree');
|
|
// }
|
|
}
|
|
async _loadInfo() {
|
|
// Quick and dirty way to trigger info to be loaded. TODO: refactored later
|
|
await this._getReader();
|
|
}
|
|
async _getReader() {
|
|
const reader = new binary_reader_1.BinaryReader(this._readFn, this._chunkSize); // new ChunkReader(this._chunkSize, this._readFn);
|
|
await reader.init();
|
|
const header = await reader.get(6);
|
|
const originalByteLength = (header[0] << 24) | (header[1] << 16) | (header[2] << 8) | header[3];
|
|
if (!this._originalByteLength) {
|
|
this._originalByteLength = originalByteLength;
|
|
}
|
|
this.info = {
|
|
headerLength: 6,
|
|
byteLength: originalByteLength,
|
|
isUnique: (header[4] & binary_tree_builder_1.FLAGS.UNIQUE_KEYS) > 0,
|
|
hasMetadata: (header[4] & binary_tree_builder_1.FLAGS.HAS_METADATA) > 0,
|
|
hasFreeSpace: (header[4] & binary_tree_builder_1.FLAGS.HAS_FREE_SPACE) > 0,
|
|
hasFillFactor: (header[4] & binary_tree_builder_1.FLAGS.HAS_FILL_FACTOR) > 0,
|
|
hasSmallLeafs: (header[4] & binary_tree_builder_1.FLAGS.HAS_SMALL_LEAFS) > 0,
|
|
hasLargePtrs: (header[4] & binary_tree_builder_1.FLAGS.HAS_LARGE_PTRS) > 0,
|
|
freeSpace: 0,
|
|
get freeSpaceIndex() { return this.hasFillFactor ? 7 : 6; },
|
|
entriesPerNode: header[5],
|
|
fillFactor: 100,
|
|
metadataKeys: [],
|
|
};
|
|
// if (!this.info.hasLargePtrs) {
|
|
// console.warn(`Warning: tree "${this.id}" is read-only because it contains small ptrs. it needs to be rebuilt`);
|
|
// }
|
|
let additionalHeaderBytes = 0;
|
|
if (this.info.hasFillFactor) {
|
|
additionalHeaderBytes += 1;
|
|
}
|
|
if (this.info.hasFreeSpace) {
|
|
additionalHeaderBytes += 4;
|
|
}
|
|
if (this.info.hasMetadata) {
|
|
additionalHeaderBytes += 4;
|
|
}
|
|
if (additionalHeaderBytes > 0) {
|
|
// The tree has fill factor, free space, and/or metadata keys, read them
|
|
this.info.headerLength += additionalHeaderBytes;
|
|
const ahbBuffer = await reader.get(additionalHeaderBytes);
|
|
let i = 0;
|
|
if (this.info.hasFillFactor) {
|
|
this.info.fillFactor = ahbBuffer[i];
|
|
i++;
|
|
}
|
|
if (this.info.hasFreeSpace) {
|
|
this.info.freeSpace = (ahbBuffer[i] << 24) | (ahbBuffer[i + 1] << 16) | (ahbBuffer[i + 2] << 8) | ahbBuffer[i + 3];
|
|
i += 4;
|
|
}
|
|
if (this.info.hasMetadata) {
|
|
const length = (ahbBuffer[i] << 24) | (ahbBuffer[i + 1] << 16) | (ahbBuffer[i + 2] << 8) | ahbBuffer[i + 3];
|
|
this.info.headerLength += length;
|
|
// Read metadata
|
|
const mdBuffer = await reader.get(length);
|
|
const keyCount = mdBuffer[0];
|
|
let index = 1;
|
|
for (let i = 0; i < keyCount; i++) {
|
|
const keyLength = mdBuffer[index];
|
|
index++;
|
|
let key = '';
|
|
for (let j = 0; j < keyLength; j++) {
|
|
key += String.fromCharCode(mdBuffer[index + j]);
|
|
}
|
|
index += keyLength;
|
|
this.info.metadataKeys.push(key);
|
|
}
|
|
}
|
|
}
|
|
// Done reading header
|
|
return reader;
|
|
}
|
|
async _readChild(reader) {
|
|
const index = reader.sourceIndex; //reader.savePosition().index;
|
|
const headerLength = 9;
|
|
const header = await reader.get(headerLength); // byte_length, is_leaf, free_byte_length
|
|
const byteLength = (header[0] << 24) | (header[1] << 16) | (header[2] << 8) | header[3]; // byte_length
|
|
const isLeaf = (header[4] & binary_tree_builder_1.FLAGS.IS_LEAF) > 0; // is_leaf
|
|
const hasExtData = (header[4] & binary_tree_builder_1.FLAGS.LEAF_HAS_EXT_DATA) > 0; // has_ext_data
|
|
const freeBytesLength = (header[5] << 24) | (header[6] << 16) | (header[7] << 8) | header[8];
|
|
// load whole node/leaf for easy processing
|
|
const dataLength = byteLength - headerLength - freeBytesLength;
|
|
const bytes = await reader.get(dataLength);
|
|
console.assert(bytes.length === dataLength, 'less bytes read than requested?');
|
|
const childInfo = new binary_tree_node_info_1.BinaryBPlusTreeNodeInfo({
|
|
tree: this,
|
|
isLeaf,
|
|
hasExtData,
|
|
bytes,
|
|
sourceIndex: index,
|
|
dataIndex: index + headerLength,
|
|
length: byteLength,
|
|
free: freeBytesLength,
|
|
});
|
|
return childInfo;
|
|
}
|
|
_getLeaf(leafInfo, reader, options) {
|
|
if (!this.info) {
|
|
throw new Error('Tree info has not been read');
|
|
}
|
|
const leaf = new binary_tree_leaf_1.BinaryBPlusTreeLeaf(leafInfo);
|
|
const bytes = leaf.bytes;
|
|
// const savedPosition = reader.savePosition(-bytes.length);
|
|
const prevLeafOffset = (0, binary_1.readSignedOffset)(bytes, 0, this.info.hasLargePtrs); // prev_leaf_ptr
|
|
let index = this.info.hasLargePtrs ? 6 : 4;
|
|
const nextLeafOffset = (0, binary_1.readSignedOffset)(bytes, index, this.info.hasLargePtrs); // next_leaf_ptr
|
|
index += this.info.hasLargePtrs ? 6 : 4;
|
|
leaf.prevLeafOffset = prevLeafOffset;
|
|
leaf.nextLeafOffset = nextLeafOffset;
|
|
if (leafInfo.hasExtData) {
|
|
leaf.extData.length = (0, binary_1.readByteLength)(bytes, index);
|
|
leaf.extData.freeBytes = (0, binary_1.readByteLength)(bytes, index + 4);
|
|
index += 8;
|
|
leaf.extData.load = async () => {
|
|
// Load all extData blocks. Needed when eg rebuilding
|
|
if (leaf.extData.loaded) {
|
|
return;
|
|
}
|
|
const index = leaf.sourceIndex + leaf.length;
|
|
const length = leaf.extData.length - leaf.extData.freeBytes;
|
|
const r = reader.clone();
|
|
r.chunkSize = length; // So it will be 1 read
|
|
await r.go(index);
|
|
const bytes = await r.get(length);
|
|
leaf.entries.forEach(entry => {
|
|
if (entry.extData) {
|
|
entry.extData.loadFromExtData(bytes);
|
|
}
|
|
});
|
|
leaf.extData.loaded = true;
|
|
};
|
|
}
|
|
const entriesLength = bytes[index]; // entries_length
|
|
index++;
|
|
const readValue = () => {
|
|
const result = readEntryValue(bytes, index);
|
|
index += result.byteLength;
|
|
return result.entryValue;
|
|
};
|
|
const readEntryValue = (bytes, index) => {
|
|
console.assert(index < bytes.length, 'invalid data');
|
|
if (index >= bytes.length) {
|
|
throw new Error('invalid data');
|
|
}
|
|
const startIndex = index;
|
|
const valueLength = bytes[index]; // value_length
|
|
// console.assert(index + valueLength <= bytes.length, 'not enough data!');
|
|
if (index + valueLength > bytes.length) {
|
|
const bytesShort = index + valueLength - bytes.length;
|
|
throw new Error(`DEV ERROR: Cannot read entry value past the end of the read buffer (${bytesShort} bytes short)`);
|
|
}
|
|
index++;
|
|
const value = [];
|
|
// value_data:
|
|
for (let j = 0; j < valueLength; j++) {
|
|
value[j] = bytes[index + j];
|
|
}
|
|
index += valueLength;
|
|
// metadata:
|
|
const metadata = this.info.hasMetadata ? {} : undefined;
|
|
this.info.metadataKeys.forEach(key => {
|
|
// metadata_value:
|
|
// NOTE: it seems strange to use getKeyFromBinary to read a value, but metadata_value is stored in the same way as a key, so this comes in handy
|
|
const valueInfo = tree_1.BPlusTree.getKeyFromBinary(bytes, index);
|
|
metadata[key] = valueInfo.key;
|
|
index += valueInfo.byteLength;
|
|
});
|
|
return {
|
|
entryValue: new binary_tree_leaf_entry_value_1.BinaryBPlusTreeLeafEntryValue(value, metadata),
|
|
byteLength: index - startIndex,
|
|
};
|
|
};
|
|
for (let i = 0; i < entriesLength; i++) {
|
|
const keyInfo = tree_1.BPlusTree.getKeyFromBinary(bytes, index);
|
|
const key = keyInfo.key;
|
|
index += keyInfo.byteLength;
|
|
// Read value(s) and return
|
|
const hasExtData = this.info.hasSmallLeafs && (bytes[index] & binary_tree_builder_1.FLAGS.ENTRY_HAS_EXT_DATA) > 0;
|
|
const valLength = this.info.hasSmallLeafs
|
|
? hasExtData ? 0 : bytes[index]
|
|
: (bytes[index] << 24) | (bytes[index + 1] << 16) | (bytes[index + 2] << 8) | bytes[index + 3]; // val_length
|
|
index += this.info.hasSmallLeafs
|
|
? 1
|
|
: 4;
|
|
if (options && options.stats) {
|
|
// Skip values, only load value count
|
|
const entry = new binary_tree_leaf_entry_1.BinaryBPlusTreeLeafEntry(key, null);
|
|
if (this.info.isUnique) {
|
|
entry.totalValues = 1;
|
|
}
|
|
else {
|
|
entry.totalValues = (bytes[index] << 24) | (bytes[index + 1] << 16) | (bytes[index + 2] << 8) | bytes[index + 3]; // value_list_length
|
|
}
|
|
leaf.entries.push(entry);
|
|
if (hasExtData) {
|
|
index += this.info.isUnique ? 4 : 8; // skip ext_data_ptr (and value_list_length if not unique)
|
|
}
|
|
else {
|
|
index += valLength; // skip value
|
|
}
|
|
}
|
|
else if (this.info.isUnique) {
|
|
// Read single value
|
|
const entryValue = readValue();
|
|
leaf.entries.push(new binary_tree_leaf_entry_1.BinaryBPlusTreeLeafEntry(key, [entryValue]));
|
|
}
|
|
else {
|
|
// Read value_list_length
|
|
const valuesListLengthIndex = leafInfo.dataIndex + index;
|
|
let valuesLength = (bytes[index] << 24) | (bytes[index + 1] << 16) | (bytes[index + 2] << 8) | bytes[index + 3]; // value_list_length
|
|
index += 4;
|
|
if (hasExtData) {
|
|
// additional data will have to be loaded upon request
|
|
// ext_data_ptr:
|
|
let extDataOffset = (0, binary_1.readByteLength)(bytes, index);
|
|
index += 4;
|
|
const extDataBlockIndex = leafInfo.sourceIndex + leafInfo.length + extDataOffset;
|
|
const entry = new binary_tree_leaf_entry_1.BinaryBPlusTreeLeafEntry(key, new Array(valuesLength));
|
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
const tree = this;
|
|
Object.defineProperties(entry, {
|
|
values: {
|
|
get() {
|
|
return this.extData.values;
|
|
},
|
|
set(values) {
|
|
this.extData.values = values;
|
|
},
|
|
},
|
|
});
|
|
entry.extData = {
|
|
_headerLoaded: false,
|
|
_length: -1,
|
|
_freeBytes: -1,
|
|
_values: null,
|
|
_listLengthIndex: valuesListLengthIndex,
|
|
get length() {
|
|
if (this._headerLoaded) {
|
|
return this._length;
|
|
}
|
|
throw new Error('ext_data header not read yet');
|
|
},
|
|
get freeBytes() {
|
|
if (this._headerLoaded) {
|
|
return this._freeBytes;
|
|
}
|
|
throw new Error('ext_data header not read yet');
|
|
},
|
|
get values() {
|
|
if (this._values !== null) {
|
|
return this._values;
|
|
}
|
|
throw new Error('ext_data values were not read yet. use entry.extData.loadValues() first');
|
|
},
|
|
set values(values) {
|
|
this._values = values;
|
|
},
|
|
leafOffset: extDataOffset,
|
|
index: extDataBlockIndex,
|
|
get totalValues() { return valuesLength; },
|
|
set totalValues(n) { valuesLength = n; },
|
|
get loaded() { return this._values !== null; },
|
|
get _headerLength() { return 8; },
|
|
async loadValues(existingLock = null) {
|
|
// load all values
|
|
// reader = reader.clone();
|
|
const self = this;
|
|
const lock = await self.loadHeader(existingLock || true);
|
|
await reader.go(self.index + self._headerLength);
|
|
const extData = await reader.get(self._length - self._freeBytes);
|
|
self._values = [];
|
|
let index = 0;
|
|
for (let i = 0; i < valuesLength; i++) {
|
|
const result = readEntryValue(extData, index);
|
|
index += result.byteLength;
|
|
self._values.push(result.entryValue);
|
|
}
|
|
self.totalValues = valuesLength;
|
|
if (!existingLock) {
|
|
lock.release();
|
|
}
|
|
if (index !== self._length - self._freeBytes) {
|
|
throw new Error('DEV ERROR: index should now be at the known end of the data');
|
|
}
|
|
return self._values;
|
|
},
|
|
async loadHeader(lockOptions) {
|
|
const self = this;
|
|
const keepLock = lockOptions === true;
|
|
const existingLock = typeof lockOptions === 'object' ? lockOptions : null;
|
|
// if (self._headerLoaded) {
|
|
// return keepLock ? ThreadSafe.lock(leaf) : Promise.resolve(null);
|
|
// }
|
|
reader = reader.clone();
|
|
// load header
|
|
const lock = existingLock || await thread_safe_1.ThreadSafe.lock(leaf);
|
|
await reader.go(self.index);
|
|
const extHeader = await reader.get(self._headerLength); // ext_block_length, ext_block_free_length
|
|
self._headerLoaded = true;
|
|
self._length = (0, binary_1.readByteLength)(extHeader, 0);
|
|
self._freeBytes = (0, binary_1.readByteLength)(extHeader, 4);
|
|
console.assert(self._length >= 0 && self._freeBytes >= 0 && self._freeBytes < self._length, 'invalid data');
|
|
if (keepLock || existingLock) {
|
|
return lock;
|
|
}
|
|
else {
|
|
return lock.release();
|
|
}
|
|
},
|
|
loadFromExtData(allExtData) {
|
|
const self = this;
|
|
let index = extDataOffset;
|
|
self._headerLoaded = true;
|
|
self._length = (0, binary_1.readByteLength)(allExtData, index);
|
|
self._freeBytes = (0, binary_1.readByteLength)(allExtData, index + 4);
|
|
index += self._headerLength; // 8
|
|
self._values = [];
|
|
for (let i = 0; i < valuesLength; i++) {
|
|
const result = readEntryValue(allExtData, index);
|
|
index += result.byteLength;
|
|
self._values.push(result.entryValue);
|
|
}
|
|
self.totalValues = valuesLength;
|
|
},
|
|
async addValue(recordPointer, metadata) {
|
|
// add value to this entry's extData block.
|
|
const self = this;
|
|
const lock = await self.loadHeader(true);
|
|
// await tree._testTree(); // Check tree when debugging
|
|
// We have to add it to ext_data, and update leaf's value_list_length
|
|
// no checking for existing recordPointer
|
|
const builder = new binary_tree_builder_1.BinaryBPlusTreeBuilder({ metadataKeys: tree.info.metadataKeys });
|
|
const extValueData = builder.getLeafEntryValueBytes(recordPointer, metadata);
|
|
let extBlockMoves = false;
|
|
let newValueIndex = -1;
|
|
if (extValueData.length > self._freeBytes) {
|
|
// NEW: check if parent ext_data block has free space, maybe we can use that space
|
|
const requiredSpace = (() => {
|
|
const grossNewLength = entry.extData.length - entry.extData.freeBytes + extValueData.length;
|
|
const newValues = Math.ceil((entry.totalValues + 1) * 1.1); // 10% more values than will have now
|
|
const avgValueLength = Math.ceil((entry.extData.length - entry.extData.freeBytes) / entry.extData.totalValues);
|
|
const netNewLength = avgValueLength * newValues;
|
|
const newFreeBytes = netNewLength - grossNewLength;
|
|
return {
|
|
bytes: netNewLength + self._headerLength,
|
|
length: netNewLength,
|
|
freeBytes: newFreeBytes,
|
|
};
|
|
})();
|
|
if (requiredSpace.bytes > leaf.extData.freeBytes) {
|
|
lock.release();
|
|
throw new detailed_error_1.DetailedError('max-extdata-size-reached', 'No space left to add value to leaf ext_data_block');
|
|
}
|
|
else {
|
|
// leaf has enough free space in its ext_data to add a new ext_data_block
|
|
// store extData there and move the leaf entry's pointer
|
|
// Load existing values now before adjusting indexes etc
|
|
await entry.extData.loadValues(lock);
|
|
// const oldIndex = entry.extData.index;
|
|
const oldOffset = extDataOffset;
|
|
const newOffset = leaf.extData.length - leaf.extData.freeBytes;
|
|
// if (newOffset === oldOffset + self._length) {
|
|
// // This is the last ext_data_block in ext_data, it can stay at the same spot
|
|
// newOffset = oldOffset;
|
|
// }
|
|
extDataOffset = newOffset;
|
|
entry.extData.index += (newOffset - oldOffset);
|
|
entry.extData._length = requiredSpace.length;
|
|
entry.extData._freeBytes = requiredSpace.freeBytes;
|
|
// // Let's check now if the ext_data_free_bytes in the file is the same as we have in memory
|
|
// const leafExtFreeBytesIndex =
|
|
// leaf.dataIndex
|
|
// + ((tree.info.hasLargePtrs ? 6 : 4) * 2) // prev_leaf_ptr, next_leaf_ptr
|
|
// + 4; // ext_byte_length
|
|
// const check = await tree._readFn(leafExtFreeBytesIndex, 4);
|
|
// const nr = _readByteLength(check, 0);
|
|
// console.assert(nr === leaf.extData.freeBytes, 'ext free bytes in file is different');
|
|
// const oldLeafExtFreeBytes = leaf.extData.freeBytes;
|
|
leaf.extData.freeBytes -= requiredSpace.bytes; // leaf.extData.length - (newOffset + requiredSpace.length);
|
|
// console.log(`addValue :: moving ext_block from index ${oldIndex} to ${entry.extData.index}, leaf's ext_data_free_bytes reduces from ${oldLeafExtFreeBytes} to ${leaf.extData.freeBytes} bytes`)
|
|
extBlockMoves = true;
|
|
}
|
|
}
|
|
else {
|
|
// Before adjusting freeBytes, calc new value storage index
|
|
newValueIndex = self.index + self._headerLength + self._length - self._freeBytes;
|
|
// Reduce free space in ext_data_block
|
|
entry.extData._freeBytes -= extValueData.length;
|
|
}
|
|
const extDataBlock = [
|
|
0, 0, 0, 0,
|
|
0, 0, 0, 0, // ext_block_free_length
|
|
];
|
|
// update ext_block_length:
|
|
(0, binary_1.writeByteLength)(extDataBlock, 0, entry.extData.length);
|
|
// update ext_block_free_length:
|
|
(0, binary_1.writeByteLength)(extDataBlock, 4, entry.extData.freeBytes);
|
|
if (extBlockMoves) {
|
|
// If the ext_data_block moves, it has to be rewritten entirely
|
|
// Add all existing values first
|
|
const builder = new binary_tree_builder_1.BinaryBPlusTreeBuilder({ metadataKeys: tree.info.metadataKeys });
|
|
entry.extData.values.forEach(val => {
|
|
const valData = builder.getLeafEntryValueBytes(val.recordPointer, val.metadata);
|
|
(0, utils_1._appendToArray)(extDataBlock, valData);
|
|
});
|
|
// Now add new value
|
|
(0, utils_1._appendToArray)(extDataBlock, extValueData);
|
|
// Check if the new size is indeed what we calculated
|
|
if (extDataBlock.length - 8 + entry.extData.freeBytes !== entry.extData.length) {
|
|
throw new Error('DEV ERROR: new ext_block size is not equal to calculated size');
|
|
}
|
|
}
|
|
const valueListLengthData = [0, 0, 0, 0];
|
|
(0, binary_1.writeByteLength)(valueListLengthData, 0, self.totalValues + 1);
|
|
// const displayIndex = index => (index + 4096).toString(16).toUpperCase();
|
|
// const displayBytes = bytes => '[' + bytes.map(b => b.toString(16)).join(',').toUpperCase() + ']';
|
|
try {
|
|
// console.log(`TreeWrite:ext_block_length(${entry.extData.length}), ext_block_free_length(${entry.extData.freeBytes})${extBlockMoves ? ', value_list' : ''} :: ${extDataBlock.length} bytes at index ${displayIndex(self.index)}: ${displayBytes(extDataBlock.slice(0,4))}, ${displayBytes(extDataBlock.slice(4,8))}${extBlockMoves ? ', [...]' : ''}`);
|
|
// console.log(`TreeWrite:value_list_length(${self.totalValues + 1}) :: ${valueListLengthData.length} bytes at index ${displayIndex(self._listLengthIndex)}: ${displayBytes(valueListLengthData)}`);
|
|
const promises = [
|
|
// Write header (ext_block_length, ext_block_free_length) or entire ext_data_block to its index:
|
|
tree._writeFn(extDataBlock, self.index),
|
|
// update value_list_length
|
|
tree._writeFn(valueListLengthData, self._listLengthIndex),
|
|
];
|
|
if (extBlockMoves) {
|
|
// Write new ext_data_ptr in leaf entry's val_data
|
|
let writeBytes = [0, 0, 0, 0];
|
|
(0, binary_1.writeByteLength)(writeBytes, 0, extDataOffset);
|
|
// console.log(`TreeWrite:ext_data_ptr(${extDataOffset}) :: ${writeBytes.length} bytes at index ${displayIndex(self._listLengthIndex + 4)}: ${displayBytes(writeBytes)}`);
|
|
let p = tree._writeFn(writeBytes, self._listLengthIndex + 4);
|
|
promises.push(p);
|
|
// Update leaf's ext_free_byte_length
|
|
const leafExtFreeBytesIndex = leaf.dataIndex
|
|
+ ((tree.info.hasLargePtrs ? 6 : 4) * 2) // prev_leaf_ptr, next_leaf_ptr
|
|
+ 4; // ext_byte_length
|
|
writeBytes = [0, 0, 0, 0];
|
|
(0, binary_1.writeByteLength)(writeBytes, 0, leaf.extData.freeBytes);
|
|
// console.log(`TreeWrite:ext_free_byte_length(${leaf.extData.freeBytes}) :: ${writeBytes.length} bytes at index ${displayIndex(leafExtFreeBytesIndex)}: ${displayBytes(writeBytes)}`);
|
|
p = tree._writeFn(writeBytes, leafExtFreeBytesIndex);
|
|
promises.push(p);
|
|
}
|
|
else {
|
|
// write new value:
|
|
// console.log(`TreeWrite:value :: ${extValueData.length} bytes at index ${displayIndex(newValueIndex)}: ${displayBytes(extValueData)}`);
|
|
const p = tree._writeFn(extValueData, newValueIndex);
|
|
promises.push(p);
|
|
}
|
|
await Promise.all(promises);
|
|
// self._freeBytes -= extValueData.length;
|
|
self.totalValues++;
|
|
// TEST
|
|
// try {
|
|
// console.log(`Values for entry '${entry.key}' updated: ${self.totalValues} values`);
|
|
// await tree._testTree();
|
|
// console.log(`Successfully added value to entry '${entry.key}'`);
|
|
// }
|
|
// catch (err) {
|
|
// console.error(`Tree is broken after updating entry '${entry.key}': ${err.message}`);
|
|
// }
|
|
}
|
|
finally {
|
|
await lock.release();
|
|
}
|
|
// TEST
|
|
// try {
|
|
// await self.loadValues();
|
|
// }
|
|
// catch (err) {
|
|
// console.error(`Values are broken after updating entry '${entry.key}': ${err.message}`);
|
|
// }
|
|
},
|
|
async removeValue(recordPointer) {
|
|
// remove value
|
|
// load the whole value, then rewrite it
|
|
const self = this;
|
|
const values = await self.loadValues();
|
|
// LOCK?
|
|
const index = values.findIndex(val => (0, typesafe_compare_1._compareBinary)(val.recordPointer, recordPointer));
|
|
if (!~index) {
|
|
return;
|
|
}
|
|
values.splice(index, 1);
|
|
// rebuild ext_data_block
|
|
const bytes = [
|
|
0, 0, 0, 0,
|
|
0, 0, 0, 0, // ext_block_free_length
|
|
];
|
|
// ext_block_length:
|
|
(0, binary_1.writeByteLength)(bytes, 0, self._length);
|
|
// Add all values
|
|
const builder = new binary_tree_builder_1.BinaryBPlusTreeBuilder({ metadataKeys: tree.info.metadataKeys });
|
|
values.forEach(val => {
|
|
const valData = builder.getLeafEntryValueBytes(val.recordPointer, val.metadata);
|
|
(0, utils_1._appendToArray)(bytes, valData);
|
|
});
|
|
// update ext_block_free_length:
|
|
(0, binary_1.writeByteLength)(bytes, 4, self._length - bytes.length);
|
|
const valueListLengthData = (0, binary_1.writeByteLength)([], 0, self.totalValues - 1);
|
|
await Promise.all([
|
|
// write ext_data_block
|
|
tree._writeFn(bytes, self.index),
|
|
// update value_list_length
|
|
tree._writeFn(valueListLengthData, self._listLengthIndex),
|
|
]);
|
|
self.totalValues--;
|
|
self._freeBytes = self._length - bytes.length;
|
|
},
|
|
};
|
|
entry.loadValues = async function loadValues() {
|
|
const lock = await thread_safe_1.ThreadSafe.lock(leaf);
|
|
await reader.go(extDataBlockIndex);
|
|
const extHeader = await reader.get(8); // ext_data_length, ext_free_byte_length
|
|
const length = (0, binary_1.readByteLength)(extHeader, 0);
|
|
const freeBytes = (0, binary_1.readByteLength)(extHeader, 4);
|
|
const data = await reader.get(length - freeBytes);
|
|
entry._values = [];
|
|
let index = 0;
|
|
for (let i = 0; i < this.totalValues; i++) {
|
|
const result = readEntryValue(data, index);
|
|
index += result.byteLength;
|
|
entry._values.push(result.entryValue);
|
|
}
|
|
lock.release();
|
|
return entry._values;
|
|
};
|
|
leaf.entries.push(entry);
|
|
}
|
|
else {
|
|
const entryValues = [];
|
|
for (let j = 0; j < valuesLength; j++) {
|
|
const entryValue = readValue();
|
|
entryValues.push(entryValue);
|
|
}
|
|
leaf.entries.push(new binary_tree_leaf_entry_1.BinaryBPlusTreeLeafEntry(key, entryValues));
|
|
}
|
|
}
|
|
}
|
|
if (prevLeafOffset !== 0) {
|
|
leaf.getPrevious = async () => {
|
|
const freshReader = reader.clone();
|
|
await freshReader.go(leaf.prevLeafIndex);
|
|
const childInfo = await this._readChild(freshReader);
|
|
console.assert(childInfo.isLeaf, `previous leaf is *not* a leaf. Current leaf index: ${leaf.sourceIndex}, next leaf offset: ${prevLeafOffset}, target index: ${leaf.dataIndex + prevLeafOffset}`);
|
|
const prevLeaf = await this._getLeaf(childInfo, freshReader, options);
|
|
return prevLeaf;
|
|
};
|
|
}
|
|
if (nextLeafOffset !== 0) {
|
|
leaf.getNext = async () => {
|
|
const freshReader = reader.clone();
|
|
await freshReader.go(leaf.nextLeafIndex);
|
|
const childInfo = await this._readChild(freshReader);
|
|
console.assert(childInfo.isLeaf, `next leaf is *not* a leaf. Current leaf index: ${leaf.sourceIndex}, next leaf offset: ${nextLeafOffset}, target index: ${leaf.dataIndex + 4 + nextLeafOffset}`);
|
|
const nextLeaf = await this._getLeaf(childInfo, freshReader, options);
|
|
console.assert(nextLeaf.entries.length === 0 || leaf.entries.length === 0 || (0, typesafe_compare_1._isMore)(nextLeaf.entries[0].key, leaf.entries[leaf.entries.length - 1].key), 'next leaf has lower keys than previous leaf?!');
|
|
return nextLeaf;
|
|
};
|
|
}
|
|
console.assert(leaf.entries.every((entry, index, arr) => index === 0 || (0, typesafe_compare_1._isMore)(entry.key, arr[index - 1].key)), 'Invalid B+Tree: leaf entries are not sorted ok');
|
|
return leaf;
|
|
}
|
|
async _writeNode(nodeInfo) {
|
|
// Rewrite the node.
|
|
// NOTE: not using BPlusTreeNode.toBinary for this, because
|
|
// that function writes children too, we don't want that
|
|
console.assert(nodeInfo.entries.length > 0, 'node has no entries!');
|
|
console.assert(nodeInfo.entries.every((entry, index, arr) => index === 0 || (0, typesafe_compare_1._isMore)(entry.key, arr[index - 1].key)), 'Node entries are not sorted ok');
|
|
try {
|
|
const builder = new binary_tree_builder_1.BinaryBPlusTreeBuilder({
|
|
uniqueKeys: this.info.isUnique,
|
|
maxEntriesPerNode: this.info.entriesPerNode,
|
|
metadataKeys: this.info.metadataKeys,
|
|
// Not needed:
|
|
byteLength: this.info.byteLength,
|
|
freeBytes: this.info.freeSpace,
|
|
});
|
|
const bytes = builder.createNode({
|
|
index: nodeInfo.index,
|
|
entries: nodeInfo.entries.map(entry => ({ key: entry.key, ltIndex: entry.ltChildIndex })),
|
|
gtIndex: nodeInfo.gtChildIndex,
|
|
}, {
|
|
addFreeSpace: true,
|
|
maxLength: nodeInfo.length,
|
|
});
|
|
console.assert(bytes.length <= nodeInfo.length, 'too many bytes allocated for node');
|
|
return await this._writeFn(bytes, nodeInfo.index);
|
|
}
|
|
catch (err) {
|
|
throw new detailed_error_1.DetailedError('write-node-fail', `Failed to write node: ${err.message}`, err);
|
|
}
|
|
}
|
|
async _writeLeaf(leafInfo) {
|
|
console.assert(leafInfo.entries.every((entry, index, arr) => index === 0 || (0, typesafe_compare_1._isMore)(entry.key, arr[index - 1].key)), 'Leaf entries are not sorted ok');
|
|
try {
|
|
const builder = new binary_tree_builder_1.BinaryBPlusTreeBuilder({
|
|
uniqueKeys: this.info.isUnique,
|
|
maxEntriesPerNode: this.info.entriesPerNode,
|
|
metadataKeys: this.info.metadataKeys,
|
|
smallLeafs: this.info.hasSmallLeafs,
|
|
// Not needed:
|
|
byteLength: this.info.byteLength,
|
|
freeBytes: this.info.freeSpace,
|
|
fillFactor: this.info.fillFactor,
|
|
});
|
|
const extData = leafInfo.extData
|
|
? {
|
|
length: leafInfo.extData.length,
|
|
freeBytes: leafInfo.extData.freeBytes,
|
|
rebuild: leafInfo.extData.loaded,
|
|
}
|
|
: null;
|
|
const addFreeSpace = true;
|
|
const writes = [];
|
|
const bytes = builder.createLeaf({
|
|
index: leafInfo.index,
|
|
prevIndex: leafInfo.prevLeafIndex,
|
|
nextIndex: leafInfo.nextLeafIndex,
|
|
entries: leafInfo.entries,
|
|
extData,
|
|
}, {
|
|
addFreeSpace,
|
|
maxLength: leafInfo.length,
|
|
addExtData: (pointerIndex, data) => {
|
|
// Write additional ext_data_block
|
|
const extIndex = extData.length - extData.freeBytes;
|
|
const fileIndex = leafInfo.sourceIndex + leafInfo.length + extIndex;
|
|
const bytes = new binary_1.Uint8ArrayBuilder();
|
|
const minRequired = data.length + 8;
|
|
if (extData.freeBytes < minRequired) {
|
|
throw new detailed_error_1.DetailedError('max-extdata-size-reached', 'Not enough free space in ext_data');
|
|
}
|
|
// Calculate free space
|
|
const maxFree = extData.freeBytes - minRequired; // Max available free space for new block
|
|
const free = addFreeSpace ? Math.min(maxFree, Math.ceil(data.length * 0.1)) : 0;
|
|
const length = data.length + free;
|
|
// ext_block_length:
|
|
bytes.writeUint32(length);
|
|
// ext_block_free_length:
|
|
bytes.writeUint32(free);
|
|
// data:
|
|
bytes.append(data);
|
|
// Add free space:
|
|
bytes.append(new Uint8Array(free));
|
|
// Adjust extData
|
|
extData.freeBytes -= bytes.length;
|
|
const writePromise = this._writeFn(bytes.data, fileIndex);
|
|
writes.push(writePromise);
|
|
return { extIndex };
|
|
},
|
|
});
|
|
const maxLength = leafInfo.length + (leafInfo.extData && leafInfo.extData.loaded ? leafInfo.extData.length : 0);
|
|
console.assert(bytes.length <= maxLength, 'more bytes needed than allocated for leaf');
|
|
// write leaf:
|
|
const promise = this._writeFn(bytes, leafInfo.index);
|
|
writes.push(promise);
|
|
// // Check ext_free_byte_length
|
|
// if (leafInfo.hasExtData) {
|
|
// const extDataLength = _writeByteLength([], 0, extData.length);
|
|
// const extDataFreeBytes = _writeByteLength([], 0, extData.freeBytes);
|
|
// // Check ext_free_byte_length
|
|
// bytes.slice(21, 25).forEach((b, i) => {
|
|
// console.assert(b === extDataLength[i], 'Not the same');
|
|
// });
|
|
// // Check ext_free_byte_length
|
|
// bytes.slice(25, 29).forEach((b, i) => {
|
|
// console.assert(b === extDataFreeBytes[i], 'Not the same');
|
|
// });
|
|
// }
|
|
const result = await Promise.all(writes);
|
|
// await this._testTree();
|
|
return result;
|
|
}
|
|
catch (err) {
|
|
throw new detailed_error_1.DetailedError('write-leaf-fail', `Failed to write leaf: ${err.message}`, err);
|
|
}
|
|
}
|
|
/**
|
|
* TODO: rename to `parseNode` or something
|
|
*/
|
|
_getNode(nodeInfo, reader) {
|
|
// const node = {
|
|
// entries: []
|
|
// };
|
|
const node = new binary_tree_node_1.BinaryBPlusTreeNode(nodeInfo);
|
|
const bytes = node.bytes;
|
|
const entriesLength = bytes[0];
|
|
console.assert(entriesLength > 0, 'Node read failure: no entries');
|
|
let index = 1;
|
|
for (let i = 0; i < entriesLength; i++) {
|
|
const keyInfo = tree_1.BPlusTree.getKeyFromBinary(bytes, index);
|
|
const key = keyInfo.key;
|
|
index += keyInfo.byteLength;
|
|
const entry = new binary_tree_node_entry_1.BinaryBPlusTreeNodeEntry(key);
|
|
node.entries.push(entry);
|
|
// read lt_child_ptr:
|
|
entry.ltChildOffset = (0, binary_1.readSignedOffset)(bytes, index, this.info.hasLargePtrs); // lt_child_ptr
|
|
console.assert(entry.ltChildOffset !== 0, 'Node read failure: invalid ltChildOffset 0');
|
|
entry.ltChildIndex = node.index + index + 9 + entry.ltChildOffset + (this.info.hasLargePtrs ? 5 : 3); // index + 9 header bytes, +5 because offset is from first byte
|
|
entry.getLtChild = async () => {
|
|
// return savedPosition.go(entry.ltChildOffset)
|
|
await reader.go(entry.ltChildIndex);
|
|
const childNodeInfo = await this._readChild(reader);
|
|
childNodeInfo.parentNode = node;
|
|
childNodeInfo.parentEntry = entry;
|
|
return childNodeInfo;
|
|
};
|
|
index += this.info.hasLargePtrs ? 6 : 4;
|
|
}
|
|
// read gt_child_ptr:
|
|
node.gtChildOffset = (0, binary_1.readSignedOffset)(bytes, index, this.info.hasLargePtrs); // gt_child_ptr
|
|
console.assert(node.gtChildOffset !== 0, 'Node read failure: invalid gtChildOffset 0');
|
|
node.gtChildIndex = node.index + index + 9 + node.gtChildOffset + (this.info.hasLargePtrs ? 5 : 3); // index + 9 header bytes, +5 because offset is from first byte
|
|
node.getGtChild = async () => {
|
|
await reader.go(node.gtChildIndex);
|
|
const childNodeInfo = await this._readChild(reader);
|
|
childNodeInfo.parentNode = node;
|
|
childNodeInfo.parentEntry = null;
|
|
return childNodeInfo;
|
|
};
|
|
return node;
|
|
}
|
|
/**
|
|
*
|
|
* @param mode If the requested lock is shared (reads) or exclusive (writes)
|
|
* @param fn function to execute with lock in place
|
|
* @returns
|
|
*/
|
|
async _threadSafe(mode, fn) {
|
|
if (!this.id) {
|
|
throw new detailed_error_1.DetailedError('tree-id-not-set', 'Set tree.id property to something unique for locking purposes');
|
|
}
|
|
const lock = await thread_safe_1.ThreadSafe.lock(this.id, { timeout: 15 * 60 * 1000, shared: mode === 'shared' }); // 15 minutes for debugging:
|
|
try {
|
|
let result = fn();
|
|
if (result instanceof Promise) {
|
|
result = await result;
|
|
}
|
|
return result;
|
|
}
|
|
finally {
|
|
lock.release();
|
|
}
|
|
}
|
|
async getFirstLeaf(options) {
|
|
return this._threadSafe('shared', () => this._getFirstLeaf(options));
|
|
}
|
|
async _getFirstLeaf(options) {
|
|
const reader = await this._getReader();
|
|
let nodeInfo = await this._readChild(reader);
|
|
while (!nodeInfo.isLeaf) {
|
|
const node = this._getNode(nodeInfo, reader);
|
|
const firstEntry = node.entries[0];
|
|
console.assert(firstEntry, 'node has no entries!');
|
|
nodeInfo = await firstEntry.getLtChild();
|
|
}
|
|
const leaf = this._getLeaf(nodeInfo, reader, options);
|
|
return leaf;
|
|
}
|
|
async getLastLeaf(options) {
|
|
return this._threadSafe('shared', () => this._getLastLeaf(options));
|
|
}
|
|
async _getLastLeaf(options) {
|
|
const reader = await this._getReader();
|
|
let nodeInfo = await this._readChild(reader);
|
|
while (!nodeInfo.isLeaf) {
|
|
const node = this._getNode(nodeInfo, reader);
|
|
nodeInfo = await node.getGtChild();
|
|
}
|
|
const leaf = this._getLeaf(nodeInfo, reader, options);
|
|
return leaf;
|
|
}
|
|
async findLeaf(searchKey, options) {
|
|
return this._threadSafe('shared', () => this._findLeaf(searchKey, options));
|
|
}
|
|
async _findLeaf(searchKey, options) {
|
|
// searchKey = _normalizeKey(searchKey); // if (_isIntString(searchKey)) { searchKey = parseInt(searchKey); }
|
|
const reader = await this._getReader();
|
|
let nodeInfo = await this._readChild(reader);
|
|
while (!nodeInfo.isLeaf) {
|
|
const node = this._getNode(nodeInfo, reader);
|
|
if (node.entries.length === 0) {
|
|
throw new Error('read node has no entries!');
|
|
}
|
|
const targetEntry = node.entries.find(entry => (0, typesafe_compare_1._isLess)(searchKey, entry.key));
|
|
if (targetEntry) {
|
|
nodeInfo = await targetEntry.getLtChild();
|
|
}
|
|
else {
|
|
nodeInfo = await node.getGtChild();
|
|
}
|
|
}
|
|
const leaf = this._getLeaf(nodeInfo, reader, options);
|
|
return leaf;
|
|
}
|
|
/**
|
|
* Searches the tree
|
|
* @param op operator to use for key comparison, can be single value operators "<", "<=", "==", "!=", ">", ">=", "matches", "!matches", double value operators "between", "!between", and multiple value operators "in", "!in"
|
|
* @param param single value or array for double/multiple value operators
|
|
* @param include what data to include in results. `filter`: recordPointers to filter upon
|
|
* @returns {Promise<{ entries?: BinaryBPlusTreeLeafEntry[], keys?: Array, keyCount?: number, valueCount?: number, values?: BinaryBPlusTreeLeafEntryValue[] }}
|
|
*/
|
|
search(op, param, include = { entries: true, values: false, keys: false, count: false }) {
|
|
return this._threadSafe('shared', () => this._search(op, param, include));
|
|
}
|
|
/**
|
|
* Searches the tree
|
|
* @param op operator to use for key comparison, can be single value operators "<", "<=", "==", "!=", ">", ">=", "matches", "!matches", double value operators "between", "!between", and multiple value operators "in", "!in"
|
|
* @param param single value or array for double/multiple value operators
|
|
* @param include what data to include in results. `filter`: recordPointers to filter upon
|
|
* @returns {Promise<{ entries?: BinaryBPlusTreeLeafEntry[]; keys?: Array; keyCount?: number; valueCount?: number; values?: BinaryBPlusTreeLeafEntryValue[] }>}
|
|
*/
|
|
_search(op, param, include = { entries: true, values: false, keys: false, count: false }) {
|
|
// TODO: async'ify
|
|
if (typeof op === 'string' && ['in', '!in', 'between', '!between'].includes(op) && !(param instanceof Array)) {
|
|
// param must be an array
|
|
throw new TypeError(`param must be an array when using operator ${op}`);
|
|
}
|
|
if (typeof op === 'string' && ['exists', '!exists'].includes(op)) {
|
|
// These operators are a bit strange: they return results for key [undefined] (op === "!exists"), or all other keys (op === "exists")
|
|
// search("exists", ..) executes ("!=", undefined)
|
|
// search("!exists", ..) executes ("==", undefined)
|
|
op = op === 'exists' ? '!=' : '==';
|
|
param = undefined;
|
|
}
|
|
if (param === null) {
|
|
param = undefined;
|
|
}
|
|
const getLeafOptions = { stats: !(include.entries || include.values) };
|
|
const results = {
|
|
entries: [],
|
|
keys: [],
|
|
keyCount: 0,
|
|
valueCount: 0,
|
|
values: [], // was not implemented? is include.values used anywhere?
|
|
};
|
|
let blacklistRpTree;
|
|
if (op instanceof BlacklistingSearchOperator) {
|
|
blacklistRpTree = new tree_1.BPlusTree(255, true);
|
|
}
|
|
// const binaryCompare = (a, b) => {
|
|
// if (a.length < b.length) { return -1; }
|
|
// if (a.length > b.length) { return 1; }
|
|
// for (let i = 0; i < a.length; i++) {
|
|
// if (a[i] < b[i]) { return -1; }
|
|
// if (a[i] > b[i]) { return 1; }
|
|
// }
|
|
// return 0;
|
|
// }
|
|
const filterRecordPointers = include.filter
|
|
// Using string comparison:
|
|
? include.filter.reduce((arr, entry) => {
|
|
arr = arr.concat(entry.values.map(val => String.fromCharCode(...val.recordPointer)));
|
|
return arr;
|
|
}, [])
|
|
// // Using binary comparison:
|
|
// ? include.filter.reduce((arr, entry) => {
|
|
// arr = arr.concat(entry.values.map(val => val.recordPointer));
|
|
// return arr;
|
|
// }, []).sort(binaryCompare)
|
|
: null;
|
|
let totalMatches = 0;
|
|
let totalAdded = 0;
|
|
const valuePromises = [];
|
|
const emptyValue = [];
|
|
const add = (entry) => {
|
|
totalMatches += entry.totalValues;
|
|
const requireValues = filterRecordPointers || include.entries || include.values || op instanceof BlacklistingSearchOperator;
|
|
if (requireValues && typeof entry.extData === 'object' && !entry.extData.loaded) {
|
|
// We haven't got its values yet
|
|
const p = entry.extData.loadValues().then(() => {
|
|
return add(entry); // Do it now
|
|
});
|
|
valuePromises.push(p);
|
|
return p;
|
|
}
|
|
if (filterRecordPointers || op instanceof BlacklistingSearchOperator) {
|
|
// Generate rp's for each value
|
|
entry.values.forEach(val => {
|
|
val.rp = String.fromCharCode(...val.recordPointer);
|
|
});
|
|
}
|
|
if (filterRecordPointers) {
|
|
// Apply filter first, only use what remains
|
|
// String comparison method seem to have slightly better performance than binary
|
|
// Using string comparison:
|
|
const values = entry.values.filter(val => filterRecordPointers.includes(val.rp));
|
|
// const recordPointers = entry.values.map(val => String.fromCharCode(...val.recordPointer));
|
|
// const values = [];
|
|
// for (let i = 0; i < recordPointers.length; i++) {
|
|
// let a = recordPointers[i];
|
|
// if (~filterRecordPointers.indexOf(a)) {
|
|
// values.push(entry.values[i]);
|
|
// }
|
|
// }
|
|
// // Using binary comparison:
|
|
// const recordPointers = entry.values.map(val => val.recordPointer).sort(binaryCompare);
|
|
// const values = [];
|
|
// for (let i = 0; i < recordPointers.length; i++) {
|
|
// let a = recordPointers[i];
|
|
// for (let j = 0; j < filterRecordPointers.length; j++) {
|
|
// let b = filterRecordPointers[j];
|
|
// let diff = binaryCompare(a, b);
|
|
// if (diff === 0) {
|
|
// let index = entry.values.findIndex(val => val.recordPointer === a);
|
|
// values.push(entry.values[index]);
|
|
// break;
|
|
// }
|
|
// else if (diff === -1) {
|
|
// // stop searching for this recordpointer
|
|
// break;
|
|
// }
|
|
// }
|
|
// }
|
|
if (values.length === 0) {
|
|
return;
|
|
}
|
|
entry.values = values;
|
|
entry.totalValues = values.length;
|
|
}
|
|
if (op instanceof BlacklistingSearchOperator) {
|
|
// // Generate rp's for each value
|
|
// entry.values.forEach(val => {
|
|
// val.rp = val.rp || String.fromCharCode(...val.recordPointer);
|
|
// });
|
|
// Check which values were previously blacklisted
|
|
entry.values = entry.values.filter(val => {
|
|
return blacklistRpTree.find(val.rp) === null;
|
|
});
|
|
if (entry.values.length === 0) {
|
|
return;
|
|
}
|
|
// Check which values should be blacklisted
|
|
const blacklistValues = op.check(entry);
|
|
if (blacklistValues instanceof Array) {
|
|
// Add to blacklist tree
|
|
blacklistValues.forEach(val => {
|
|
blacklistRpTree.add(val.rp, emptyValue);
|
|
});
|
|
// Remove from current results
|
|
entry.values = blacklistValues === entry.values
|
|
? [] // Same array, so all values were blacklisted
|
|
: entry.values.filter(value => blacklistValues.indexOf(value) < 0);
|
|
const removed = { values: 0, entries: 0 };
|
|
if (include.values) {
|
|
// Remove from previous results (values)
|
|
for (let i = 0; i < results.values.length; i++) {
|
|
const val = results.values[i];
|
|
// if (!val.rp) { val.rp = String.fromCharCode(...val.recordPointer); }
|
|
if (blacklistRpTree.find(val.rp)) {
|
|
results.values.splice(i, 1);
|
|
i--;
|
|
removed.values++;
|
|
}
|
|
}
|
|
}
|
|
if (include.entries) {
|
|
// Remove from previous results (entries, keys)
|
|
for (let i = 0; i < results.entries.length; i++) {
|
|
const entry = results.entries[i];
|
|
for (let j = 0; j < entry.values.length; j++) {
|
|
const val = entry.values[j];
|
|
// if (!val.rp) { val.rp = String.fromCharCode(...val.recordPointer); }
|
|
if (blacklistRpTree.find(val.rp)) {
|
|
entry.values.splice(j, 1);
|
|
j--;
|
|
if (!include.values) {
|
|
removed.values++;
|
|
}
|
|
if (entry.values.length === 0) {
|
|
results.entries.splice(i, 1);
|
|
i--;
|
|
removed.entries++;
|
|
if (include.keys) {
|
|
results.keys.splice(results.keys.indexOf(entry.key), 1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
results.valueCount -= removed.values;
|
|
results.keyCount -= removed.entries;
|
|
if (entry.values.length === 0) {
|
|
return;
|
|
}
|
|
}
|
|
// The way BlacklistingSearchOperator is currently used (including ALL values
|
|
// in the index besides the ones that are blacklisted along the way), we only
|
|
// want unique "non-blacklisted" recordpointers in the results. So, we want to
|
|
// remove all values that are already present in the current results:
|
|
for (let i = 0; i < entry.values.length; i++) {
|
|
const currentValue = entry.values[i];
|
|
let remove = false;
|
|
if (include.values) {
|
|
// TODO: change to: remove = results.values.includes
|
|
const index = results.values.findIndex(val => val.rp === currentValue.rp);
|
|
remove = index >= 0;
|
|
}
|
|
else if (include.entries) {
|
|
// Check result entries
|
|
for (let j = 0; j < results.entries.length; j++) {
|
|
const entry = results.entries[j];
|
|
// TODO: change to: remove = entry.values.includes
|
|
const index = entry.values.findIndex(val => val.rp === currentValue.rp);
|
|
remove = index >= 0;
|
|
if (remove) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (remove) {
|
|
entry.values.splice(i, 1);
|
|
i--;
|
|
}
|
|
}
|
|
if (entry.values.length === 0) {
|
|
return;
|
|
}
|
|
}
|
|
if (include.entries) {
|
|
results.entries.push(entry);
|
|
}
|
|
if (include.keys) {
|
|
results.keys.push(entry.key);
|
|
}
|
|
if (include.values) {
|
|
entry.values.forEach(val => results.values.push(val));
|
|
}
|
|
if (include.count) {
|
|
results.keyCount++;
|
|
results.valueCount += entry.totalValues;
|
|
}
|
|
totalAdded += entry.totalValues;
|
|
};
|
|
// const t1 = Date.now();
|
|
// const ret = () => {
|
|
// const t2 = Date.now();
|
|
// console.log(`tree.search [${op} ${param}] took ${t2-t1}ms, matched ${totalMatches} values, returning ${totalAdded} values in ${results.entries.length} entries`);
|
|
// return results;
|
|
// };
|
|
const ret = () => {
|
|
if (valuePromises.length > 0) {
|
|
return Promise.all(valuePromises)
|
|
.then(() => results);
|
|
}
|
|
else {
|
|
return results;
|
|
}
|
|
};
|
|
if (op instanceof BlacklistingSearchOperator) {
|
|
// NEW: custom callback methods to check match
|
|
// Full index scan needed
|
|
const processLeaf = (leaf) => {
|
|
for (let i = 0; i < leaf.entries.length; i++) {
|
|
const entry = leaf.entries[i];
|
|
// const keyMatch = typeof op.keyCheck === 'function' ? op.keyCheck(entry.key) : true;
|
|
// if (!keyMatch) { continue; }
|
|
add(entry); // check will be done by add
|
|
}
|
|
if (leaf.hasNext) {
|
|
return leaf.getNext()
|
|
.then(processLeaf);
|
|
}
|
|
else {
|
|
return ret(); // results; //ret(results);
|
|
}
|
|
};
|
|
return this._getFirstLeaf(getLeafOptions)
|
|
.then(processLeaf);
|
|
}
|
|
else if (['<', '<='].indexOf(op) >= 0) {
|
|
const processLeaf = (leaf) => {
|
|
let stop = false;
|
|
for (let i = 0; i < leaf.entries.length; i++) {
|
|
const entry = leaf.entries[i];
|
|
if (op === '<=' && (0, typesafe_compare_1._isLessOrEqual)(entry.key, param)) {
|
|
add(entry);
|
|
}
|
|
else if (op === '<' && (0, typesafe_compare_1._isLess)(entry.key, param)) {
|
|
add(entry);
|
|
}
|
|
else {
|
|
stop = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!stop && leaf.getNext) {
|
|
return leaf.getNext()
|
|
.then(processLeaf);
|
|
}
|
|
else {
|
|
return ret(); //results; //ret(results);
|
|
}
|
|
};
|
|
return this._getFirstLeaf(getLeafOptions)
|
|
.then(processLeaf);
|
|
}
|
|
else if (['>', '>='].indexOf(op) >= 0) {
|
|
const processLeaf = (leaf) => {
|
|
for (let i = 0; i < leaf.entries.length; i++) {
|
|
const entry = leaf.entries[i];
|
|
if (op === '>=' && (0, typesafe_compare_1._isMoreOrEqual)(entry.key, param)) {
|
|
add(entry);
|
|
}
|
|
else if (op === '>' && (0, typesafe_compare_1._isMore)(entry.key, param)) {
|
|
add(entry);
|
|
}
|
|
}
|
|
if (leaf.hasNext) {
|
|
return leaf.getNext()
|
|
.then(processLeaf);
|
|
}
|
|
else {
|
|
return ret(); //results; //ret(results);
|
|
}
|
|
};
|
|
return this._findLeaf(param, getLeafOptions)
|
|
.then(processLeaf);
|
|
}
|
|
else if (op === '==') {
|
|
return this._findLeaf(param, getLeafOptions)
|
|
.then(leaf => {
|
|
const entry = leaf.entries.find(entry => (0, typesafe_compare_1._isEqual)(entry.key, param)); //entry.key === param
|
|
if (entry) {
|
|
add(entry);
|
|
}
|
|
return ret(); // results; //ret(results);
|
|
});
|
|
}
|
|
else if (op === '!=') {
|
|
// Full index scan needed
|
|
const processLeaf = (leaf) => {
|
|
for (let i = 0; i < leaf.entries.length; i++) {
|
|
const entry = leaf.entries[i];
|
|
if ((0, typesafe_compare_1._isNotEqual)(entry.key, param)) {
|
|
add(entry);
|
|
} //entry.key !== param
|
|
}
|
|
if (leaf.hasNext) {
|
|
return leaf.getNext()
|
|
.then(processLeaf);
|
|
}
|
|
else {
|
|
return ret(); // results; //ret(results);
|
|
}
|
|
};
|
|
return this._getFirstLeaf(getLeafOptions)
|
|
.then(processLeaf);
|
|
}
|
|
else if (op === 'like') {
|
|
if (typeof param !== 'string') {
|
|
throw new TypeError(`search param value must be a string for operator 'like'`);
|
|
}
|
|
const wildcardIndex = ~(~param.indexOf('*') || ~param.indexOf('?')); // TODO: make less cryptic
|
|
const startSearch = wildcardIndex > 0 ? param.slice(0, wildcardIndex) : '';
|
|
const pattern = '^' + param.replace(/\*/g, '.*').replace(/\?/g, '.') + '$';
|
|
const re = new RegExp(pattern, 'i');
|
|
const processLeaf = (leaf) => {
|
|
for (let i = 0; i < leaf.entries.length; i++) {
|
|
const entry = leaf.entries[i];
|
|
if (re.test(entry.key.toString())) {
|
|
add(entry);
|
|
}
|
|
}
|
|
let stop = false;
|
|
if (wildcardIndex > 0 && leaf.entries.length > 0) {
|
|
// Check if we can stop. If the last entry does not start with the first part of the string.
|
|
// Eg: like 'Al*', we can stop if the last entry starts with 'Am'
|
|
const lastEntry = leaf.entries[leaf.entries.length - 1];
|
|
stop = lastEntry.key.toString().slice(0, wildcardIndex) > startSearch;
|
|
}
|
|
if (!stop && leaf.getNext) {
|
|
return leaf.getNext()
|
|
.then(processLeaf);
|
|
}
|
|
else {
|
|
return ret();
|
|
}
|
|
};
|
|
if (wildcardIndex === 0) {
|
|
return this._getFirstLeaf(getLeafOptions)
|
|
.then(processLeaf);
|
|
}
|
|
else {
|
|
return this._findLeaf(startSearch, getLeafOptions)
|
|
.then(processLeaf);
|
|
}
|
|
}
|
|
else if (op === '!like') {
|
|
// Full index scan needed
|
|
if (typeof param !== 'string') {
|
|
throw new TypeError(`search param value must be a string for operator '!like'`);
|
|
}
|
|
const pattern = '^' + param.replace(/\*/g, '.*').replace(/\?/g, '.') + '$';
|
|
const re = new RegExp(pattern, 'i');
|
|
const processLeaf = (leaf) => {
|
|
for (let i = 0; i < leaf.entries.length; i++) {
|
|
const entry = leaf.entries[i];
|
|
if (!re.test(entry.key.toString())) {
|
|
add(entry);
|
|
}
|
|
}
|
|
if (leaf.hasNext) {
|
|
return leaf.getNext()
|
|
.then(processLeaf);
|
|
}
|
|
else {
|
|
return ret(); // results; //ret(results);
|
|
}
|
|
};
|
|
return this._getFirstLeaf(getLeafOptions)
|
|
.then(processLeaf);
|
|
}
|
|
else if (op === 'in') {
|
|
if (!(param instanceof Array)) {
|
|
throw new TypeError(`search param value must be an array for operator 'in'`);
|
|
}
|
|
const sorted = param.slice().sort();
|
|
let searchKey = sorted.shift();
|
|
const processLeaf = (leaf) => {
|
|
while (true) {
|
|
const entry = leaf.entries.find(entry => (0, typesafe_compare_1._isEqual)(entry.key, searchKey)); //entry.key === searchKey
|
|
if (entry) {
|
|
add(entry);
|
|
}
|
|
searchKey = sorted.shift();
|
|
if (!searchKey) {
|
|
return ret(); // results; //ret(results);
|
|
}
|
|
else if ((0, typesafe_compare_1._isMore)(searchKey, leaf.entries[leaf.entries.length - 1].key)) {
|
|
return this._findLeaf(searchKey).then(processLeaf);
|
|
}
|
|
// Stay in the loop trying more keys on the same leaf
|
|
}
|
|
};
|
|
return this._findLeaf(searchKey, getLeafOptions)
|
|
.then(processLeaf);
|
|
}
|
|
else if (op === '!in') {
|
|
// Full index scan needed
|
|
if (!(param instanceof Array)) {
|
|
throw new TypeError(`search param value must be an array for operator '!in'`);
|
|
}
|
|
const keys = param;
|
|
const processLeaf = (leaf) => {
|
|
for (let i = 0; i < leaf.entries.length; i++) {
|
|
const entry = leaf.entries[i];
|
|
if (keys.findIndex(key => (0, typesafe_compare_1._isEqual)(key, entry.key)) < 0) {
|
|
add(entry);
|
|
} //if (keys.indexOf(entry.key) < 0)
|
|
}
|
|
if (leaf.hasNext) {
|
|
return leaf.getNext()
|
|
.then(processLeaf);
|
|
}
|
|
else {
|
|
return ret(); //results; //ret(results);
|
|
}
|
|
};
|
|
return this._getFirstLeaf(getLeafOptions)
|
|
.then(processLeaf);
|
|
}
|
|
else if (op === 'between') {
|
|
if (!(param instanceof Array)) {
|
|
throw new TypeError(`search param value must be an array for operator 'between'`);
|
|
}
|
|
let bottom = param[0], top = param[1];
|
|
if (top < bottom) {
|
|
const swap = top;
|
|
top = bottom;
|
|
bottom = swap;
|
|
}
|
|
return this._findLeaf(bottom, getLeafOptions)
|
|
.then(leaf => {
|
|
let stop = false;
|
|
const processLeaf = (leaf) => {
|
|
for (let i = 0; i < leaf.entries.length; i++) {
|
|
const entry = leaf.entries[i];
|
|
if ((0, typesafe_compare_1._isMoreOrEqual)(entry.key, bottom) && (0, typesafe_compare_1._isLessOrEqual)(entry.key, top)) {
|
|
add(entry);
|
|
}
|
|
if ((0, typesafe_compare_1._isMore)(entry.key, top)) {
|
|
stop = true;
|
|
break;
|
|
}
|
|
}
|
|
if (stop || !leaf.getNext) {
|
|
return ret(); // results; //ret(results);
|
|
}
|
|
else {
|
|
return leaf.getNext().then(processLeaf);
|
|
}
|
|
};
|
|
return processLeaf(leaf);
|
|
});
|
|
}
|
|
else if (op === '!between') {
|
|
// Equal to key < bottom || key > top
|
|
if (!(param instanceof Array)) {
|
|
throw new TypeError(`search param value must be an array for operator '!between'`);
|
|
}
|
|
let bottom = param[0], top = param[1];
|
|
if (top < bottom) {
|
|
const swap = top;
|
|
top = bottom;
|
|
bottom = swap;
|
|
}
|
|
// Add lower range first, lowest value < val < bottom
|
|
return this._getFirstLeaf(getLeafOptions)
|
|
.then(leaf => {
|
|
let stop = false;
|
|
const processLeaf = (leaf) => {
|
|
for (let i = 0; i < leaf.entries.length; i++) {
|
|
const entry = leaf.entries[i];
|
|
if ((0, typesafe_compare_1._isLess)(entry.key, bottom)) {
|
|
add(entry);
|
|
}
|
|
else {
|
|
stop = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!stop && leaf.getNext) {
|
|
return leaf.getNext().then(processLeaf);
|
|
}
|
|
};
|
|
return processLeaf(leaf);
|
|
})
|
|
.then(() => {
|
|
// Now add upper range, top < val < highest value
|
|
return this._findLeaf(top, getLeafOptions);
|
|
})
|
|
.then(leaf => {
|
|
const processLeaf = (leaf) => {
|
|
for (let i = 0; i < leaf.entries.length; i++) {
|
|
const entry = leaf.entries[i];
|
|
if ((0, typesafe_compare_1._isMore)(entry.key, top)) {
|
|
add(entry);
|
|
}
|
|
}
|
|
if (!leaf.getNext) {
|
|
return ret(); // results; //ret(results);
|
|
}
|
|
else {
|
|
return leaf.getNext().then(processLeaf);
|
|
}
|
|
};
|
|
return processLeaf(leaf);
|
|
});
|
|
}
|
|
else if (op === 'matches' || op === '!matches') {
|
|
// Full index scan needed
|
|
if (!(param instanceof RegExp)) {
|
|
throw new TypeError(`search param value must be a RegExp for operator 'matches' and '!matches'`);
|
|
}
|
|
const re = param;
|
|
const processLeaf = (leaf) => {
|
|
for (let i = 0; i < leaf.entries.length; i++) {
|
|
const entry = leaf.entries[i];
|
|
const isMatch = re.test(entry.key.toString());
|
|
if ((isMatch && op === 'matches') || (!isMatch && op === '!matches')) {
|
|
add(entry);
|
|
}
|
|
}
|
|
if (leaf.hasNext) {
|
|
return leaf.getNext()
|
|
.then(processLeaf);
|
|
}
|
|
else {
|
|
return ret(); // results; //ret(results);
|
|
}
|
|
};
|
|
return this._getFirstLeaf(getLeafOptions)
|
|
.then(processLeaf);
|
|
}
|
|
else {
|
|
throw new Error(`Unknown search operator "${op}"`);
|
|
}
|
|
}
|
|
/**
|
|
* @returns returns a promise that resolves with 1 value (unique keys), a values array or the number of values (options.stats === true)
|
|
*/
|
|
async find(searchKey, options) {
|
|
return this._threadSafe('shared', () => this._find(searchKey, options));
|
|
}
|
|
/**
|
|
* @returns returns a promise that resolves with 1 value (unique keys), a values array or the number of values (options.stats === true)
|
|
*/
|
|
async _find(searchKey, options) {
|
|
// searchKey = _normalizeKey(searchKey); //if (_isIntString(searchKey)) { searchKey = parseInt(searchKey); }
|
|
const leaf = await this._findLeaf(searchKey, options);
|
|
const entry = leaf.entries.find(entry => (0, typesafe_compare_1._isEqual)(searchKey, entry.key));
|
|
if (!this.info) {
|
|
throw new NoTreeInfoError();
|
|
}
|
|
if (options && options.stats) {
|
|
return entry ? entry.totalValues : 0;
|
|
}
|
|
else if (entry) {
|
|
if (entry.extData) {
|
|
await entry.extData.loadValues();
|
|
}
|
|
return this.info.isUnique
|
|
? entry.values[0]
|
|
: entry.values;
|
|
}
|
|
else {
|
|
return null;
|
|
}
|
|
}
|
|
/**
|
|
* @param options `existingOnly`: Whether to only return lookup results for keys that were actually found
|
|
*/
|
|
async findAll(keys, options = { existingOnly: true }) {
|
|
return this._threadSafe('shared', () => this._findAll(keys, options));
|
|
}
|
|
async _findAll(keys, options = { existingOnly: true }) {
|
|
if (keys.length <= 2) {
|
|
const promises = keys.map(async (key) => {
|
|
const value = await this._find(key);
|
|
return { key, value };
|
|
});
|
|
const results = await Promise.all(promises);
|
|
return options.existingOnly
|
|
? results.filter(r => r.value !== null)
|
|
: results;
|
|
}
|
|
// Get upperbound
|
|
const lastLeaf = await this._getLastLeaf();
|
|
const lastEntry = lastLeaf.entries.slice(-1)[0];
|
|
const lastKey = lastEntry.key;
|
|
// Sort the keys
|
|
keys = keys.slice().sort();
|
|
if (keys[0] > lastKey) {
|
|
// First key to lookup is > lastKey, no need to lookup anything!
|
|
return options.existingOnly ? [] : keys.map(key => ({ key, value: null }));
|
|
}
|
|
// Get lowerbound
|
|
const firstLeaf = await this._getLastLeaf();
|
|
const firstEntry = firstLeaf.entries[0];
|
|
const firstKey = firstEntry.key;
|
|
if (keys.slice(-1)[0] < firstKey) {
|
|
// Last key to lookup is < firstKey, no need to lookup anything!
|
|
return options.existingOnly ? [] : keys.map(key => ({ key, value: null }));
|
|
}
|
|
// Some keys might be out of bounds, others must be looked up
|
|
const results = [], lookups = [];
|
|
for (let i = 0; i < keys.length; i++) {
|
|
const key = keys[i];
|
|
if ((0, typesafe_compare_1._isLess)(key, firstKey) || (0, typesafe_compare_1._isMore)(key, lastKey)) {
|
|
// Out of bounds, no need to lookup
|
|
options.existingOnly || results.push({ key, value: null });
|
|
}
|
|
else {
|
|
// Lookup
|
|
const promise = this._find(key).then(value => {
|
|
if (value !== null || !options.existingOnly) {
|
|
results.push({ key, value });
|
|
}
|
|
});
|
|
lookups.push(promise);
|
|
}
|
|
}
|
|
if (lookups.length > 0) {
|
|
await Promise.all(lookups);
|
|
}
|
|
return results;
|
|
}
|
|
async _growTree(bytesNeeded) {
|
|
if (!this.info) {
|
|
throw new NoTreeInfoError();
|
|
}
|
|
if (!this._autoGrow) {
|
|
throw new Error('Cannot grow tree - autoGrow not enabled');
|
|
}
|
|
const grow = bytesNeeded - this.info.freeSpace;
|
|
this.info.byteLength += grow;
|
|
this.info.freeSpace += grow;
|
|
await this._writeAllocationBytes(); // write
|
|
}
|
|
async writeAllocationBytes() {
|
|
return this._threadSafe('exclusive', () => this._writeAllocationBytes());
|
|
}
|
|
async _writeAllocationBytes() {
|
|
if (!this.info) {
|
|
throw new NoTreeInfoError();
|
|
}
|
|
await Promise.all([
|
|
// byte_length:
|
|
this._writeFn((0, binary_1.writeByteLength)([], 0, this.info.byteLength), 0),
|
|
// free_byte_length:
|
|
this._writeFn((0, binary_1.writeByteLength)([], 0, this.info.freeSpace), this.info.freeSpaceIndex),
|
|
]);
|
|
}
|
|
async _registerFreeSpace(index, length) {
|
|
if (!this.info) {
|
|
throw new NoTreeInfoError();
|
|
}
|
|
if (!this._fst) {
|
|
this._fst = [];
|
|
}
|
|
if (index + length === this.info.byteLength - this.info.freeSpace) {
|
|
// Cancel free space allocated at the end of the file
|
|
// console.log(`Freeing ${length} bytes from index ${index} (at end of file)`);
|
|
this.info.freeSpace += length;
|
|
await this._writeFn((0, binary_1.writeByteLength)([], 0, this.info.freeSpace), this.info.freeSpaceIndex); // free_byte_length
|
|
}
|
|
else {
|
|
// console.log(`Freeing ${length} bytes from index ${index} to ${index+length}`);
|
|
this._fst.push({ index, length });
|
|
// Normalize fst by joining adjacent blocks
|
|
this._fst.sort((a, b) => a.index < b.index ? -1 : 1);
|
|
let i = 0;
|
|
while (i + 1 < this._fst.length) {
|
|
const block = this._fst[i];
|
|
const next = this._fst[i + 1];
|
|
if (next.index === block.index + block.length) {
|
|
// Adjacent!
|
|
block.length += next.length; // Add space to this item
|
|
this._fst.splice(i + 1, 1); // Remove next
|
|
}
|
|
else {
|
|
i++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
async _claimFreeSpace(bytesRequired) {
|
|
if (!this.info) {
|
|
throw new NoTreeInfoError();
|
|
}
|
|
// if (bytesRequired === 0) { return Promise.reject(new Error('Claiming 0 bytes')); } // ALLOW this!
|
|
if (bytesRequired > this.info.freeSpace) {
|
|
throw new Error('Attempt to claim more bytes than available in trailing free space');
|
|
}
|
|
this.info.freeSpace -= bytesRequired;
|
|
await this._writeFn((0, binary_1.writeByteLength)([], 0, this.info.freeSpace), this.info.freeSpaceIndex);
|
|
}
|
|
async _requestFreeSpace(bytesRequired) {
|
|
if (!this.info) {
|
|
throw new NoTreeInfoError();
|
|
}
|
|
if (bytesRequired === 0) {
|
|
throw new Error('Requesting 0 bytes');
|
|
}
|
|
if (!this._fst) {
|
|
this._fst = [];
|
|
}
|
|
const available = this._fst.filter(block => block.length >= bytesRequired);
|
|
if (available.length > 0) {
|
|
const best = available.sort((a, b) => a.length < b.length ? -1 : 1)[0];
|
|
this._fst.splice(this._fst.indexOf(best), 1);
|
|
return best;
|
|
}
|
|
else {
|
|
// Check if there is not too much wasted space
|
|
const wastedSpace = this._fst.reduce((total, block) => total + block.length, 0);
|
|
const maxWaste = Math.round(this._originalByteLength * 0.5); // max 50% waste
|
|
if (wastedSpace > maxWaste) {
|
|
throw new Error('too much space being wasted. tree rebuild is needed');
|
|
}
|
|
if (this.info.freeSpace < bytesRequired) {
|
|
if (this.autoGrow) {
|
|
await this._growTree(bytesRequired);
|
|
}
|
|
else {
|
|
throw new detailed_error_1.DetailedError('tree-full-no-autogrow', `tree doesn't have ${bytesRequired} free bytes and autoGrow is not enabled`);
|
|
}
|
|
}
|
|
const index = this.info.byteLength - this.info.freeSpace;
|
|
this.info.freeSpace -= bytesRequired;
|
|
await this._writeFn((0, binary_1.writeByteLength)([], 0, this.info.freeSpace), this.info.freeSpaceIndex);
|
|
return { index, length: bytesRequired };
|
|
}
|
|
}
|
|
/**
|
|
*
|
|
* @param {BinaryBPlusTreeLeaf} leaf
|
|
* @param {object} options
|
|
* @param {boolean} [options.growData=false]
|
|
* @param {boolean} [options.growExtData=false]
|
|
* @param {(leaf: BinaryBPlusTreeLeaf) => any} [options.applyChanges] callback function to apply changes to leaf before writing
|
|
* @param {boolean} [options.rollbackOnFailure=true] Whether to rewrite the original leaf on failure (only done if this is a one leaf tree) - disable if this rebuild is called because of a failure to write an updated leaf (rollback will fail too!)
|
|
*/
|
|
async _rebuildLeaf(leaf, options = {
|
|
growData: false,
|
|
growExtData: false,
|
|
rollbackOnFailure: true,
|
|
prevLeaf: null,
|
|
nextLeaf: null,
|
|
}) {
|
|
// rebuild the leaf
|
|
// await this._testTree();
|
|
const newLeafExtDataLength = options.growExtData ? Math.ceil(leaf.extData.length * 1.1) : leaf.extData.length;
|
|
const extDataGrowth = newLeafExtDataLength - leaf.extData.length;
|
|
const newLeafLength = options.growData ? Math.ceil(leaf.length * 1.1) : leaf.length;
|
|
const leafGrows = options.growData || options.growExtData;
|
|
const bytesNeeded = newLeafLength + newLeafExtDataLength; //leafGrows ? newLeafLength + newLeafExtDataLength : 0;
|
|
if (!this.info) {
|
|
throw new NoTreeInfoError();
|
|
}
|
|
if (this.info.freeSpace < bytesNeeded && !options.growData && options.growExtData && leaf.free >= extDataGrowth) {
|
|
// ext_data must grow but can't because there is not enough free space to create a new leaf
|
|
// the leaf however has enough free space to shrink a bit for the ext_data
|
|
if (!leaf.extData.loaded) {
|
|
await leaf.extData.load();
|
|
}
|
|
leaf.length -= extDataGrowth;
|
|
leaf.free -= extDataGrowth;
|
|
leaf.extData.length += extDataGrowth;
|
|
leaf.extData.freeBytes += extDataGrowth;
|
|
// options.growExtData = false; // already done by stealing from leaf
|
|
// return this._rebuildLeaf(leaf, options);
|
|
options.applyChanges && options.applyChanges(leaf);
|
|
return this._writeLeaf(leaf);
|
|
}
|
|
// Read additional data needed to rebuild this leaf
|
|
const reads = [];
|
|
if (leaf.hasExtData) {
|
|
if (!leaf.extData.loaded) {
|
|
// Load all ext_data
|
|
reads.push(leaf.extData.load());
|
|
}
|
|
else if (!leafGrows) {
|
|
// We're done after rewriting this leaf
|
|
options.applyChanges && options.applyChanges(leaf);
|
|
return this._writeLeaf(leaf);
|
|
}
|
|
}
|
|
if (reads.length > 0) {
|
|
// Continue after all additional data has been loaded
|
|
await Promise.all(reads);
|
|
}
|
|
const oneLeafTree = !leaf.parentNode;
|
|
try {
|
|
let allocated;
|
|
if (oneLeafTree) {
|
|
const available = leaf.length + leaf.extData.length + this.info.freeSpace;
|
|
if (bytesNeeded < available) {
|
|
await this._claimFreeSpace(bytesNeeded - leaf.length - leaf.extData.length);
|
|
allocated = { index: leaf.index, length: bytesNeeded }; // overwrite leaf at same index
|
|
}
|
|
else if (this.autoGrow) {
|
|
const growBytes = bytesNeeded - available;
|
|
await this._growTree(growBytes);
|
|
allocated = { index: leaf.index, length: bytesNeeded };
|
|
}
|
|
else {
|
|
throw new Error('Not enough space to overwrite one leaf tree'); // not possible to overwrite
|
|
}
|
|
}
|
|
else {
|
|
allocated = await this._requestFreeSpace(bytesNeeded); // request free space
|
|
}
|
|
// Create new leaf
|
|
const newLeaf = new binary_tree_leaf_1.BinaryBPlusTreeLeaf({
|
|
isLeaf: true,
|
|
index: allocated.index,
|
|
length: allocated.length - newLeafExtDataLength,
|
|
hasExtData: leaf.hasExtData,
|
|
tree: leaf.tree,
|
|
});
|
|
newLeaf.prevLeafIndex = leaf.prevLeafIndex;
|
|
newLeaf.nextLeafIndex = leaf.nextLeafIndex;
|
|
newLeaf.entries = leaf.entries.map(entry => new binary_tree_leaf_entry_1.BinaryBPlusTreeLeafEntry(entry.key, entry.values.slice()));
|
|
if (leaf.hasExtData) {
|
|
newLeaf.extData = {
|
|
loaded: true,
|
|
length: newLeafExtDataLength,
|
|
freeBytes: leaf.extData.freeBytes + (newLeafExtDataLength - leaf.extData.length),
|
|
load: noop,
|
|
};
|
|
}
|
|
// Update indexes pointing to this leaf
|
|
if (leaf.parentEntry) {
|
|
leaf.parentEntry.ltChildIndex = newLeaf.index;
|
|
}
|
|
else if (leaf.parentNode) {
|
|
leaf.parentNode.gtChildIndex = newLeaf.index;
|
|
}
|
|
const freedBytes = leaf.length + leaf.extData.length;
|
|
// console.log(`Rebuilding leaf for entries "${leaf.entries[0].key}" to "${leaf.entries[leaf.entries.length-1].key}"`);
|
|
options.applyChanges && options.applyChanges(newLeaf);
|
|
// Start transaction
|
|
const tx = new tx_1.TX();
|
|
// Write new leaf:
|
|
tx.queue({
|
|
name: 'new leaf',
|
|
action: async () => {
|
|
const result = await this._writeLeaf(newLeaf);
|
|
// console.log(`new leaf for entries "${newLeaf.entries[0].key}" to "${newLeaf.entries.slice(-1)[0].key}" was written successfully at index ${newLeaf.index} (used to be at ${leaf.index})`);
|
|
// // TEST leaf
|
|
// const leaf = await this._findLeaf(newLeaf.entries[0].key);
|
|
// const promises = leaf.entries.filter(entry => entry.extData).map(entry => entry.extData.loadValues());
|
|
// await Promise.all(promises);
|
|
return `${result.length} leaf writes`;
|
|
},
|
|
rollback: async () => {
|
|
// release allocated space again
|
|
if (oneLeafTree) {
|
|
if (options.rollbackOnFailure === false) {
|
|
return;
|
|
}
|
|
return this._writeLeaf(leaf);
|
|
}
|
|
else {
|
|
return this._registerFreeSpace(allocated.index, allocated.length);
|
|
}
|
|
},
|
|
});
|
|
// Adjust previous leaf's next_leaf_ptr:
|
|
if (leaf.hasPrevious) {
|
|
const prevLeaf = {
|
|
nextPointerIndex: leaf.prevLeafIndex + binary_tree_leaf_1.BinaryBPlusTreeLeaf.nextLeafPtrIndex,
|
|
oldOffset: binary_tree_leaf_1.BinaryBPlusTreeLeaf.getNextLeafOffset(leaf.prevLeafIndex, leaf.index),
|
|
newOffset: binary_tree_leaf_1.BinaryBPlusTreeLeaf.getNextLeafOffset(leaf.prevLeafIndex, newLeaf.index),
|
|
};
|
|
tx.queue({
|
|
name: 'prev leaf next_leaf_ptr',
|
|
action: async () => {
|
|
const bytes = (0, binary_1.writeSignedOffset)([], 0, prevLeaf.newOffset, true);
|
|
return this._writeFn(bytes, prevLeaf.nextPointerIndex);
|
|
},
|
|
rollback: async () => {
|
|
const bytes = (0, binary_1.writeSignedOffset)([], 0, prevLeaf.oldOffset, true);
|
|
return this._writeFn(bytes, prevLeaf.nextPointerIndex);
|
|
},
|
|
});
|
|
}
|
|
// Adjust next leaf's prev_leaf_ptr:
|
|
if (leaf.hasNext) {
|
|
const nextLeaf = {
|
|
prevPointerIndex: leaf.nextLeafIndex + binary_tree_leaf_1.BinaryBPlusTreeLeaf.prevLeafPtrIndex,
|
|
oldOffset: binary_tree_leaf_1.BinaryBPlusTreeLeaf.getPrevLeafOffset(leaf.nextLeafIndex, leaf.index),
|
|
newOffset: binary_tree_leaf_1.BinaryBPlusTreeLeaf.getPrevLeafOffset(leaf.nextLeafIndex, newLeaf.index),
|
|
};
|
|
tx.queue({
|
|
name: 'next leaf prev_leaf_ptr',
|
|
action: async () => {
|
|
const bytes = (0, binary_1.writeSignedOffset)([], 0, nextLeaf.newOffset, true);
|
|
return this._writeFn(bytes, nextLeaf.prevPointerIndex);
|
|
},
|
|
rollback: async () => {
|
|
const bytes = (0, binary_1.writeSignedOffset)([], 0, nextLeaf.oldOffset, true);
|
|
return this._writeFn(bytes, nextLeaf.prevPointerIndex);
|
|
},
|
|
});
|
|
}
|
|
// Rewrite parent node
|
|
if (leaf.parentNode) {
|
|
tx.queue({
|
|
name: 'parent node',
|
|
action: async () => {
|
|
return this._writeNode(leaf.parentNode);
|
|
},
|
|
rollback: async () => {
|
|
// Set the target leaf indexes back to the originals
|
|
if (leaf.parentEntry) {
|
|
leaf.parentEntry.ltChildIndex = leaf.index;
|
|
}
|
|
else {
|
|
leaf.parentNode.gtChildIndex = leaf.index;
|
|
}
|
|
if (options.nextLeaf.parentNode === leaf.parentNode) {
|
|
if (options.nextLeaf.parentEntry) {
|
|
options.nextLeaf.parentEntry.ltChildIndex = options.nextLeaf.index;
|
|
}
|
|
else {
|
|
options.nextLeaf.parentNode.gtChildIndex = options.nextLeaf.index;
|
|
}
|
|
}
|
|
return this._writeNode(leaf.parentNode);
|
|
},
|
|
});
|
|
}
|
|
const results = await tx.execute(true);
|
|
if (!oneLeafTree) {
|
|
await this._registerFreeSpace(leaf.index, freedBytes);
|
|
}
|
|
// await this._testTree();
|
|
return results;
|
|
}
|
|
catch (err) {
|
|
throw new detailed_error_1.DetailedError('rebuild-leaf-failed', `Failed to rebuild leaf: ${err.message}`, err);
|
|
}
|
|
}
|
|
async _splitNode(node, options = { keepEntries: 0, cancelCallback: null }) {
|
|
// split node if it could not be written.
|
|
// There needs to be enough free space to store another leaf the size of current node,
|
|
// and the parent node must not be full.
|
|
if (typeof options.cancelCallback !== 'function') {
|
|
throw new Error('specify options.cancelCallback to undo any changes when a rollback needs to be performed');
|
|
}
|
|
try {
|
|
if (!node.parentNode) {
|
|
throw new detailed_error_1.DetailedError('cannot-split-top-level-node', 'Cannot split top-level node, tree rebuild is needed');
|
|
}
|
|
if (node.parentNode.entries.length >= this.info.entriesPerNode) {
|
|
// Split parent node before continuing
|
|
const { node1, node2 } = await this._splitNode(node.parentNode, { cancelCallback: noop });
|
|
// find out if this node is now a child of node1 or node2, update properties accordingly
|
|
const parentEntry1 = node1.entries.find(e => e.ltChildIndex === node.index);
|
|
const parentEntry2 = node2.entries.find(e => e.ltChildIndex === node.index);
|
|
if (parentEntry1) {
|
|
node.parentNode = node1;
|
|
node.parentEntry = parentEntry1;
|
|
}
|
|
else if (parentEntry2) {
|
|
node.parentNode = node2;
|
|
node.parentEntry = parentEntry2;
|
|
}
|
|
else if (node1.gtChildIndex === node.index) {
|
|
node.parentNode = node1;
|
|
node.parentEntry = null;
|
|
}
|
|
else if (node2.gtChildIndex === node.index) {
|
|
node.parentNode = node2;
|
|
node.parentEntry = null;
|
|
}
|
|
else {
|
|
throw new Error('DEV ERROR: new parent nodes do not reference this node');
|
|
}
|
|
}
|
|
if (typeof options.keepEntries !== 'number' || options.keepEntries === 0) {
|
|
options.keepEntries = Math.floor(node.entries.length / 2);
|
|
}
|
|
if (options.keepEntries > this.info.entriesPerNode - 2) {
|
|
options.keepEntries = this.info.entriesPerNode - 2; // 1 entry will move to the next node, 1 entry is removed as it becomes this node's gtChild
|
|
}
|
|
const newNodeLength = node.length; // Use same length as current node
|
|
const allocated = await this._requestFreeSpace(newNodeLength);
|
|
// console.log(`Splitting node "${node.entries[0].key}" to "${node.entries.slice(-1)[0].key}", cutting at "${movingEntries[0].key}"`);
|
|
// Create new node
|
|
const newNode = new binary_tree_node_1.BinaryBPlusTreeNode({
|
|
isLeaf: false,
|
|
length: newNodeLength,
|
|
index: allocated.index,
|
|
tree: node.tree,
|
|
});
|
|
// current node's gtChild becomes new node's gtChild
|
|
newNode.gtChildIndex = node.gtChildIndex;
|
|
// move entries
|
|
const movingEntries = node.entries.splice(options.keepEntries);
|
|
// First entry is not moving, it becomes original node's gtChild and new parent node entry key
|
|
const disappearingEntry = movingEntries.shift();
|
|
node.gtChildIndex = disappearingEntry.ltChildIndex;
|
|
// Add all other entries to new node
|
|
newNode.entries.push(...movingEntries);
|
|
// movingEntries.forEach(entry => {
|
|
// const childIndex = node.index + entry.ltChildOffset;
|
|
// const newEntry = new BinaryBPlusTreeNodeEntry(entry.key);
|
|
// newEntry.ltChildIndex = childIndex - newNode.index;
|
|
// newNode.entries.push(newEntry);
|
|
// });
|
|
// console.log(`Creating new node for ${movingEntries.length} entries`);
|
|
// Update parent node entry pointing to this node
|
|
const oldParentNode = new binary_tree_node_1.BinaryBPlusTreeNode({
|
|
isLeaf: false,
|
|
index: node.parentNode.index,
|
|
length: node.parentNode.length,
|
|
free: node.parentNode.free,
|
|
});
|
|
oldParentNode.gtChildIndex = node.parentNode.gtChildIndex;
|
|
oldParentNode.entries = node.parentNode.entries.map(entry => {
|
|
const newEntry = new binary_tree_node_entry_1.BinaryBPlusTreeNodeEntry(entry.key);
|
|
newEntry.ltChildIndex = entry.ltChildIndex;
|
|
return newEntry;
|
|
});
|
|
if (node.parentEntry !== null) {
|
|
// Current node is a parent node entry's ltChild
|
|
// eg: current node [10,11,12, ... ,18,19] is parent node [10,20,30] second entry's (20) ltChild.
|
|
// When splitting to [10, ..., 14] and [15, ..., 19], we have to add key 15 to parent: [10,15,20,30]
|
|
const newEntryKey = node.parentEntry.key; // (20 in above example)
|
|
node.parentEntry.key = disappearingEntry.key; // movingEntries[0].key; // (15 in above example)
|
|
// Add new node entry for created node
|
|
const insertIndex = node.parentNode.entries.indexOf(node.parentEntry) + 1;
|
|
const newNodeEntry = new binary_tree_node_entry_1.BinaryBPlusTreeNodeEntry(newEntryKey);
|
|
newNodeEntry.ltChildIndex = newNode.index; // Set the target index, so _writeNode knows it must calculate the target offset
|
|
node.parentNode.entries.splice(insertIndex, 0, newNodeEntry);
|
|
}
|
|
else {
|
|
// Current node is parent node's gtChild
|
|
const newNodeEntry = new binary_tree_node_entry_1.BinaryBPlusTreeNodeEntry(disappearingEntry.key); //new BinaryBPlusTreeNodeEntry(movingEntries[0].key);
|
|
newNodeEntry.ltChildIndex = node.index;
|
|
node.parentNode.entries.push(newNodeEntry);
|
|
node.parentNode.gtChildIndex = newNode.index;
|
|
}
|
|
// Start transaction
|
|
const tx = new tx_1.TX();
|
|
// Write new node:
|
|
tx.queue({
|
|
name: 'write new node',
|
|
action: async () => {
|
|
return this._writeNode(newNode);
|
|
},
|
|
rollback: async () => {
|
|
// Release allocated space again
|
|
return this._registerFreeSpace(allocated.index, allocated.length);
|
|
},
|
|
// No need to add rollback step to remove new node. It'll be overwritten later
|
|
});
|
|
// Rewrite this node:
|
|
tx.queue({
|
|
name: 'rewrite current node',
|
|
action: async () => {
|
|
return this._writeNode(node);
|
|
},
|
|
rollback: async () => {
|
|
node.entries.push(...movingEntries);
|
|
const p = options.cancelCallback();
|
|
if (p instanceof Promise) {
|
|
return p.then(() => {
|
|
return this._writeNode(node);
|
|
});
|
|
}
|
|
return this._writeNode(node);
|
|
},
|
|
});
|
|
// Rewrite parent node:
|
|
tx.queue({
|
|
name: 'rewrite parent node',
|
|
action: async () => {
|
|
return this._writeNode(node.parentNode);
|
|
},
|
|
rollback: async () => {
|
|
// this is the last step, we don't need to rollback if we are running the tx sequentially.
|
|
// Because we run parallel, we need rollback code here:
|
|
return this._writeNode(oldParentNode);
|
|
},
|
|
});
|
|
await tx.execute(true); // run parallel
|
|
// await this._testTree();
|
|
return { node1: node, node2: newNode };
|
|
}
|
|
catch (err) {
|
|
throw new detailed_error_1.DetailedError('split-node-failed', `Unable to split node: ${err.message}`, err);
|
|
}
|
|
}
|
|
async _splitLeaf(leaf, options = { nextLeaf: null, keepEntries: 0, cancelCallback: null }) {
|
|
// split leaf if it could not be written.
|
|
// console.log('splitLeaf');
|
|
// There needs to be enough free space to store another leaf the size of current leaf
|
|
if (typeof options.cancelCallback !== 'function') {
|
|
throw new Error('specify options.cancelCallback to undo any changes when a rollback needs to be performed');
|
|
}
|
|
if (leaf.parentNode.entries.length >= this.info.entriesPerNode) {
|
|
// TODO: TEST splitting node
|
|
// throw new DetailedError('parent-node-full', `Cannot split leaf because parent node is full`);
|
|
// NEW: split parent node!
|
|
const { node1, node2 } = await this._splitNode(leaf.parentNode, { keepEntries: options.keepEntries, cancelCallback: noop });
|
|
// find out if leaf is now a child of node1 or node2, update properties and try again
|
|
const parentEntry1 = node1.entries.find(e => e.ltChildIndex === leaf.index); // node1.entries.find(e => node1.index + e.ltChildOffset === leaf.index); //node1.entries.find(entry => entry === node.parentEntry); //
|
|
const parentEntry2 = node2.entries.find(e => e.ltChildIndex === leaf.index); // node1.entries.find(e => node1.index + e.ltChildOffset === leaf.index); //node1.entries.find(entry => entry === node.parentEntry); //
|
|
if (parentEntry1) {
|
|
leaf.parentNode = node1;
|
|
leaf.parentEntry = parentEntry1;
|
|
}
|
|
else if (parentEntry2) {
|
|
leaf.parentNode = node2;
|
|
leaf.parentEntry = parentEntry2;
|
|
}
|
|
else if (node1.gtChildIndex === leaf.index) {
|
|
leaf.parentNode = node1;
|
|
leaf.parentEntry = null;
|
|
}
|
|
else if (node2.gtChildIndex === leaf.index) {
|
|
leaf.parentNode = node2;
|
|
leaf.parentEntry = null;
|
|
}
|
|
else {
|
|
throw new Error('DEV ERROR: new parent nodes have no reference this leaf');
|
|
// if (leaf.entries[0].key <= node2.entries[node2.entries.length-1].key) {
|
|
// throw new Error(`DEV ERROR: Leaf's first entry key (${leaf.entries[0].key}) <= node2's last entry key ${node2.entries[node2.entries.length-1].key}`);
|
|
// }
|
|
}
|
|
}
|
|
if (typeof options.keepEntries !== 'number' || options.keepEntries === 0) {
|
|
options.keepEntries = leaf.hasNext
|
|
? Math.floor(leaf.entries.length / 2) // Split leaf entries into 2 equal parts
|
|
: Math.floor(this.info.entriesPerNode * (this.info.fillFactor / 100)); // No next leaf, split at fill factor
|
|
}
|
|
// Check if additional data has to be loaded before proceeding
|
|
const reads = [];
|
|
if (!options.nextLeaf && leaf.hasNext) {
|
|
// Load next leaf first
|
|
reads.push(leaf.getNext()
|
|
.then(nextLeaf => {
|
|
options.nextLeaf = nextLeaf;
|
|
}));
|
|
}
|
|
if (leaf.hasExtData && !leaf.extData.loaded) {
|
|
// load all ext_data before proceeding with split
|
|
reads.push(leaf.extData.load());
|
|
}
|
|
if (reads.length > 0) {
|
|
await Promise.all(reads);
|
|
}
|
|
try {
|
|
const movingEntries = leaf.entries.slice(options.keepEntries);
|
|
// const movingExtDataLength = movingEntry.extData ? Math.ceil((movingEntry.extData.length - movingEntry.extData.freeBytes) * 1.1) : 0;
|
|
// const movingExtDataLength = Math.ceil(movingEntries.reduce((length, entry) => {
|
|
// return length + (entry.extData ? entry.extData.length + 8 - entry.extData.freeBytes : 0);
|
|
// }, 0) / movingEntries.length * this.info.entriesPerNode);
|
|
const extDataLengths = leaf.entries
|
|
.filter(entry => entry.extData)
|
|
.map(entry => entry.extData.length + 8 - entry.extData.freeBytes);
|
|
const avgExtDataLength = extDataLengths.length === 0 ? 0 : extDataLengths.reduce((total, length) => total + length, 0) / extDataLengths.length;
|
|
//const movingExtDataLength = Math.ceil(avgExtDataLength * movingEntries.length);
|
|
const movingExtDataLength = movingEntries.reduce((total, entry) => total + (entry.extData ? entry.extData.length + 8 - entry.extData.freeBytes : 0), 0);
|
|
const newLeafExtDataLength = Math.ceil(avgExtDataLength * this.info.entriesPerNode); //Math.ceil(movingExtDataLength * 1.1);
|
|
const newLeafLength = leaf.length; // Use same length as current leaf
|
|
const allocated = await this._requestFreeSpace(newLeafLength + newLeafExtDataLength);
|
|
// console.log(`Splitting leaf "${leaf.entries[0].key}" to "${leaf.entries.slice(-1)[0].key}", cutting at "${movingEntries[0].key}"`);
|
|
const nextLeaf = options.nextLeaf;
|
|
// Create new leaf
|
|
const newLeaf = new binary_tree_leaf_1.BinaryBPlusTreeLeaf({
|
|
isLeaf: true,
|
|
length: newLeafLength,
|
|
index: allocated.index,
|
|
tree: leaf.tree,
|
|
hasExtData: newLeafExtDataLength > 0,
|
|
});
|
|
if (newLeafExtDataLength > 0) {
|
|
newLeaf.extData = {
|
|
loaded: true,
|
|
length: newLeafExtDataLength,
|
|
freeBytes: newLeafExtDataLength - movingExtDataLength,
|
|
load: noop,
|
|
};
|
|
}
|
|
// Adjust free space length and prev & next offsets
|
|
// this.info.freeSpace -= newLeafLength + newLeafExtDataLength;
|
|
newLeaf.prevLeafIndex = leaf.index;
|
|
newLeaf.nextLeafIndex = nextLeaf ? nextLeaf.index : 0;
|
|
leaf.nextLeafIndex = newLeaf.index;
|
|
if (nextLeaf) {
|
|
nextLeaf.prevLeafIndex = newLeaf.index;
|
|
}
|
|
// move entries
|
|
leaf.entries.splice(-movingEntries.length);
|
|
newLeaf.entries.push(...movingEntries);
|
|
// console.log(`Creating new leaf for ${movingEntries.length} entries`);
|
|
// Update parent node entry pointing to this leaf
|
|
const oldParentNode = new binary_tree_node_1.BinaryBPlusTreeNode({
|
|
isLeaf: false,
|
|
index: leaf.parentNode.index,
|
|
length: leaf.parentNode.length,
|
|
free: leaf.parentNode.free,
|
|
});
|
|
oldParentNode.gtChildIndex = leaf.parentNode.gtChildIndex;
|
|
oldParentNode.entries = leaf.parentNode.entries.map(entry => {
|
|
const newEntry = new binary_tree_node_entry_1.BinaryBPlusTreeNodeEntry(entry.key);
|
|
newEntry.ltChildIndex = entry.ltChildIndex;
|
|
return newEntry;
|
|
});
|
|
if (leaf.parentEntry !== null) {
|
|
// Current leaf is a parent node entry's ltChild
|
|
// eg: current leaf [10,11,12, ... ,18,19] is parent node [10,20,30] second entry's (20) ltChild.
|
|
// When splitting to [10, ..., 14] and [15, ..., 19], we have to add key 15 to parent: [10,15,20,30]
|
|
const newEntryKey = leaf.parentEntry.key; // (20 in above example)
|
|
leaf.parentEntry.key = movingEntries[0].key; // (15 in above example)
|
|
// Add new node entry for created leaf
|
|
const insertIndex = leaf.parentNode.entries.indexOf(leaf.parentEntry) + 1;
|
|
const newNodeEntry = new binary_tree_node_entry_1.BinaryBPlusTreeNodeEntry(newEntryKey);
|
|
newNodeEntry.ltChildIndex = newLeaf.index; // Set the target index, so _writeNode knows it must calculate the target offset
|
|
leaf.parentNode.entries.splice(insertIndex, 0, newNodeEntry);
|
|
}
|
|
else {
|
|
// Current leaf is parent node's gtChild
|
|
const newNodeEntry = new binary_tree_node_entry_1.BinaryBPlusTreeNodeEntry(movingEntries[0].key);
|
|
newNodeEntry.ltChildIndex = leaf.index;
|
|
leaf.parentNode.entries.push(newNodeEntry);
|
|
leaf.parentNode.gtChildIndex = newLeaf.index;
|
|
}
|
|
// Start transaction
|
|
const tx = new tx_1.TX();
|
|
// Write new leaf:
|
|
tx.queue({
|
|
name: 'write new leaf',
|
|
action: async () => {
|
|
return this._writeLeaf(newLeaf);
|
|
},
|
|
rollback: async () => {
|
|
// Release allocated space again
|
|
return this._registerFreeSpace(allocated.index, allocated.length);
|
|
},
|
|
// No need to add rollback step to remove new leaf. It'll be overwritten later
|
|
});
|
|
// Rewrite next leaf:
|
|
nextLeaf && tx.queue({
|
|
name: 'rewrite next leaf',
|
|
action: async () => {
|
|
return this._writeLeaf(nextLeaf);
|
|
},
|
|
rollback: async () => {
|
|
nextLeaf.prevLeafIndex = leaf.index;
|
|
return this._writeLeaf(nextLeaf);
|
|
},
|
|
});
|
|
// Rewrite this leaf:
|
|
tx.queue({
|
|
name: 'rewrite current leaf',
|
|
action: async () => {
|
|
return this._writeLeaf(leaf);
|
|
},
|
|
rollback: async () => {
|
|
leaf.entries.push(...movingEntries);
|
|
leaf.nextLeafIndex = nextLeaf ? nextLeaf.index : 0;
|
|
await options.cancelCallback(); // await in case cancelCallback returns a promise
|
|
return this._writeLeaf(leaf);
|
|
},
|
|
});
|
|
// Rewrite parent node:
|
|
tx.queue({
|
|
name: 'rewrite parent node',
|
|
action: async () => {
|
|
return this._writeNode(leaf.parentNode);
|
|
// TODO: If node grew larger than allocated size, try rebuilding it.
|
|
},
|
|
rollback: async () => {
|
|
// this is the last step, we don't need to rollback if we are running the tx sequentially.
|
|
// Because we run parallel, we need rollback code here:
|
|
return this._writeNode(oldParentNode);
|
|
},
|
|
});
|
|
const results = await tx.execute(true); // run parallel
|
|
// await this._testTree();
|
|
return results;
|
|
}
|
|
catch (err) {
|
|
throw new detailed_error_1.DetailedError('split-leaf-failed', `Unable to split leaf: ${err.message}`, err);
|
|
}
|
|
}
|
|
// async _testTree() {
|
|
// // Test tree by looking up all entries individually
|
|
// let leaf = await this._getFirstLeaf();
|
|
// const keys = leaf.entries.map(e => e.key);
|
|
// while (leaf.hasNext) {
|
|
// leaf = await leaf.getNext();
|
|
// keys.push(...leaf.entries.map(e => e.key));
|
|
// }
|
|
// console.warn(`TREE TEST: testing ${keys.length} keys`);
|
|
// // console.warn(keys);
|
|
// for (let i = 0; i < keys.length - 1; i++) {
|
|
// const key1 = keys[i], key2 = keys[i + 1];
|
|
// console.assert(_isLess(key1, key2), `Key "${key1}" must be smaller than "${key2}"`);
|
|
// }
|
|
// for (let i = 0; i < keys.length; i++) {
|
|
// const key = keys[i];
|
|
// leaf = await this._findLeaf(key);
|
|
// const entry = leaf?.entries.find(e => e.key === key)
|
|
// console.assert(entry, `Key "${key}" must be in leaf`);
|
|
// }
|
|
// console.warn(`TREE TEST: testing ext_data`);
|
|
// leaf = await this._getFirstLeaf();
|
|
// while (leaf) {
|
|
// if (leaf.hasExtData) {
|
|
// const leafExtDataIndex = leaf.sourceIndex + leaf.length;
|
|
// const endIndex = leafExtDataIndex + leaf.extData.length - leaf.extData.freeBytes;
|
|
// const testEntries = leaf.entries.filter(entry => entry.extData);
|
|
// for (const entry of testEntries) {
|
|
// // if (!entry.extData.loaded) {}
|
|
// await entry.extData.loadHeader();
|
|
// if (entry.extData.index + entry.extData.length > endIndex) {
|
|
// throw new Error(`TREE TEST FAILED: ext_block is larger than allowed (in ext_data free space that starts at index ${endIndex})`);
|
|
// }
|
|
// }
|
|
// try {
|
|
// await leaf.extData.load();
|
|
// }
|
|
// catch (err) {
|
|
// throw new Error(`TREE TEST FAILED: could not load leaf extData. ext_data free space starts at index ${endIndex}`, err.message);
|
|
// }
|
|
// }
|
|
// leaf = leaf.hasNext ? await leaf.getNext() : null;
|
|
// }
|
|
// console.warn(`TREE TEST SUCCESSFUL`);
|
|
// }
|
|
async add(key, recordPointer, metadata) {
|
|
return this._threadSafe('exclusive', () => this._add(key, recordPointer, metadata));
|
|
}
|
|
async _add(key, recordPointer, metadata) {
|
|
if (!this.info) {
|
|
await this._loadInfo();
|
|
}
|
|
const err = (0, utils_1._checkNewEntryArgs)(key, recordPointer, this.info.metadataKeys, metadata);
|
|
if (err) {
|
|
throw err;
|
|
}
|
|
const entryValue = new binary_tree_leaf_entry_value_1.BinaryBPlusTreeLeafEntryValue(recordPointer, metadata);
|
|
if (!this.id) {
|
|
throw new detailed_error_1.DetailedError('tree-id-not-set', 'To edit tree, set the id property to something unique for locking purposes');
|
|
}
|
|
try {
|
|
const leaf = await this._findLeaf(key);
|
|
if (!this.info.hasLargePtrs) {
|
|
throw new detailed_error_1.DetailedError('small-ptrs-deprecated', 'small ptrs have deprecated, tree will have to be rebuilt');
|
|
}
|
|
const entryIndex = leaf.entries.findIndex(entry => (0, typesafe_compare_1._isEqual)(key, entry.key));
|
|
let addNew = false;
|
|
if (this.info.isUnique) {
|
|
// Make sure key doesn't exist yet
|
|
if (entryIndex >= 0) {
|
|
throw new detailed_error_1.DetailedError('unique-key-violation', `Cannot add duplicate key "${key}": tree expects unique keys`);
|
|
}
|
|
addNew = true;
|
|
}
|
|
else {
|
|
if (entryIndex >= 0) {
|
|
const entry = leaf.entries[entryIndex];
|
|
if (entry.extData) {
|
|
try {
|
|
return await entry.extData.addValue(recordPointer, metadata);
|
|
}
|
|
catch (err) {
|
|
// Something went wrong adding the value. ext_data_block is probably full
|
|
// and needs to grow
|
|
// console.log(`Leaf rebuild necessary - unable to add value to key "${key}": ${err.message}`);
|
|
if (err.code !== 'max-extdata-size-reached') {
|
|
throw err;
|
|
}
|
|
const rebuildOptions = {
|
|
growData: false,
|
|
growExtData: true,
|
|
applyChanges: (leaf) => {
|
|
const entry = leaf.entries.find(entry => (0, typesafe_compare_1._isEqual)(entry.key, key)); //[entryIndex];
|
|
entry.values.push(new binary_tree_leaf_entry_value_1.BinaryBPlusTreeLeafEntryValue(recordPointer, metadata));
|
|
},
|
|
};
|
|
return await this._rebuildLeaf(leaf, rebuildOptions);
|
|
}
|
|
}
|
|
entry.values.push(entryValue);
|
|
}
|
|
else {
|
|
addNew = true;
|
|
}
|
|
}
|
|
if (!addNew) {
|
|
try {
|
|
return await this._writeLeaf(leaf);
|
|
}
|
|
catch (err) {
|
|
// Leaf got too small? Try rebuilding it
|
|
const extDataError = detailed_error_1.DetailedError.hasErrorCode(err, 'max-extdata-size-reached'); //leaf.hasExtData && err.message.match(/ext_data/) !== null;
|
|
try {
|
|
return await this._rebuildLeaf(leaf, {
|
|
growData: !extDataError,
|
|
growExtData: extDataError,
|
|
rollbackOnFailure: false, // Disable original leaf rewriting on failure
|
|
});
|
|
}
|
|
catch (err) {
|
|
throw new detailed_error_1.DetailedError('add-value-failed', `Can't add value to key '${key}': ${err.message}`, err);
|
|
}
|
|
}
|
|
}
|
|
// If we get here, we have to add a new leaf entry
|
|
const entry = new binary_tree_leaf_entry_1.BinaryBPlusTreeLeafEntry(key, [entryValue]);
|
|
// Insert it
|
|
const insertBeforeIndex = leaf.entries.findIndex(entry => (0, typesafe_compare_1._isMore)(entry.key, key));
|
|
const isLastEntry = insertBeforeIndex === -1;
|
|
if (isLastEntry) {
|
|
leaf.entries.push(entry);
|
|
}
|
|
else {
|
|
leaf.entries.splice(insertBeforeIndex, 0, entry);
|
|
}
|
|
if (leaf.entries.length <= this.info.entriesPerNode) {
|
|
try {
|
|
return await this._writeLeaf(leaf);
|
|
}
|
|
catch (err) {
|
|
if (!detailed_error_1.DetailedError.hasErrorCode(err, 'max-leaf-size-reached')) {
|
|
throw err;
|
|
}
|
|
// Leaf had no space left, try rebuilding it
|
|
return await this._rebuildLeaf(leaf, {
|
|
growData: true,
|
|
growExtData: leaf.hasExtData,
|
|
rollbackOnFailure: false, // Don't try rewriting updated leaf on failure
|
|
});
|
|
}
|
|
}
|
|
// If we get here, our leaf has too many entries
|
|
const undoAdd = () => {
|
|
const index = leaf.entries.indexOf(entry);
|
|
index >= 0 && leaf.entries.splice(index, 1);
|
|
};
|
|
if (!leaf.parentNode) {
|
|
// No parent, so this is a 1 leaf "tree"
|
|
undoAdd();
|
|
throw new detailed_error_1.DetailedError('slt-no-space-available', `Cannot add key "${key}", no space left in single leaf tree`);
|
|
}
|
|
// Split leaf
|
|
return await this._splitLeaf(leaf, { cancelCallback: undoAdd, keepEntries: isLastEntry ? this.info.entriesPerNode : 0 });
|
|
}
|
|
catch (err) {
|
|
throw new detailed_error_1.DetailedError('add-key-failed', `Can't add key '${key}': ${err.message}`, err);
|
|
}
|
|
// .then(() => {
|
|
// // TEST the tree adjustments by getting the leaf with the added key,
|
|
// // and then previous and next leafs!
|
|
// console.warn(`TESTING leaf adjustment after adding "${key}". Remove code when all is well!`);
|
|
// return this._findLeaf(key);
|
|
// })
|
|
// .then(leaf => {
|
|
// let promises = leaf.entries.map(entry => {
|
|
// if (entry.extData) {
|
|
// return entry.extData.loadValues();
|
|
// }
|
|
// return null;
|
|
// })
|
|
// .filter(p => p !== null);
|
|
// return Promise.all(promises);
|
|
// // return leaf.hasExtData && leaf.extData.load();
|
|
// });
|
|
// .then(leaf => {
|
|
// let prev = leaf.getPrevious ? leaf.getPrevious() : null;
|
|
// let next = leaf.getNext ? leaf.getNext() : null;
|
|
// return Promise.all([leaf, prev, next]);
|
|
// })
|
|
// .then(results => {
|
|
// let leaf = results[0];
|
|
// let prev = results[1];
|
|
// let next = results[2];
|
|
// });
|
|
}
|
|
// /**
|
|
// * @param {BinaryBPlusTreeTransactionOperation[]} operations
|
|
// */
|
|
// async process(operations) {
|
|
// return this._threadSafe('exclusive', () => this._process(operations));
|
|
// }
|
|
async _process(operations) {
|
|
if (!this.info) {
|
|
await this._loadInfo();
|
|
}
|
|
if (!this.info.isUnique) {
|
|
throw new detailed_error_1.DetailedError('non-unique-tree', 'DEV ERROR: process() should not be called on non-unique trees because of ext_data complexity, cannot handle that yet. Use old "transaction" logic instead');
|
|
}
|
|
if (operations.length === 0) {
|
|
return;
|
|
}
|
|
operations.filter(op => op.type === 'add').forEach(({ key, recordPointer, metadata }) => {
|
|
const err = (0, utils_1._checkNewEntryArgs)(key, recordPointer, this.info.metadataKeys, metadata); // Fixed this.metadataKeys issue during TS port
|
|
if (err) {
|
|
throw err;
|
|
}
|
|
});
|
|
operations.filter(op => op.type === 'update').forEach(({ key, newValue }) => {
|
|
const err = (0, utils_1._checkNewEntryArgs)(key, newValue.recordPointer, this.info.metadataKeys, newValue.metadata); // Fixed this.metadataKeys issue during TS port
|
|
if (err) {
|
|
throw err;
|
|
}
|
|
});
|
|
if (!this.info.hasLargePtrs) {
|
|
throw new detailed_error_1.DetailedError('small-ptrs-deprecated', 'small ptrs have deprecated, tree will have to be rebuilt');
|
|
}
|
|
let batchedOps = [];
|
|
// const debugRemoved = [];
|
|
// let debugThrownError;
|
|
try {
|
|
// Sort the entries
|
|
operations.sort((a, b) => (0, typesafe_compare_1._isLess)(a.key, b.key) ? -1 : 1);
|
|
// Get first leaf to edit
|
|
let leaf = await this._findLeaf(operations[0].key);
|
|
let undo = [];
|
|
const saveLeaf = async () => {
|
|
// debugRemoved.forEach(r => {
|
|
// if (leaf.entries.find(e => e.key === r.key)) {
|
|
// debugger;
|
|
// }
|
|
// });
|
|
if (leaf.entries.length > this.info.entriesPerNode) {
|
|
// Leaf too large to save, must split
|
|
const cancelCallback = () => undo.splice(0).reverse().forEach(fn => fn());
|
|
const keepEntries = leaf.hasNext ? 0 : this.info.entriesPerNode;
|
|
// console.log('*process _splitLeaf');
|
|
await this._splitLeaf(leaf, { cancelCallback, keepEntries });
|
|
}
|
|
else if (leaf.entries.length > 0 || !leaf.parentNode) {
|
|
// Leaf has entries or is a single-leaf tree
|
|
try {
|
|
// console.log('*process _writeLeaf');
|
|
await this._writeLeaf(leaf);
|
|
}
|
|
catch (err) {
|
|
// Leaf had no space left, try rebuilding it with more space
|
|
// console.log('*process _rebuildLeaf');
|
|
await this._rebuildLeaf(leaf, {
|
|
growData: true,
|
|
growExtData: true,
|
|
rollbackOnFailure: false, // Don't try rewriting updated leaf on failure
|
|
});
|
|
}
|
|
}
|
|
else if (leaf.parentNode.entries.length > 1) {
|
|
// Remove leaf
|
|
// console.log('*process _removeLeaf');
|
|
await this._removeLeaf(leaf);
|
|
}
|
|
else {
|
|
// Parent node has only 1 entry, removing it would also make parent node empty...
|
|
throw new detailed_error_1.DetailedError('leaf-empty', 'leaf is now empty and parent node has only 1 entry, tree will have to be rebuilt');
|
|
}
|
|
};
|
|
while (operations.length > 0) {
|
|
const op = operations.shift();
|
|
// tx.queue({
|
|
// name: 'start',
|
|
// action() { operations.shift(); },
|
|
// rollback() {operations.unshift(op); }
|
|
// })
|
|
const { type, key, recordPointer, metadata, newValue, currentValue } = op;
|
|
// Should this entry be added to this leaf?
|
|
const applyToThisLeaf = (() => {
|
|
if (leaf.entries.length > this.info.entriesPerNode) {
|
|
return false;
|
|
}
|
|
// Check if the "roadsigns" in parent nodes will point to this leaf for the new key
|
|
const pointsThisDirection = (node) => {
|
|
if (node.parentEntry) {
|
|
// Parent node's entry has a less than connection to this node/leaf
|
|
return (0, typesafe_compare_1._isLess)(key, node.parentEntry.key);
|
|
}
|
|
else if (node.parentNode) {
|
|
// Parent node's "greater than" pointer goes to this node/leaf.
|
|
if (!(0, typesafe_compare_1._isMoreOrEqual)(key, node.parentNode.entries.slice(-1)[0].key)) {
|
|
return false; // Does this ever happen?
|
|
}
|
|
// Check resursively
|
|
return pointsThisDirection(node.parentNode);
|
|
}
|
|
else {
|
|
// There is no parent, this is the gtChild
|
|
if (!(0, typesafe_compare_1._isMoreOrEqual)(key, node.entries.slice(-1)[0].key)) {
|
|
throw new Error('DEV ERROR: this tree is not right..');
|
|
}
|
|
return true;
|
|
}
|
|
};
|
|
return pointsThisDirection(leaf);
|
|
})();
|
|
if (!applyToThisLeaf) {
|
|
// No. Save leaf edits and load a new one
|
|
// try {
|
|
await saveLeaf();
|
|
// }
|
|
// catch (err) {
|
|
// failedOps.push(...batchedOps);
|
|
// }
|
|
// Load new leaf
|
|
batchedOps = [];
|
|
undo = [];
|
|
leaf = await this._findLeaf(key);
|
|
}
|
|
batchedOps.push(op);
|
|
// Make adjustment to leaf
|
|
const entryIndex = leaf.entries.findIndex(entry => (0, typesafe_compare_1._isEqual)(key, entry.key));
|
|
const entry = leaf.entries[entryIndex];
|
|
if (type === 'remove') {
|
|
// Remove an entry
|
|
if (!entry) {
|
|
throw new detailed_error_1.DetailedError('key-not-found', `Cannot remove key "${key}" because it is not present in the tree`);
|
|
// continue; // Entry not in leaf, nothing changes
|
|
}
|
|
else {
|
|
// debugRemoved.push(entry);
|
|
leaf.entries.splice(entryIndex, 1);
|
|
undo.push(() => {
|
|
// console.log(`Undo remove ${entry.key}`);
|
|
leaf.entries.splice(entryIndex, 0, entry);
|
|
});
|
|
// if (entryIndex === 0 && !leaf.parentEntry) {
|
|
// // Somehow the entry is not removed in this case. DEBUG!
|
|
// const checkEntry = leaf.entries.find(e => _isEqual(key, e.key));
|
|
// debugger;
|
|
// }
|
|
}
|
|
}
|
|
else if (type === 'add') {
|
|
if (entry) {
|
|
throw new detailed_error_1.DetailedError('unique-key-violation', `Cannot add duplicate key "${key}": tree expects unique keys`);
|
|
}
|
|
else {
|
|
// Add new entry
|
|
const value = new binary_tree_leaf_entry_value_1.BinaryBPlusTreeLeafEntryValue(recordPointer, metadata);
|
|
const entry = new binary_tree_leaf_entry_1.BinaryBPlusTreeLeafEntry(key, [value]);
|
|
const insertBeforeIndex = leaf.entries.findIndex(entry => (0, typesafe_compare_1._isMore)(entry.key, key));
|
|
const isLastEntry = insertBeforeIndex === -1;
|
|
if (isLastEntry) {
|
|
leaf.entries.push(entry);
|
|
}
|
|
else {
|
|
leaf.entries.splice(insertBeforeIndex, 0, entry);
|
|
}
|
|
undo.push(() => leaf.entries.splice(leaf.entries.indexOf(entry), 1));
|
|
}
|
|
}
|
|
else if (type === 'update') {
|
|
if (!entry) {
|
|
throw new detailed_error_1.DetailedError('key-not-found', `Cannot update key "${key}" because it is not present in the tree`);
|
|
}
|
|
else {
|
|
// const currentValue = entry.values[0];
|
|
// const newValue = new BinaryBPlusTreeLeafEntryValue(recordPointer, metadata);
|
|
entry.values[0] = newValue;
|
|
undo.push(() => entry.values[0] = currentValue);
|
|
}
|
|
}
|
|
}
|
|
if (batchedOps.length > 0) {
|
|
await saveLeaf();
|
|
}
|
|
// batchedOps = [];
|
|
}
|
|
catch (err) {
|
|
operations.push(...batchedOps);
|
|
// debugThrownError = err;
|
|
throw err; //new DetailedError('process-error', 'Could not process all requested operations', err);
|
|
}
|
|
// finally {
|
|
// // await this._testTree();
|
|
// for (let removedEntry of debugRemoved) {
|
|
// const leaf = await this._findLeaf(removedEntry.key);
|
|
// if (leaf.entries.find(e => _isEqual(e.key, removedEntry.key))) {
|
|
// console.log(debugThrownError);
|
|
// debugger;
|
|
// }
|
|
// }
|
|
// }
|
|
}
|
|
async remove(key, recordPointer) {
|
|
return this._threadSafe('exclusive', () => this._remove(key, recordPointer));
|
|
}
|
|
async _remove(key, recordPointer) {
|
|
// key = _normalizeKey(key); //if (_isIntString(key)) { key = parseInt(key); }
|
|
try {
|
|
const leaf = await this._findLeaf(key);
|
|
// This is the leaf the key should be in
|
|
if (!this.info.hasLargePtrs) {
|
|
throw new detailed_error_1.DetailedError('small-ptrs-deprecated', 'small ptrs have deprecated, tree will have to be rebuilt');
|
|
}
|
|
const entryIndex = leaf.entries.findIndex(entry => (0, typesafe_compare_1._isEqual)(key, entry.key));
|
|
if (!~entryIndex) {
|
|
return;
|
|
}
|
|
if (this.info.isUnique || typeof recordPointer === 'undefined' || leaf.entries[entryIndex].totalValues === 1) {
|
|
leaf.entries.splice(entryIndex, 1);
|
|
}
|
|
else if (leaf.entries[entryIndex].extData) {
|
|
return leaf.entries[entryIndex].extData.removeValue(recordPointer);
|
|
}
|
|
else {
|
|
const valueIndex = leaf.entries[entryIndex].values.findIndex(val => (0, typesafe_compare_1._compareBinary)(val.recordPointer, recordPointer));
|
|
if (!~valueIndex) {
|
|
return;
|
|
}
|
|
leaf.entries[entryIndex].values.splice(valueIndex, 1);
|
|
}
|
|
if (leaf.parentNode && leaf.entries.length === 0) {
|
|
// This is not a single leaf tree, and the leaf is now empty. Remove it
|
|
if (leaf.parentNode.entries.length === 1) {
|
|
// Parent node has only 1 entry, removing it would also make parent node empty...
|
|
throw new detailed_error_1.DetailedError('leaf-empty', 'leaf is now empty and parent node has only 1 entry, tree will have to be rebuilt');
|
|
}
|
|
return await this._removeLeaf(leaf);
|
|
}
|
|
return await this._writeLeaf(leaf);
|
|
}
|
|
catch (err) {
|
|
throw new detailed_error_1.DetailedError('remove-key-failed', `Can't remove key '${key}': ${err.message}`, err);
|
|
}
|
|
}
|
|
/**
|
|
* Removes an empty leaf
|
|
*/
|
|
async _removeLeaf(leaf) {
|
|
try {
|
|
console.assert(leaf.parentNode && leaf.parentNode.entries.length >= 2, 'Leaf to remove must have a parent node with at least 2 entries'); // TODO: implement _removeNode
|
|
console.assert(leaf.entries.length === 0, 'Leaf to remove must be empty');
|
|
const freedBytes = leaf.length + leaf.extData.length;
|
|
// Start transaction
|
|
const tx = new tx_1.TX();
|
|
// Adjust previous leaf's next_leaf_ptr: (point it to leaf's next leaf)
|
|
if (leaf.hasPrevious) {
|
|
const prevLeaf = {
|
|
nextPointerIndex: leaf.prevLeafIndex + binary_tree_leaf_1.BinaryBPlusTreeLeaf.nextLeafPtrIndex,
|
|
oldOffset: binary_tree_leaf_1.BinaryBPlusTreeLeaf.getNextLeafOffset(leaf.prevLeafIndex, leaf.index),
|
|
newOffset: binary_tree_leaf_1.BinaryBPlusTreeLeaf.getNextLeafOffset(leaf.prevLeafIndex, leaf.nextLeafIndex),
|
|
};
|
|
tx.queue({
|
|
name: 'prev leaf next_leaf_ptr',
|
|
action: async () => {
|
|
const bytes = (0, binary_1.writeSignedOffset)([], 0, prevLeaf.newOffset, true);
|
|
return this._writeFn(bytes, prevLeaf.nextPointerIndex);
|
|
},
|
|
rollback: async () => {
|
|
const bytes = (0, binary_1.writeSignedOffset)([], 0, prevLeaf.oldOffset, true);
|
|
return this._writeFn(bytes, prevLeaf.nextPointerIndex);
|
|
},
|
|
});
|
|
}
|
|
// Adjust next leaf's prev_leaf_ptr: (point it to leaf's previous leaf)
|
|
if (leaf.hasNext) {
|
|
const nextLeaf = {
|
|
prevPointerIndex: leaf.nextLeafIndex + binary_tree_leaf_1.BinaryBPlusTreeLeaf.prevLeafPtrIndex,
|
|
oldOffset: binary_tree_leaf_1.BinaryBPlusTreeLeaf.getPrevLeafOffset(leaf.nextLeafIndex, leaf.index),
|
|
newOffset: binary_tree_leaf_1.BinaryBPlusTreeLeaf.getPrevLeafOffset(leaf.nextLeafIndex, leaf.prevLeafIndex),
|
|
};
|
|
tx.queue({
|
|
name: 'next leaf prev_leaf_ptr',
|
|
action: async () => {
|
|
const bytes = (0, binary_1.writeSignedOffset)([], 0, nextLeaf.newOffset, true);
|
|
return this._writeFn(bytes, nextLeaf.prevPointerIndex);
|
|
},
|
|
rollback: async () => {
|
|
const bytes = (0, binary_1.writeSignedOffset)([], 0, nextLeaf.oldOffset, true);
|
|
return this._writeFn(bytes, nextLeaf.prevPointerIndex);
|
|
},
|
|
});
|
|
}
|
|
// Rewrite parent node
|
|
const parentNodeInfo = {
|
|
entries: leaf.parentNode.entries.slice(),
|
|
gtChildIndex: leaf.parentNode.gtChildIndex,
|
|
};
|
|
// Remove parent node entry or change gtChildOffset
|
|
if (leaf.parentEntry) {
|
|
const removeEntryIndex = leaf.parentNode.entries.indexOf(leaf.parentEntry);
|
|
leaf.parentNode.entries.splice(removeEntryIndex, 1);
|
|
}
|
|
else {
|
|
// Change gtChildOffset to last entry's offset
|
|
const lastEntry = leaf.parentNode.entries.splice(-1)[0];
|
|
leaf.parentNode.gtChildIndex = lastEntry.ltChildIndex;
|
|
}
|
|
tx.queue({
|
|
name: 'parent node',
|
|
action: async () => {
|
|
return this._writeNode(leaf.parentNode);
|
|
},
|
|
rollback: async () => {
|
|
// Set the target leaf indexes back to the originals
|
|
leaf.parentNode.entries = parentNodeInfo.entries;
|
|
leaf.parentNode.gtChildIndex = parentNodeInfo.gtChildIndex;
|
|
return this._writeNode(leaf.parentNode);
|
|
},
|
|
});
|
|
await tx.execute(true);
|
|
await this._registerFreeSpace(leaf.index, freedBytes);
|
|
// await this._testTree();
|
|
}
|
|
catch (err) {
|
|
throw new detailed_error_1.DetailedError('remove-leaf-failed', `Failed to remove leaf: ${err.message}`, err);
|
|
}
|
|
}
|
|
async update(key, newRecordPointer, currentRecordPointer, newMetadata) {
|
|
return this._threadSafe('exclusive', () => this._update(key, newRecordPointer, currentRecordPointer, newMetadata));
|
|
}
|
|
async _update(key, newRecordPointer, currentRecordPointer, newMetadata) {
|
|
try {
|
|
// key = _normalizeKey(key); // if (_isIntString(key)) { key = parseInt(key); }
|
|
if (currentRecordPointer === null) {
|
|
currentRecordPointer = undefined;
|
|
}
|
|
const newEntryValue = new tree_leaf_entry_value_1.BPlusTreeLeafEntryValue(newRecordPointer, newMetadata);
|
|
const leaf = await this._findLeaf(key);
|
|
// This is the leaf the key should be in
|
|
const entryIndex = leaf.entries.findIndex(entry => (0, typesafe_compare_1._isEqual)(entry.key, key));
|
|
if (!~entryIndex) {
|
|
throw new detailed_error_1.DetailedError('key-not-found', `Key to update ("${key}") not found`);
|
|
}
|
|
const entry = leaf.entries[entryIndex];
|
|
if (this.info.isUnique) {
|
|
entry.values = [newEntryValue];
|
|
}
|
|
else if (typeof currentRecordPointer === 'undefined') {
|
|
throw new detailed_error_1.DetailedError('current-value-not-given', 'To update a non-unique key, the current value must be passed as parameter');
|
|
}
|
|
else {
|
|
const valueIndex = entry.values.findIndex(val => (0, typesafe_compare_1._compareBinary)(val.recordPointer, currentRecordPointer));
|
|
if (!~valueIndex) {
|
|
throw new detailed_error_1.DetailedError('key-value-pair-not-found', `Key/value combination to update not found (key: "${key}") `);
|
|
}
|
|
entry.values[valueIndex] = newEntryValue;
|
|
}
|
|
return await this._writeLeaf(leaf);
|
|
}
|
|
catch (err) {
|
|
throw new detailed_error_1.DetailedError('update-value-failed', `Could not update value for key '${key}': ${err.message}`, err);
|
|
}
|
|
}
|
|
/**
|
|
* Executes all operations until execution fails: remaining operations are left in passed array
|
|
*/
|
|
async transaction(operations) {
|
|
return this._threadSafe('exclusive', () => this._transaction(operations));
|
|
}
|
|
async _transaction(operations) {
|
|
if (!this.info) {
|
|
// Populate info for this tree
|
|
const reader = await this._getReader();
|
|
if (typeof reader.close === 'function') {
|
|
reader.close();
|
|
}
|
|
}
|
|
if (this.info.isUnique) {
|
|
return this._process(operations);
|
|
}
|
|
while (operations.length > 0) {
|
|
const op = operations.shift();
|
|
try {
|
|
switch (op.type) {
|
|
case 'add': {
|
|
await this._add(op.key, op.recordPointer, op.metadata);
|
|
break;
|
|
}
|
|
case 'remove': {
|
|
await this._remove(op.key, op.recordPointer);
|
|
break;
|
|
}
|
|
case 'update': {
|
|
await this._update(op.key, op.newValue.recordPointer, op.currentValue.recordPointer, op.newValue.metadata);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
catch (err) {
|
|
operations.unshift(op);
|
|
throw err;
|
|
}
|
|
}
|
|
}
|
|
async toTree(fillFactor = 100) {
|
|
const builder = await this.toTreeBuilder(fillFactor);
|
|
return builder.create();
|
|
}
|
|
/**
|
|
* @returns Promise that resolves with a BPlusTreeBuilder
|
|
*/
|
|
async toTreeBuilder(fillFactor) {
|
|
return this._threadSafe('shared', () => this._toTreeBuilder(fillFactor));
|
|
}
|
|
/**
|
|
* @returns Promise that resolves with a BPlusTreeBuilder
|
|
*/
|
|
async _toTreeBuilder(fillFactor) {
|
|
const treeBuilder = new tree_builder_1.BPlusTreeBuilder(this.info.isUnique, fillFactor, this.info.metadataKeys);
|
|
let leaf = await this._getFirstLeaf();
|
|
while (leaf) {
|
|
leaf.entries.forEach(entry => {
|
|
entry.values.forEach(entryValue => treeBuilder.add(entry.key, entryValue.value, entryValue.metadata));
|
|
});
|
|
leaf = leaf.getNext ? await leaf.getNext() : null;
|
|
}
|
|
return treeBuilder;
|
|
}
|
|
async rebuild(writer, options) {
|
|
return this._threadSafe('exclusive', () => this._rebuild(writer, options));
|
|
}
|
|
async _rebuild(writer, options = {
|
|
allocatedBytes: 0,
|
|
fillFactor: 95,
|
|
keepFreeSpace: true,
|
|
increaseMaxEntries: true,
|
|
reserveSpaceForNewEntries: 0,
|
|
}) {
|
|
const perf = {};
|
|
const mark = (name) => {
|
|
const keys = name.split('.');
|
|
const key = keys.pop();
|
|
const target = keys.reduce((t, key) => key in t ? t[key] : (t[key] = {}), perf);
|
|
target[key] = Date.now(); // performance.mark(name);
|
|
};
|
|
const measure = (mark1, mark2) => {
|
|
const getMark = (name) => {
|
|
const keys = name.split('.');
|
|
const key = keys.pop();
|
|
const target = keys.reduce((t, key) => key in t ? t[key] : (t[key] = {}), perf);
|
|
return target[key];
|
|
};
|
|
return getMark(mark2) - getMark(mark1);
|
|
};
|
|
mark('start');
|
|
if (!(writer instanceof binary_writer_1.BinaryWriter)) {
|
|
throw new detailed_error_1.DetailedError('invalid-argument', 'writer argument must be an instance of BinaryWriter');
|
|
}
|
|
if (!this.info) {
|
|
// Hasn't been initialized yet.
|
|
await this._getReader(); // _getReader populates the info
|
|
}
|
|
const originalChunkSize = this._chunkSize;
|
|
// this._chunkSize = 1024 * 1024; // Read 1MB at a time to speed up IO
|
|
options = options || {};
|
|
options.fillFactor = options.fillFactor || this.info.fillFactor || 95;
|
|
options.keepFreeSpace = options.keepFreeSpace !== false;
|
|
options.increaseMaxEntries = options.increaseMaxEntries !== false;
|
|
options.treeStatistics = options.treeStatistics || { byteLength: 0, totalEntries: 0, totalValues: 0, totalLeafs: 0, depth: 0, entriesPerNode: 0 };
|
|
if (typeof options.allocatedBytes === 'number') {
|
|
options.treeStatistics.byteLength = options.allocatedBytes;
|
|
}
|
|
let maxEntriesPerNode = this.info.entriesPerNode;
|
|
if (options.increaseMaxEntries && maxEntriesPerNode < 255) {
|
|
// Increase nr of entries per node with 10%
|
|
maxEntriesPerNode = Math.min(255, Math.round(maxEntriesPerNode * 1.1));
|
|
}
|
|
options.treeStatistics.entriesPerNode = maxEntriesPerNode;
|
|
// let entriesPerLeaf = Math.round(maxEntriesPerNode * (options.fillFactor / 100));
|
|
// let entriesPerNode = entriesPerLeaf;
|
|
// How many entries does the tree have in total?
|
|
// TODO: store this in this.info.totalEntries (and in binary file)
|
|
const leafStats = {
|
|
// debugEntries: [],
|
|
totalEntries: 0,
|
|
totalValues: 0,
|
|
totalEntryBytes: 0,
|
|
totalKeyBytes: 0,
|
|
readLeafs: 0,
|
|
readEntries: 0,
|
|
writtenLeafs: 0,
|
|
writtenEntries: 0,
|
|
get averageEntryLength() {
|
|
return Math.ceil(this.totalEntryBytes / this.totalEntries);
|
|
},
|
|
get averageKeyLength() {
|
|
return Math.ceil(this.totalKeyBytes / this.totalEntries);
|
|
},
|
|
};
|
|
const getKeySize = (key) => {
|
|
if (typeof key === 'number' || key instanceof Date) {
|
|
return 4;
|
|
}
|
|
if (typeof key === 'string') {
|
|
return key.length;
|
|
}
|
|
if (typeof key === 'boolean') {
|
|
return 1;
|
|
}
|
|
if (typeof key === 'bigint') {
|
|
// bigint has variable length
|
|
return bigintToBytes(key).length;
|
|
}
|
|
};
|
|
// let leafsSeen = 0;
|
|
// console.log(`[${Date.toString()}] Starting tree rebuild`);
|
|
try {
|
|
const getLeafStartKeys = async (entriesPerLeaf) => {
|
|
mark('getLeafStartKeys.start');
|
|
const leafStartKeys = [];
|
|
let entriesFromLastLeafStart = 0;
|
|
let leaf = await this._getFirstLeaf();
|
|
let loop = 1;
|
|
while (leaf) {
|
|
mark(`getLeafStartKeys.loop${loop++}`);
|
|
// leafsSeen++;
|
|
// console.log(`Processing leaf with ${leaf.entries.length} entries, total=${totalEntries}`);
|
|
// leafStats.debugEntries.push(...leaf.entries);
|
|
if (leaf.entries.length === 0) {
|
|
// For leafs that were previously left empty (are now removed, see issue #5)
|
|
leaf = leaf.getNext ? await leaf.getNext() : null;
|
|
continue;
|
|
}
|
|
leafStats.totalEntries += leaf.entries.length;
|
|
leafStats.totalValues += leaf.entries.reduce((total, entry) => total + entry.totalValues, 0);
|
|
leafStats.totalEntryBytes += leaf.length;
|
|
leafStats.totalKeyBytes += leaf.entries.reduce((total, entry) => total + getKeySize(entry.key), 0);
|
|
if (leafStartKeys.length === 0 || entriesFromLastLeafStart === entriesPerLeaf) {
|
|
// This is the first leaf being processed, or last leaf entries filled whole new leaf
|
|
leafStartKeys.push(leaf.entries[0].key);
|
|
entriesFromLastLeafStart = 0;
|
|
}
|
|
if (entriesFromLastLeafStart + leaf.entries.length <= entriesPerLeaf) {
|
|
// All entries fit into current leaf
|
|
entriesFromLastLeafStart += leaf.entries.length;
|
|
}
|
|
else {
|
|
// some of the entries fit in current leaf
|
|
let cutIndex = entriesPerLeaf - entriesFromLastLeafStart;
|
|
// new leaf starts at cutIndex
|
|
let firstLeafEntry = leaf.entries[cutIndex];
|
|
leafStartKeys.push(firstLeafEntry.key);
|
|
// How many entries for the new leaf do we have already?
|
|
entriesFromLastLeafStart = leaf.entries.length - cutIndex;
|
|
while (entriesFromLastLeafStart > entriesPerLeaf) {
|
|
// Too many for 1 leaf
|
|
cutIndex += entriesPerLeaf;
|
|
firstLeafEntry = leaf.entries[cutIndex];
|
|
leafStartKeys.push(firstLeafEntry.key);
|
|
entriesFromLastLeafStart = leaf.entries.length - cutIndex;
|
|
}
|
|
}
|
|
// console.log(`Processed ${leafsSeen} leafs in source tree`);
|
|
leaf = leaf.getNext ? await leaf.getNext() : null;
|
|
}
|
|
mark('getLeafStartKeys.end');
|
|
return leafStartKeys;
|
|
};
|
|
let lastLeaf = null;
|
|
let getEntryCalls = 1;
|
|
/**
|
|
* Gets next leaf's entries
|
|
* @param n unused
|
|
*/
|
|
const getEntries = async (n) => {
|
|
if (getEntryCalls === 1) {
|
|
mark('getEntries.first');
|
|
}
|
|
mark(`getEntries.start${getEntryCalls}`);
|
|
try {
|
|
const leaf = lastLeaf
|
|
? lastLeaf.getNext ? await lastLeaf.getNext() : null
|
|
: await this._getFirstLeaf();
|
|
if (leaf) {
|
|
// If leaf has extData, load it first
|
|
if (leaf.hasExtData && !leaf.extData.loaded) {
|
|
await leaf.extData.load();
|
|
}
|
|
lastLeaf = leaf;
|
|
leafStats.readLeafs++;
|
|
leafStats.readEntries += leaf.entries.length;
|
|
if (leaf.entries.length === 0 && leaf.getNext) {
|
|
// For leafs that were previously left empty (are now removed, see issue #5)
|
|
return getEntries(n); // processes next leaf
|
|
}
|
|
return leaf.entries;
|
|
}
|
|
else {
|
|
return [];
|
|
}
|
|
}
|
|
finally {
|
|
mark(`getEntries.end${getEntryCalls++}`);
|
|
mark('getEntries.last'); // overwrites 'last' each loop
|
|
}
|
|
};
|
|
mark('tree.createStart');
|
|
await BinaryBPlusTree.create({
|
|
getLeafStartKeys,
|
|
getEntries,
|
|
writer,
|
|
treeStatistics: options.treeStatistics,
|
|
fillFactor: options.fillFactor,
|
|
maxEntriesPerNode,
|
|
isUnique: this.info.isUnique,
|
|
metadataKeys: this.info.metadataKeys,
|
|
allocatedBytes: options.allocatedBytes,
|
|
keepFreeSpace: options.keepFreeSpace,
|
|
reserveSpaceForNewEntries: options.reserveSpaceForNewEntries,
|
|
});
|
|
mark('tree.createEnd');
|
|
options.treeStatistics.totalLeafs = leafStats.writtenLeafs;
|
|
options.treeStatistics.totalEntries = leafStats.totalEntries;
|
|
options.treeStatistics.totalValues = leafStats.totalValues;
|
|
this._chunkSize = originalChunkSize; // Reset chunk size to original
|
|
// await this._testTree();
|
|
}
|
|
catch (err) {
|
|
throw new detailed_error_1.DetailedError('tree_rebuild_error', 'Failed to rebuild tree', err);
|
|
}
|
|
finally {
|
|
mark('end');
|
|
// if (perf) {
|
|
// // inspect perf here
|
|
// console.log(`[perf] tree rebuild took ${measure('start', 'end')}ms`);
|
|
// console.log(`[perf] getLeafStartKeys: ${measure('getLeafStartKeys.start', 'getLeafStartKeys.end')}ms`);
|
|
// console.log(`[perf] getEntries: ${measure('getEntries.first', 'getEntries.last')}ms`);
|
|
// console.log(`[perf] tree.create: ${measure('tree.createStart', 'tree.createEnd')}ms`);
|
|
// }
|
|
}
|
|
}
|
|
static async create(options) {
|
|
const writer = options.writer;
|
|
if (typeof options.maxEntriesPerNode !== 'number') {
|
|
options.maxEntriesPerNode = 255;
|
|
}
|
|
if (typeof options.fillFactor !== 'number') {
|
|
options.fillFactor = 100;
|
|
}
|
|
const entriesPerLeaf = Math.round(options.maxEntriesPerNode * (options.fillFactor / 100));
|
|
const entriesPerNode = entriesPerLeaf;
|
|
try {
|
|
const leafStartKeys = await options.getLeafStartKeys(entriesPerLeaf);
|
|
// Now we know how many leafs we will be building and what their first key values are
|
|
const createLeafs = leafStartKeys.length;
|
|
options.treeStatistics.totalLeafs = createLeafs;
|
|
let childLevelNodes = leafStartKeys;
|
|
const levels = [];
|
|
while (childLevelNodes.length > 1) {
|
|
// Create another level
|
|
childLevelNodes = childLevelNodes.reduce((nodes, child, index, arr) => {
|
|
const entriesLeft = arr.length - index;
|
|
let currentNode = nodes[nodes.length - 1];
|
|
const isLast = index === arr.length - 1 // Literally the last child
|
|
|| currentNode.entries.length === entriesPerNode // gt connection of this node
|
|
|| (entriesLeft === 3 && currentNode.entries.length + entriesLeft > entriesPerNode); // early chop off gt connection to save entries for next node
|
|
if (isLast) {
|
|
// gt connection
|
|
const key = typeof child === 'object' && 'gtMaxKey' in child
|
|
? child.gtMaxKey // child is node
|
|
: arr[index + 1]; // child is leaf start key
|
|
currentNode.gtMaxKey = key;
|
|
currentNode.gtChildIndex = index;
|
|
if (index < arr.length - 1) {
|
|
// More to come..
|
|
currentNode = { entries: [], gtChildIndex: -1, gtMaxKey: null };
|
|
nodes.push(currentNode);
|
|
}
|
|
// connections = 0;
|
|
}
|
|
else {
|
|
// lt connection
|
|
const key = typeof child === 'object' && 'gtMaxKey' in child
|
|
? child.gtMaxKey // child is node
|
|
: arr[index + 1]; // child is leaf start key
|
|
currentNode.entries.push({ key, ltChildIndex: index });
|
|
// connections++;
|
|
}
|
|
return nodes;
|
|
}, [{ entries: [], gtChildIndex: -1, gtMaxKey: null }]);
|
|
levels.push(childLevelNodes);
|
|
}
|
|
options.treeStatistics.depth = levels.length;
|
|
options.treeStatistics.writtenLeafs = 0;
|
|
options.treeStatistics.writtenEntries = 0;
|
|
// Now that we have the keys for each node level, we can start building the actual tree
|
|
// Do this efficiently by reusing the level keys array, reducing them in size as we go
|
|
// Write in this order:
|
|
// 1) header
|
|
// 2) all nodes,
|
|
// 3) all leafs,
|
|
// 4) all nodes again with the right child pointers (or just the pointers),
|
|
// 5) overwrite header with real data
|
|
const builder = new binary_tree_builder_1.BinaryBPlusTreeBuilder({
|
|
uniqueKeys: options.isUnique,
|
|
byteLength: options.allocatedBytes,
|
|
maxEntriesPerNode: options.maxEntriesPerNode,
|
|
freeBytes: options.keepFreeSpace ? 1 : 0,
|
|
metadataKeys: options.metadataKeys,
|
|
smallLeafs: config_1.WRITE_SMALL_LEAFS,
|
|
fillFactor: options.fillFactor,
|
|
});
|
|
// Create header
|
|
let header = builder.getHeader();
|
|
let index = header.length;
|
|
// const rootNodeIndex = index;
|
|
const leafIndexes = [];
|
|
let largestLeafLength = 0;
|
|
await writer.append(header);
|
|
// Write all node levels for the first time
|
|
// (lt/gt child index pointers won't make sense yet)
|
|
let l = levels.length;
|
|
while (l > 0) {
|
|
l--;
|
|
const nodes = levels[l];
|
|
const writes = [];
|
|
nodes.forEach(node => {
|
|
node.index = index; //writer.length;
|
|
const bytes = builder.createNode({
|
|
index: node.index,
|
|
entries: node.entries.map(entry => ({ key: entry.key, ltIndex: 0 })),
|
|
gtIndex: 0,
|
|
}, { addFreeSpace: options.keepFreeSpace, allowMissingChildIndexes: true });
|
|
node.byteLength = bytes.length;
|
|
index += bytes.length;
|
|
const p = writer.append(bytes);
|
|
writes.push(p);
|
|
});
|
|
await Promise.all(writes);
|
|
}
|
|
// Write all leafs
|
|
const newLeafEntries = [];
|
|
let prevIndex = 0;
|
|
let currentLeafIndex = 0;
|
|
let totalWrittenEntries = 0;
|
|
const writeLeaf = async (entries) => {
|
|
let emptyLeaf = false;
|
|
if (entries.length === 0 && leafStartKeys.length === 0) {
|
|
// Write an empty leaf
|
|
emptyLeaf = true;
|
|
}
|
|
// console.log(`Writing leaf with ${entries.length} entries at index ${index}, keys range: ["${entries[0].key}", "${entries[entries.length-1].key}"]`)
|
|
// console.assert(entries.every((entry, index, arr) => index === 0 || _isMoreOrEqual(entry.key, arr[index-1].key)), 'Leaf entries are not sorted ok');
|
|
const i = leafIndexes.length;
|
|
// console.assert(emptyLeaf || _isEqual(leafStartKeys[i], entries[0].key), `first entry for leaf has wrong key, must be ${leafStartKeys[i]}!`);
|
|
leafIndexes.push(index);
|
|
const isLastLeaf = emptyLeaf || leafIndexes.length === leafStartKeys.length;
|
|
const newLeaf = builder.createLeaf({ index, prevIndex, nextIndex: isLastLeaf ? 0 : 'adjacent', entries }, { addFreeSpace: options.keepFreeSpace });
|
|
largestLeafLength = Math.max(largestLeafLength, newLeaf.length);
|
|
prevIndex = index;
|
|
index += newLeaf.length;
|
|
totalWrittenEntries += entries.length;
|
|
return writer.append(newLeaf);
|
|
};
|
|
const flush = async (flushAll = false) => {
|
|
const cutEntryKey = leafStartKeys[currentLeafIndex + 1];
|
|
let entries;
|
|
if (typeof cutEntryKey === 'undefined') {
|
|
// Last batch
|
|
if (flushAll) {
|
|
// console.assert(newLeafEntries.length <= entriesPerLeaf, 'check logic');
|
|
entries = newLeafEntries.splice(0);
|
|
}
|
|
else {
|
|
return; // Wait for remaining entries
|
|
}
|
|
}
|
|
else {
|
|
const cutEntryIndex = newLeafEntries.findIndex(entry => (0, typesafe_compare_1._isEqual)(entry.key, cutEntryKey));
|
|
if (cutEntryIndex === -1) {
|
|
// Not enough entries yet
|
|
// console.assert(!flushAll, 'check logic');
|
|
// console.assert(newLeafEntries.length <= entriesPerLeaf, 'check logic!');
|
|
return;
|
|
}
|
|
entries = newLeafEntries.splice(0, cutEntryIndex);
|
|
}
|
|
options.treeStatistics.writtenLeafs++;
|
|
options.treeStatistics.writtenEntries += entries.length;
|
|
currentLeafIndex++;
|
|
await writeLeaf(entries);
|
|
// Write more?
|
|
if (newLeafEntries.length >= entriesPerLeaf || (flushAll && newLeafEntries.length > 0)) {
|
|
await flush(flushAll);
|
|
}
|
|
};
|
|
const processEntries = async (entries) => {
|
|
if (entries.length === 0) {
|
|
return flush(true); // done!
|
|
}
|
|
// options.treeStatistics.readEntries += entries.length;
|
|
// console.assert(entries.every((entry, index, arr) => index === 0 || _isMoreOrEqual(entry.key, arr[index-1].key)), 'Leaf entries are not sorted ok');
|
|
// console.assert(newLeafEntries.length === 0 || _isMore(entries[0].key, newLeafEntries[newLeafEntries.length-1].key), 'adding entries will corrupt sort order');
|
|
newLeafEntries.push(...entries);
|
|
const writePromise = flush(false);
|
|
const readNextPromise = options.getEntries(options.maxEntriesPerNode);
|
|
const [moreEntries] = await Promise.all([readNextPromise, writePromise]);
|
|
await processEntries(moreEntries);
|
|
};
|
|
const entries = await options.getEntries(options.maxEntriesPerNode);
|
|
await processEntries(entries);
|
|
// .then(() => {
|
|
// // // DEbug tree writing
|
|
// // let debugTree = levels.map(nodes => nodes.slice()); // copy
|
|
// // debugTree.forEach((nodes, levelIndex) => {
|
|
// // debugTree[levelIndex] = nodes.map(node => {
|
|
// // return {
|
|
// // node,
|
|
// // gtChild: levelIndex === 0
|
|
// // ? leafStartKeys[node.gtChildIndex]
|
|
// // : debugTree[levelIndex-1][node.gtChildIndex],
|
|
// // entries: node.entries.map(entry => {
|
|
// // return {
|
|
// // key: entry.key,
|
|
// // ltChild: levelIndex === 0
|
|
// // ? leafStartKeys[entry.ltChildIndex]
|
|
// // : debugTree[levelIndex-1][entry.ltChildIndex]
|
|
// // };
|
|
// // })
|
|
// // };
|
|
// // });
|
|
// // });
|
|
// // debugTree.reverse(); // Now top-down
|
|
// // console.error(debugTree);
|
|
// // debugTree.forEach((nodes, levelIndex) => {
|
|
// // let allEntries = nodes.map(node => `[${node.entries.map(entry => entry.key).join(',')}]`).join(' | ')
|
|
// // console.error(`node level ${levelIndex}: ${allEntries}`);
|
|
// // });
|
|
// // console.error(`leafs: [${leafStartKeys.join(`..] | [`)}]`);
|
|
// })
|
|
// Now adjust the header data & write free bytes
|
|
let byteLength = index;
|
|
let freeBytes = 0;
|
|
if (options.allocatedBytes > 0) {
|
|
freeBytes = options.allocatedBytes - byteLength;
|
|
byteLength = options.allocatedBytes;
|
|
}
|
|
else {
|
|
// Use 10% free space, or the largest leaf length + 10%, or requested free leaf space, whichever is the largest
|
|
freeBytes = Math.max(Math.ceil(byteLength * 0.1), Math.ceil(largestLeafLength * 1.1), Math.ceil(Math.ceil((options.reserveSpaceForNewEntries || 0) / entriesPerLeaf) * largestLeafLength * 1.1));
|
|
// console.log(`new tree gets ${freeBytes} free bytes`);
|
|
byteLength += freeBytes;
|
|
}
|
|
// Rebuild header
|
|
builder.byteLength = byteLength; // - header.length;
|
|
builder.freeBytes = freeBytes;
|
|
header = builder.getHeader();
|
|
options.treeStatistics.byteLength = byteLength;
|
|
options.treeStatistics.freeBytes = freeBytes;
|
|
// Append free space bytes
|
|
const bytesPerWrite = 1024 * 100; // 100KB per write seems fair?
|
|
const writeBatches = Math.ceil(builder.freeBytes / bytesPerWrite);
|
|
for (let i = 0; i < writeBatches; i++) {
|
|
const length = i + 1 < writeBatches
|
|
? bytesPerWrite
|
|
: builder.freeBytes % bytesPerWrite;
|
|
const zeroes = new Uint8Array(length);
|
|
await writer.append(zeroes);
|
|
}
|
|
// Done appending data, close stream
|
|
await writer.end();
|
|
// Overwrite header
|
|
const writePromises = [
|
|
writer.write(header, 0),
|
|
];
|
|
// Assign all nodes' child indexes to the real file indexes
|
|
levels.forEach((nodes, index) => {
|
|
nodes.forEach(node => {
|
|
if (index === 0) {
|
|
// first level references leafs
|
|
node.gtChildIndex = leafIndexes[node.gtChildIndex];
|
|
node.entries.forEach(entry => {
|
|
entry.ltChildIndex = leafIndexes[entry.ltChildIndex];
|
|
});
|
|
}
|
|
else {
|
|
// use node index on next (lower) level
|
|
node.gtChildIndex = levels[index - 1][node.gtChildIndex].index;
|
|
node.entries.forEach(entry => {
|
|
entry.ltChildIndex = levels[index - 1][entry.ltChildIndex].index;
|
|
});
|
|
}
|
|
// Regenerate bytes
|
|
const bytes = builder.createNode({
|
|
index: node.index,
|
|
entries: node.entries.map(entry => ({ key: entry.key, ltIndex: entry.ltChildIndex })),
|
|
gtIndex: node.gtChildIndex,
|
|
}, { addFreeSpace: options.keepFreeSpace, maxLength: node.byteLength });
|
|
// And overwrite them in the file
|
|
const p = writer.write(bytes, node.index);
|
|
writePromises.push(p);
|
|
});
|
|
});
|
|
await Promise.all(writePromises);
|
|
}
|
|
catch (err) {
|
|
throw new detailed_error_1.DetailedError('tree_create_error', 'Failed to create BinaryBlusTree', err);
|
|
}
|
|
}
|
|
/**
|
|
* Creates a binary tree from a stream of entries.
|
|
* An entry stream must be a binary data stream containing only leaf entries
|
|
* a leaf entry can be created using BinaryBPlusTree.createStreamEntry(key, values)
|
|
*/
|
|
static createFromEntryStream(reader, writer, options) {
|
|
// Steps:
|
|
// 1 - loop through all entries to calculate leaf start keys
|
|
// 2 - create nodes
|
|
// 3 - create leafs
|
|
// const entriesPerLeaf = Math.round(options.maxEntriesPerNode * (options.fillFactor / 100));
|
|
const getLeafStartKeys = async (entriesPerLeaf) => {
|
|
options.treeStatistics.totalEntries = 0;
|
|
await reader.init();
|
|
const leafStartKeys = [];
|
|
while (true) {
|
|
options.treeStatistics.totalEntries++;
|
|
const entryIndex = reader.sourceIndex;
|
|
try {
|
|
const entryLength = await reader.getUint32();
|
|
if (options.treeStatistics.totalEntries % entriesPerLeaf === 1) {
|
|
const key = await reader.getValue();
|
|
// console.log(key);
|
|
leafStartKeys.push(key);
|
|
await reader.go(entryIndex + entryLength);
|
|
}
|
|
else {
|
|
// skip reading this entry's key
|
|
await reader.go(entryIndex + entryLength);
|
|
}
|
|
}
|
|
catch (err) {
|
|
if (err.code === 'EOF') {
|
|
break;
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
await reader.go(0); // Reset
|
|
return leafStartKeys;
|
|
};
|
|
const getEntries = async (n) => {
|
|
// read n entries
|
|
const entries = [];
|
|
reader.chunkSize = 1024 * 1024; // 1MB chunks
|
|
while (true) {
|
|
try {
|
|
// read entry_length:
|
|
const entryLength = await reader.getUint32();
|
|
const buffer = await reader.get(entryLength - 4); // -4 because entry_length is 4 bytes
|
|
// read key:
|
|
const k = binary_reader_1.BinaryReader.readValue(buffer, 0);
|
|
const entry = new binary_tree_leaf_entry_1.BinaryBPlusTreeLeafEntry(k.value, []);
|
|
let index = k.byteLength;
|
|
// read values_length
|
|
const totalValues = binary_reader_1.BinaryReader.readUint32(buffer, index);
|
|
index += 4;
|
|
for (let i = 0; i < totalValues; i++) {
|
|
// read value_length
|
|
const valueLength = binary_reader_1.BinaryReader.readUint32(buffer, index);
|
|
index += 4;
|
|
const val = buffer.slice(index, index + valueLength);
|
|
index += valueLength;
|
|
// val contains rp_length, rp_data, metadata
|
|
const rpLength = val[0]; // rp_length
|
|
const recordPointer = val.slice(1, 1 + rpLength); // rp_data
|
|
// metadata:
|
|
let valIndex = 1 + rpLength;
|
|
const metadata = {};
|
|
for (let j = 0; j < options.metadataKeys.length; j++) {
|
|
const mdKey = options.metadataKeys[j];
|
|
const mdValue = binary_reader_1.BinaryReader.readValue(val, valIndex);
|
|
metadata[mdKey] = mdValue.value;
|
|
valIndex += mdValue.byteLength;
|
|
}
|
|
const value = new binary_tree_leaf_entry_value_1.BinaryBPlusTreeLeafEntryValue(recordPointer, metadata);
|
|
entry.values.push(value);
|
|
}
|
|
entries.push(entry);
|
|
if (entries.length >= n) {
|
|
break;
|
|
}
|
|
}
|
|
catch (err) {
|
|
// EOF?
|
|
if (err.code === 'EOF') {
|
|
break;
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
return entries;
|
|
};
|
|
return BinaryBPlusTree.create({
|
|
getLeafStartKeys,
|
|
getEntries,
|
|
writer,
|
|
treeStatistics: options.treeStatistics,
|
|
fillFactor: options.fillFactor,
|
|
allocatedBytes: options.allocatedBytes,
|
|
isUnique: options.isUnique,
|
|
keepFreeSpace: options.keepFreeSpace,
|
|
maxEntriesPerNode: options.maxEntriesPerNode,
|
|
metadataKeys: options.metadataKeys,
|
|
});
|
|
}
|
|
}
|
|
exports.BinaryBPlusTree = BinaryBPlusTree;
|
|
BinaryBPlusTree.EntryValue = binary_tree_leaf_entry_value_1.BinaryBPlusTreeLeafEntryValue;
|
|
BinaryBPlusTree.TransactionOperation = binary_tree_transaction_operation_1.BinaryBPlusTreeTransactionOperation;
|
|
|
|
}).call(this)}).call(this,require("buffer").Buffer)
|
|
},{"../binary":212,"../detailed-error":238,"../thread-safe":254,"./binary-reader":214,"./binary-tree-builder":215,"./binary-tree-leaf":218,"./binary-tree-leaf-entry":217,"./binary-tree-leaf-entry-value":216,"./binary-tree-node":221,"./binary-tree-node-entry":219,"./binary-tree-node-info":220,"./binary-tree-transaction-operation":222,"./binary-writer":224,"./config":225,"./tree":233,"./tree-builder":227,"./tree-leaf-entry-value":228,"./tx":234,"./typesafe-compare":235,"./utils":236,"acebase-core":12,"buffer":27}],224:[function(require,module,exports){
|
|
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.BinaryWriter = void 0;
|
|
const binary_1 = require("../binary");
|
|
const binary_tree_builder_1 = require("./binary-tree-builder");
|
|
const acebase_core_1 = require("acebase-core");
|
|
const { numberToBytes, bytesToNumber } = acebase_core_1.Utils;
|
|
class BinaryWriter {
|
|
constructor(stream, writeFn) {
|
|
this._stream = stream;
|
|
this._write = writeFn;
|
|
this._written = 0;
|
|
}
|
|
static forArray(bytes) {
|
|
let bytesWritten = 0;
|
|
const stream = {
|
|
get bytesWritten() {
|
|
return bytesWritten;
|
|
},
|
|
write(data) {
|
|
for (let i = 0; i < data.byteLength; i++) {
|
|
bytes.push(data[i]);
|
|
}
|
|
bytesWritten += data.byteLength;
|
|
return true; // let caller know its ok to continue writing
|
|
},
|
|
end(callback) {
|
|
callback();
|
|
},
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
once(event, callback) {
|
|
if (event === 'drain') {
|
|
callback();
|
|
}
|
|
return this;
|
|
},
|
|
};
|
|
const writer = new BinaryWriter(stream, async (data, position) => {
|
|
for (let i = bytes.length; i < position; i++) {
|
|
bytes[i] = 0; // prevent "undefined" bytes when writing to a position greater than current array length
|
|
}
|
|
for (let i = 0; i < data.byteLength; i++) {
|
|
bytes[position + i] = data[i];
|
|
}
|
|
});
|
|
return writer;
|
|
}
|
|
static forUint8ArrayBuilder(builder) {
|
|
let bytesWritten = 0;
|
|
const stream = {
|
|
get bytesWritten() {
|
|
return bytesWritten;
|
|
},
|
|
write(data) {
|
|
builder.append(data);
|
|
bytesWritten += data.byteLength;
|
|
return true; // let caller know its ok to continue writing
|
|
},
|
|
end(callback) {
|
|
callback();
|
|
},
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
once(event, callback) {
|
|
if (event === 'drain') {
|
|
callback();
|
|
}
|
|
return this;
|
|
},
|
|
};
|
|
const writer = new BinaryWriter(stream, async (data, position) => {
|
|
builder.write(data, position);
|
|
});
|
|
return writer;
|
|
}
|
|
static forFunction(writeFn) {
|
|
const maxSimultaniousWrites = 50;
|
|
let currentPosition = 0;
|
|
let bytesWritten = 0;
|
|
let pendingWrites = 0;
|
|
const drainCallbacks = [];
|
|
let endCallback = null;
|
|
let ended = false;
|
|
const stream = {
|
|
get bytesWritten() {
|
|
return bytesWritten;
|
|
},
|
|
write(data) {
|
|
console.assert(!ended, 'streaming was ended already!');
|
|
if (pendingWrites === maxSimultaniousWrites) {
|
|
console.warn('Warning: you should wait for "drain" event before writing new data!');
|
|
}
|
|
// console.assert(_pendingWrites < _maxSimultaniousWrites, 'Wait for "drain" event before writing new data!');
|
|
pendingWrites++;
|
|
const success = () => {
|
|
bytesWritten += data.byteLength;
|
|
pendingWrites--;
|
|
if (ended && pendingWrites === 0) {
|
|
endCallback();
|
|
}
|
|
const drainCallback = drainCallbacks.shift();
|
|
drainCallback && drainCallback();
|
|
};
|
|
const fail = (err) => {
|
|
console.error(`Failed to write to stream: ${err.message}`);
|
|
success();
|
|
};
|
|
writeFn(data, currentPosition)
|
|
.then(success)
|
|
.catch(fail);
|
|
currentPosition += data.byteLength;
|
|
const ok = pendingWrites < maxSimultaniousWrites;
|
|
return ok; // let caller know if its ok to continue writing
|
|
},
|
|
end(callback) {
|
|
if (ended) {
|
|
throw new Error('end can only be called once');
|
|
}
|
|
ended = true;
|
|
endCallback = callback;
|
|
if (pendingWrites === 0) {
|
|
callback();
|
|
}
|
|
},
|
|
once(event, callback) {
|
|
console.assert(event === 'drain', 'Custom stream can only handle "drain" event');
|
|
drainCallbacks.push(callback);
|
|
return this;
|
|
},
|
|
};
|
|
const writer = new BinaryWriter(stream, (data, position) => {
|
|
return writeFn(data, position);
|
|
});
|
|
return writer;
|
|
}
|
|
get length() { return this._written; }
|
|
get queued() { return this._written - this._stream.bytesWritten; }
|
|
append(data) {
|
|
const buffer = data instanceof Array
|
|
? Uint8Array.from(data)
|
|
: data;
|
|
return new Promise(resolve => {
|
|
const ok = this._stream.write(buffer);
|
|
this._written += buffer.byteLength;
|
|
if (!ok) {
|
|
this._stream.once('drain', resolve);
|
|
}
|
|
else {
|
|
resolve();
|
|
}
|
|
});
|
|
}
|
|
write(data, position) {
|
|
const buffer = data instanceof Array
|
|
? Uint8Array.from(data)
|
|
: data;
|
|
return this._write(buffer, position);
|
|
}
|
|
end() {
|
|
return new Promise(resolve => {
|
|
this._stream.end(resolve);
|
|
// writer.stream.on('finish', resolve);
|
|
});
|
|
}
|
|
static getBytes(value) {
|
|
return binary_tree_builder_1.BinaryBPlusTreeBuilder.getKeyBytes(value);
|
|
}
|
|
static numberToBytes(number) {
|
|
return numberToBytes(number);
|
|
}
|
|
static bytesToNumber(bytes) {
|
|
return bytesToNumber(bytes);
|
|
}
|
|
static writeUint32(number, bytes, index) {
|
|
return (0, binary_1.writeByteLength)(bytes, index, number);
|
|
}
|
|
static writeInt32(signedNumber, bytes, index) {
|
|
return (0, binary_1.writeSignedNumber)(bytes, index, signedNumber);
|
|
}
|
|
}
|
|
exports.BinaryWriter = BinaryWriter;
|
|
|
|
},{"../binary":212,"./binary-tree-builder":215,"acebase-core":12}],225:[function(require,module,exports){
|
|
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.MAX_LEAF_ENTRY_VALUES = exports.MAX_SMALL_LEAF_VALUE_LENGTH = exports.WRITE_SMALL_LEAFS = void 0;
|
|
exports.WRITE_SMALL_LEAFS = true;
|
|
exports.MAX_SMALL_LEAF_VALUE_LENGTH = 127 - 4; // -4 because value_list_length is now included in data length
|
|
exports.MAX_LEAF_ENTRY_VALUES = Math.pow(2, 32) - 1;
|
|
|
|
},{}],226:[function(require,module,exports){
|
|
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.BlacklistingSearchOperator = exports.BinaryReader = exports.BinaryWriter = exports.BPlusTreeBuilder = exports.BinaryBPlusTreeLeafEntry = exports.BinaryBPlusTreeLeaf = exports.BinaryBPlusTree = exports.BPlusTree = void 0;
|
|
const binary_reader_1 = require("./binary-reader");
|
|
Object.defineProperty(exports, "BinaryReader", { enumerable: true, get: function () { return binary_reader_1.BinaryReader; } });
|
|
const binary_tree_1 = require("./binary-tree");
|
|
Object.defineProperty(exports, "BinaryBPlusTree", { enumerable: true, get: function () { return binary_tree_1.BinaryBPlusTree; } });
|
|
Object.defineProperty(exports, "BlacklistingSearchOperator", { enumerable: true, get: function () { return binary_tree_1.BlacklistingSearchOperator; } });
|
|
const binary_tree_leaf_1 = require("./binary-tree-leaf");
|
|
Object.defineProperty(exports, "BinaryBPlusTreeLeaf", { enumerable: true, get: function () { return binary_tree_leaf_1.BinaryBPlusTreeLeaf; } });
|
|
const binary_tree_leaf_entry_1 = require("./binary-tree-leaf-entry");
|
|
Object.defineProperty(exports, "BinaryBPlusTreeLeafEntry", { enumerable: true, get: function () { return binary_tree_leaf_entry_1.BinaryBPlusTreeLeafEntry; } });
|
|
const binary_writer_1 = require("./binary-writer");
|
|
Object.defineProperty(exports, "BinaryWriter", { enumerable: true, get: function () { return binary_writer_1.BinaryWriter; } });
|
|
const tree_1 = require("./tree");
|
|
Object.defineProperty(exports, "BPlusTree", { enumerable: true, get: function () { return tree_1.BPlusTree; } });
|
|
const tree_builder_1 = require("./tree-builder");
|
|
Object.defineProperty(exports, "BPlusTreeBuilder", { enumerable: true, get: function () { return tree_builder_1.BPlusTreeBuilder; } });
|
|
|
|
},{"./binary-reader":214,"./binary-tree":223,"./binary-tree-leaf":218,"./binary-tree-leaf-entry":217,"./binary-writer":224,"./tree":233,"./tree-builder":227}],227:[function(require,module,exports){
|
|
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.BPlusTreeBuilder = void 0;
|
|
const detailed_error_1 = require("../detailed-error");
|
|
const tree_1 = require("./tree");
|
|
const tree_leaf_1 = require("./tree-leaf");
|
|
const tree_leaf_entry_1 = require("./tree-leaf-entry");
|
|
const tree_leaf_entry_value_1 = require("./tree-leaf-entry-value");
|
|
const tree_node_1 = require("./tree-node");
|
|
const tree_node_entry_1 = require("./tree-node-entry");
|
|
const typesafe_compare_1 = require("./typesafe-compare");
|
|
const utils_1 = require("./utils");
|
|
class BPlusTreeBuilder {
|
|
/**
|
|
* @param {boolean} uniqueKeys
|
|
* @param {number} [fillFactor=100]
|
|
* @param {string[]} [metadataKeys=[]]
|
|
*/
|
|
constructor(uniqueKeys, fillFactor = 100, metadataKeys = []) {
|
|
this.uniqueKeys = uniqueKeys;
|
|
this.fillFactor = fillFactor;
|
|
this.metadataKeys = metadataKeys;
|
|
this.list = new Map();
|
|
this.indexedValues = 0;
|
|
}
|
|
add(key, recordPointer, metadata) {
|
|
// key = _normalizeKey(key); // if (_isIntString(key)) { key = parseInt(key); }
|
|
const err = (0, utils_1._checkNewEntryArgs)(key, recordPointer, this.metadataKeys, metadata);
|
|
if (err) {
|
|
throw err;
|
|
}
|
|
const entryValue = new tree_leaf_entry_value_1.BPlusTreeLeafEntryValue(recordPointer, metadata);
|
|
const existing = this.list.get(key); // this.list[key]
|
|
if (this.uniqueKeys && typeof existing !== 'undefined') {
|
|
throw new detailed_error_1.DetailedError('unique-key-violation', `Cannot add duplicate key "${key}", tree must have unique keys`);
|
|
}
|
|
else if (existing) {
|
|
existing.push(entryValue);
|
|
}
|
|
else {
|
|
this.list.set(key, this.uniqueKeys //this.list[key] =
|
|
? entryValue
|
|
: [entryValue]);
|
|
}
|
|
this.indexedValues++;
|
|
}
|
|
/**
|
|
* @param key
|
|
* @param recordPointer specific recordPointer to remove. If the tree has unique keys, this can be omitted
|
|
*/
|
|
remove(key, recordPointer) {
|
|
// key = _normalizeKey(key); // if (_isIntString(key)) { key = parseInt(key); }
|
|
const isEqual = (val1, val2) => {
|
|
if (val1 instanceof Array && val2 instanceof Array) {
|
|
return val1.every((v, i) => val2[i] === v);
|
|
}
|
|
return val1 === val2;
|
|
};
|
|
if (this.uniqueKeys) {
|
|
this.list.delete(key);
|
|
}
|
|
else {
|
|
const entryValues = this.list.get(key);
|
|
const valIndex = entryValues.findIndex(entryValue => isEqual(entryValue.recordPointer, recordPointer));
|
|
if (~valIndex) {
|
|
if (entryValues.length === 1) {
|
|
this.list.delete(key);
|
|
}
|
|
else {
|
|
entryValues.splice(valIndex, 1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
create(maxEntries) {
|
|
// Create a tree bottom-up with all nodes filled to the max (optionally capped to fillFactor)
|
|
const list = [];
|
|
this.list.forEach((val, key) => {
|
|
list.push({ key, val });
|
|
});
|
|
this.list.clear();
|
|
this.list = null; // Make unusable
|
|
list.sort((a, b) => {
|
|
return (0, typesafe_compare_1._sortCompare)(a.key, b.key);
|
|
// if (_isLess(a.key, b.key)) { return -1; }
|
|
// else if (_isMore(a.key, b.key)) { return 1; }
|
|
// return 0;
|
|
});
|
|
//const length = Object.keys(this.list).length;
|
|
const minNodeSize = 3; //25;
|
|
const maxNodeSize = 255;
|
|
const entriesPerNode = typeof maxEntries === 'number' ? maxEntries : Math.min(maxNodeSize, Math.max(minNodeSize, Math.ceil(list.length / 10)));
|
|
const entriesPerLeaf = Math.max(minNodeSize, Math.floor(entriesPerNode * (this.fillFactor / 100)));
|
|
const minParentEntries = Math.max(1, Math.floor(entriesPerNode / 2));
|
|
const tree = new tree_1.BPlusTree(entriesPerNode, this.uniqueKeys, this.metadataKeys);
|
|
tree.fillFactor = this.fillFactor;
|
|
const nrOfLeafs = Math.max(1, Math.ceil(list.length / entriesPerLeaf));
|
|
const parentConnections = entriesPerNode + 1; // should be +1 because the > connection
|
|
let currentLevel = 1;
|
|
let nrOfNodesAtLevel = nrOfLeafs;
|
|
let nrOfParentNodes = Math.ceil(nrOfNodesAtLevel / parentConnections);
|
|
let nodesAtLevel = []; // & { prevNode?: BPlusTreeNode; nextNode?: BPlusTreeNode; prevLeaf?: BPlusTreeLeaf; nextLeaf: BPlusTreeLeaf }
|
|
while (true) {
|
|
// Create parent nodes
|
|
const creatingLeafs = currentLevel === 1;
|
|
const parentNodes = [];
|
|
for (let i = 0; i < nrOfParentNodes; i++) {
|
|
const node = new tree_node_1.BPlusTreeNode(tree, null);
|
|
if (i > 0) {
|
|
const prevNode = parentNodes[i - 1];
|
|
node.prevNode = prevNode;
|
|
prevNode.nextNode = node;
|
|
}
|
|
parentNodes.push(node);
|
|
}
|
|
for (let i = 0; i < nrOfNodesAtLevel; i++) {
|
|
// Eg 500 leafs with 25 entries each, 500/25 = 20 parent nodes:
|
|
// When i is between 0 and (25-1), parent node index = 0
|
|
// When i is between 25 and (50-1), parent index = 1 etc
|
|
// So, parentIndex = Math.floor(i / 25)
|
|
const parentIndex = Math.floor(i / parentConnections);
|
|
const parent = parentNodes[parentIndex];
|
|
if (creatingLeafs) {
|
|
// Create leaf
|
|
const leaf = new tree_leaf_1.BPlusTreeLeaf(parent);
|
|
nodesAtLevel.push(leaf);
|
|
// Setup linked list properties
|
|
const prevLeaf = nodesAtLevel[nodesAtLevel.length - 2];
|
|
if (prevLeaf) {
|
|
leaf.prevLeaf = prevLeaf;
|
|
prevLeaf.nextLeaf = leaf;
|
|
}
|
|
// Create leaf entries
|
|
const fromIndex = i * entriesPerLeaf;
|
|
const entryKVPs = list.slice(fromIndex, fromIndex + entriesPerLeaf);
|
|
entryKVPs.forEach(kvp => {
|
|
const entry = new tree_leaf_entry_1.BPlusTreeLeafEntry(leaf, kvp.key);
|
|
entry.values = this.uniqueKeys ? [kvp.val] : kvp.val;
|
|
leaf.entries.push(entry);
|
|
});
|
|
const isLastLeaf = Math.floor((i + 1) / parentConnections) > parentIndex
|
|
|| i === nrOfNodesAtLevel - 1;
|
|
if (isLastLeaf) {
|
|
// Have parent's gtChild point to this last leaf
|
|
parent.gtChild = leaf;
|
|
if (parentNodes.length > 1 && parent.entries.length < minParentEntries) {
|
|
/* Consider this order 4 B+Tree: 3 entries per node, 4 connections
|
|
|
|
12 >
|
|
4 7 10 > || >
|
|
1 2 3 || 4 5 6 || 7 8 9 || 10 11 12 || 13 14 15
|
|
|
|
The last leaf (13 14 15) is the only child of its parent, its assignment to
|
|
parent.gtChild is right, but there is no entry to > compare to. In this case, we have to
|
|
move the previous leaf's parent entry to our own parent:
|
|
|
|
10 >
|
|
4 7 > || 13 >
|
|
1 2 3 || 4 5 6 || 7 8 9 || 10 11 12 || 13 14 15
|
|
|
|
We moved just 1 parent entry which is fine in case of an order 4 tree, floor((O-1) / 2) is the
|
|
minimum entries for a node, floor((4-1) / 2) = floor(1.5) = 1.
|
|
When the tree order is higher, it's effect on higher tree nodes becomes greater and the tree
|
|
becomes inbalanced if we do not meet the minimum entries p/node requirement.
|
|
So, we'll have to move Math.floor(entriesPerNode / 2) parent entries to our parent
|
|
*/
|
|
const nrOfParentEntries2Move = minParentEntries - parent.entries.length;
|
|
const prevParent = parent.prevNode;
|
|
for (let j = 0; j < nrOfParentEntries2Move; j++) {
|
|
const firstChild = parent.entries.length === 0
|
|
? leaf // In first iteration, firstLeaf === leaf === "13 14 15"
|
|
: parent.entries[0].ltChild; // In following iterations, firstLeaf === last moved leaf "10 11 12"
|
|
//const prevChild = firstChild.prevChild;
|
|
const moveEntry = prevParent.entries.pop(); // removes "10" from prevLeaf's parent
|
|
const moveLeaf = prevParent.gtChild;
|
|
prevParent.gtChild = moveEntry.ltChild; // assigns "7 8 9" leaf to prevLeaf's parent > connection
|
|
moveEntry.key = firstChild.entries[0].key; // changes the key to "13"
|
|
moveLeaf.parent = parent; // changes moving "10 11 12" leaf's parent to ours
|
|
moveEntry.ltChild = moveLeaf; // assigns "10 11 12" leaf to <13 connection
|
|
parent.entries.unshift(moveEntry); // inserts "13" entry into our parent node
|
|
moveEntry.node = parent; // changes moving entry's parent to ours
|
|
}
|
|
//console.log(`Moved ${nrOfParentEntries2Move} parent node entries`);
|
|
}
|
|
}
|
|
else {
|
|
// Create parent entry with ltChild that points to this leaf
|
|
const ltChildKey = list[fromIndex + entriesPerLeaf].key;
|
|
const parentEntry = new tree_node_entry_1.BPlusTreeNodeEntry(parent, ltChildKey);
|
|
parentEntry.ltChild = leaf;
|
|
parent.entries.push(parentEntry);
|
|
}
|
|
}
|
|
else {
|
|
// Nodes have already been created at the previous iteration,
|
|
// we have to create entries for parent nodes only
|
|
const node = nodesAtLevel[i];
|
|
node.parent = parent;
|
|
// // Setup linked list properties - not needed by BPlusTreeNode itself, but used in code below
|
|
// const prevNode = nodesAtLevel[nodesAtLevel.length-2];
|
|
// if (prevNode) {
|
|
// node.prevNode = prevNode;
|
|
// prevNode.nextNode = node;
|
|
// }
|
|
const isLastNode = Math.floor((i + 1) / parentConnections) > parentIndex
|
|
|| i === nrOfNodesAtLevel - 1;
|
|
if (isLastNode) {
|
|
parent.gtChild = node;
|
|
if (parentNodes.length > 1 && parent.entries.length < minParentEntries) {
|
|
// This is not right, we have to fix it.
|
|
// See leaf code above for additional info
|
|
const nrOfParentEntries2Move = minParentEntries - parent.entries.length;
|
|
const prevParent = parent.prevNode;
|
|
for (let j = 0; j < nrOfParentEntries2Move; j++) {
|
|
const firstChild = parent.entries.length === 0
|
|
? node
|
|
: parent.entries[0].ltChild;
|
|
const moveEntry = prevParent.entries.pop();
|
|
const moveNode = prevParent.gtChild;
|
|
prevParent.gtChild = moveEntry.ltChild;
|
|
let ltChild = firstChild.entries[0].ltChild;
|
|
while (!(ltChild instanceof tree_leaf_1.BPlusTreeLeaf)) {
|
|
ltChild = ltChild.entries[0].ltChild;
|
|
}
|
|
// BUG in next line? Mistake discovered during TS port
|
|
moveEntry.key = ltChild.entries[0].key; //firstChild.entries[0].key;
|
|
moveNode.parent = parent;
|
|
moveEntry.ltChild = moveNode;
|
|
parent.entries.unshift(moveEntry);
|
|
moveEntry.node = parent;
|
|
}
|
|
//console.log(`Moved ${nrOfParentEntries2Move} parent node entries`);
|
|
}
|
|
}
|
|
else {
|
|
let ltChild = node.nextNode;
|
|
while (!(ltChild instanceof tree_leaf_1.BPlusTreeLeaf)) {
|
|
ltChild = ltChild.entries[0].ltChild;
|
|
}
|
|
const ltChildKey = ltChild.entries[0].key; //node.gtChild.entries[node.gtChild.entries.length-1].key; //nodesAtLevel[i+1].entries[0].key;
|
|
const parentEntry = new tree_node_entry_1.BPlusTreeNodeEntry(parent, ltChildKey);
|
|
parentEntry.ltChild = node;
|
|
parent.entries.push(parentEntry);
|
|
}
|
|
}
|
|
}
|
|
if (nrOfLeafs === 1) {
|
|
// Very little data. Only 1 leaf
|
|
const leaf = nodesAtLevel[0];
|
|
leaf.parent = tree;
|
|
tree.root = leaf;
|
|
break;
|
|
}
|
|
else if (nrOfParentNodes === 1) {
|
|
// Done
|
|
tree.root = parentNodes[0];
|
|
break;
|
|
}
|
|
currentLevel++; // Level up
|
|
nodesAtLevel = parentNodes;
|
|
nrOfNodesAtLevel = nodesAtLevel.length;
|
|
nrOfParentNodes = Math.ceil(nrOfNodesAtLevel / parentConnections);
|
|
tree.depth++;
|
|
}
|
|
// // TEST the tree
|
|
// const ok = list.every(item => {
|
|
// const val = tree.find(item.key);
|
|
// if (val === null) {
|
|
// return false;
|
|
// }
|
|
// return true;
|
|
// //return !== null;
|
|
// })
|
|
// if (!ok) {
|
|
// throw new Error(`This tree is not ok`);
|
|
// }
|
|
return tree;
|
|
}
|
|
dumpToFile(filename) {
|
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
const fs = require('fs');
|
|
fs.appendFileSync(filename, this.uniqueKeys + '\n');
|
|
fs.appendFileSync(filename, this.fillFactor + '\n');
|
|
for (const [key, val] of this.list) {
|
|
const json = JSON.stringify({ key, val }) + '\n';
|
|
fs.appendFileSync(filename, json);
|
|
}
|
|
}
|
|
static fromFile(filename) {
|
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
const fs = require('fs');
|
|
const entries = fs.readFileSync(filename, 'utf8')
|
|
.split('\n')
|
|
.map(str => str.length > 0 ? JSON.parse(str) : '');
|
|
const last = entries.pop(); // Remove last empty one (because split \n)
|
|
console.assert(last === '');
|
|
const uniqueKeys = entries.shift() === 'true';
|
|
const fillFactor = parseInt(entries.shift());
|
|
const builder = new BPlusTreeBuilder(uniqueKeys, fillFactor);
|
|
// while(entries.length > 0) {
|
|
// let entry = entries.shift();
|
|
// builder.list.set(entry.key, entry.val);
|
|
// }
|
|
for (let i = 0; i < entries.length; i++) {
|
|
builder.list.set(entries[i].key, entries[i].val);
|
|
}
|
|
return builder;
|
|
}
|
|
}
|
|
exports.BPlusTreeBuilder = BPlusTreeBuilder;
|
|
|
|
},{"../detailed-error":238,"./tree":233,"./tree-leaf":230,"./tree-leaf-entry":229,"./tree-leaf-entry-value":228,"./tree-node":232,"./tree-node-entry":231,"./typesafe-compare":235,"./utils":236,"fs":27}],228:[function(require,module,exports){
|
|
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.BPlusTreeLeafEntryValue = void 0;
|
|
class BPlusTreeLeafEntryValue {
|
|
/**
|
|
* @param recordPointer used to be called "value", renamed to prevent confusion
|
|
* @param metadata
|
|
*/
|
|
constructor(recordPointer, metadata) {
|
|
this.recordPointer = recordPointer;
|
|
this.metadata = metadata;
|
|
}
|
|
/** @deprecated use .recordPointer instead */
|
|
get value() {
|
|
return this.recordPointer;
|
|
}
|
|
}
|
|
exports.BPlusTreeLeafEntryValue = BPlusTreeLeafEntryValue;
|
|
|
|
},{}],229:[function(require,module,exports){
|
|
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.BPlusTreeLeafEntry = void 0;
|
|
const tree_leaf_entry_value_1 = require("./tree-leaf-entry-value");
|
|
class BPlusTreeLeafEntry {
|
|
constructor(leaf, key, value) {
|
|
this.leaf = leaf;
|
|
this.key = key;
|
|
if (typeof value !== 'undefined' && !(value instanceof tree_leaf_entry_value_1.BPlusTreeLeafEntryValue)) {
|
|
throw new Error('value must be an instance of BPlusTreeLeafEntryValue');
|
|
}
|
|
this.values = typeof value === 'undefined' ? [] : [value];
|
|
}
|
|
}
|
|
exports.BPlusTreeLeafEntry = BPlusTreeLeafEntry;
|
|
|
|
},{"./tree-leaf-entry-value":228}],230:[function(require,module,exports){
|
|
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.BPlusTreeLeaf = void 0;
|
|
const binary_1 = require("../binary");
|
|
const detailed_error_1 = require("../detailed-error");
|
|
const binary_tree_builder_1 = require("./binary-tree-builder");
|
|
const config_1 = require("./config");
|
|
const tree_1 = require("./tree");
|
|
const tree_leaf_entry_1 = require("./tree-leaf-entry");
|
|
const tree_leaf_entry_value_1 = require("./tree-leaf-entry-value");
|
|
const tree_node_1 = require("./tree-node");
|
|
const tree_node_entry_1 = require("./tree-node-entry");
|
|
const typesafe_compare_1 = require("./typesafe-compare");
|
|
const utils_1 = require("./utils");
|
|
class BPlusTreeLeaf {
|
|
constructor(parent) {
|
|
this.parent = parent;
|
|
this.entries = [];
|
|
this.prevLeaf = null;
|
|
this.nextLeaf = null;
|
|
}
|
|
/**
|
|
* The BPlusTree this leaf is in
|
|
*/
|
|
get tree() {
|
|
return this.parent instanceof tree_1.BPlusTree ? this.parent : this.parent.tree;
|
|
}
|
|
/**
|
|
* Adds an entry to this leaf
|
|
* @param key
|
|
* @param recordPointer data to store with the key, max size is 255
|
|
* @param {object} [metadata] data to include, must contain all keys used in BPlusTree constructor
|
|
* @returns {BPlusTreeLeafEntry} returns the added leaf entry
|
|
*/
|
|
add(key, recordPointer, metadata) {
|
|
// key = _normalizeKey(key); // if (_isIntString(key)) { key = parseInt(key); }
|
|
if (typeof recordPointer === 'string') {
|
|
// For now, allow this. Convert to byte array
|
|
console.warn(`WARNING: converting recordPointer "${recordPointer}" to byte array. This is deprecated, will fail in the future`);
|
|
const bytes = [];
|
|
for (let i = 0; i < recordPointer.length; i++) {
|
|
bytes.push(recordPointer.charCodeAt(i));
|
|
}
|
|
recordPointer = bytes;
|
|
}
|
|
const err = (0, utils_1._checkNewEntryArgs)(key, recordPointer, this.tree.metadataKeys, metadata);
|
|
if (err) {
|
|
throw err;
|
|
}
|
|
const entryValue = new tree_leaf_entry_value_1.BPlusTreeLeafEntryValue(recordPointer, metadata);
|
|
// First. check if we already have an entry with this key
|
|
const entryIndex = this.entries.findIndex(entry => (0, typesafe_compare_1._isEqual)(entry.key, key));
|
|
if (entryIndex >= 0) {
|
|
if (this.tree.uniqueKeys) {
|
|
throw new detailed_error_1.DetailedError('duplicate-node-key', `Cannot insert duplicate key ${key}`);
|
|
}
|
|
const entry = this.entries[entryIndex];
|
|
entry.values.push(entryValue);
|
|
return entry;
|
|
}
|
|
// New key, create entry
|
|
const entry = new tree_leaf_entry_1.BPlusTreeLeafEntry(this, key, entryValue);
|
|
if (this.entries.length === 0) {
|
|
this.entries.push(entry);
|
|
}
|
|
else {
|
|
// Find where to insert sorted
|
|
const insertIndex = this.entries.findIndex(otherEntry => (0, typesafe_compare_1._isMore)(otherEntry.key, entry.key));
|
|
if (insertIndex < 0) {
|
|
this.entries.push(entry);
|
|
}
|
|
else {
|
|
this.entries.splice(insertIndex, 0, entry);
|
|
}
|
|
// FInd out if there are too many entries
|
|
if (this.entries.length > this.tree.maxEntriesPerNode) {
|
|
// Split the leaf
|
|
const splitIndex = Math.ceil(this.tree.maxEntriesPerNode / 2);
|
|
const moveEntries = this.entries.splice(splitIndex);
|
|
const copyUpKey = moveEntries[0].key;
|
|
if (this.parent instanceof tree_1.BPlusTree) {
|
|
// We have to create the first parent node
|
|
const tree = this.parent;
|
|
this.parent = new tree_node_1.BPlusTreeNode(tree, null);
|
|
tree.root = this.parent;
|
|
tree.depth = 2;
|
|
const newLeaf = new BPlusTreeLeaf(this.parent);
|
|
newLeaf.entries = moveEntries;
|
|
const newEntry = new tree_node_entry_1.BPlusTreeNodeEntry(this.parent, copyUpKey);
|
|
newEntry.ltChild = this;
|
|
this.parent.gtChild = newLeaf;
|
|
this.parent.entries = [newEntry];
|
|
// Update linked list pointers
|
|
newLeaf.prevLeaf = this;
|
|
if (this.nextLeaf) {
|
|
newLeaf.nextLeaf = this.nextLeaf;
|
|
newLeaf.nextLeaf.prevLeaf = newLeaf;
|
|
}
|
|
this.nextLeaf = newLeaf;
|
|
}
|
|
else {
|
|
const newLeaf = new BPlusTreeLeaf(this.parent);
|
|
newLeaf.entries = moveEntries;
|
|
this.parent.insertKey(copyUpKey, this, newLeaf);
|
|
// Update linked list pointers
|
|
newLeaf.prevLeaf = this;
|
|
if (this.nextLeaf) {
|
|
newLeaf.nextLeaf = this.nextLeaf;
|
|
newLeaf.nextLeaf.prevLeaf = newLeaf;
|
|
}
|
|
this.nextLeaf = newLeaf;
|
|
}
|
|
}
|
|
}
|
|
return entry;
|
|
}
|
|
toString() {
|
|
const str = 'Leaf: [' + this.entries.map(entry => entry.key).join(' | ') + ']';
|
|
return str;
|
|
}
|
|
async toBinary(keepFreeSpace = false, writer) {
|
|
// See BPlusTreeNode.toBinary() for data layout
|
|
console.assert(this.entries.every((entry, index, arr) => index === 0 || (0, typesafe_compare_1._isMore)(entry.key, arr[index - 1].key)), 'Leaf entries are not sorted ok');
|
|
const bytes = [];
|
|
const startIndex = writer.length;
|
|
// byte_length:
|
|
bytes.push(0, 0, 0, 0);
|
|
// leaf_flags:
|
|
const leafFlagsIndex = bytes.length;
|
|
bytes.push(binary_tree_builder_1.FLAGS.IS_LEAF);
|
|
// free_byte_length:
|
|
bytes.push(0, 0, 0, 0);
|
|
const references = [];
|
|
// prev_leaf_ptr:
|
|
this.prevLeaf && references.push({ name: `<${this.entries[0].key}`, target: this.prevLeaf, index: startIndex + bytes.length });
|
|
bytes.push(0, 0, 0, 0, 0, 0);
|
|
// next_leaf_ptr:
|
|
this.nextLeaf && references.push({ name: `>${this.entries[this.entries.length - 1].key}`, target: this.nextLeaf, index: startIndex + bytes.length });
|
|
bytes.push(0, 0, 0, 0, 0, 0);
|
|
// ext_byte_length, ext_free_byte_length: (will be removed when no ext_data is written)
|
|
const extDataHeaderIndex = bytes.length;
|
|
bytes.push(0, 0, 0, 0, // ext_byte_length
|
|
0, 0, 0, 0);
|
|
// entries_length:
|
|
bytes.push(this.entries.length);
|
|
const entriesStartIndex = bytes.length;
|
|
const moreDataBlocks = [];
|
|
this.entries.forEach(entry => {
|
|
console.assert(entry.values.length <= config_1.MAX_LEAF_ENTRY_VALUES, 'too many leaf entry values to store in binary');
|
|
const keyBytes = tree_1.BPlusTree.getBinaryKeyData(entry.key);
|
|
bytes.push(...keyBytes);
|
|
// val_length:
|
|
const valLengthIndex = bytes.length;
|
|
if (config_1.WRITE_SMALL_LEAFS) {
|
|
bytes.push(0);
|
|
}
|
|
else {
|
|
bytes.push(0, 0, 0, 0);
|
|
}
|
|
const valueBytes = [];
|
|
const writeValue = (entryValue) => {
|
|
const { recordPointer, metadata } = entryValue;
|
|
// const startIndex = bytes.length;
|
|
// const target = valueBytes;
|
|
// value_length:
|
|
valueBytes.push(recordPointer.length);
|
|
// value_data:
|
|
valueBytes.push(...recordPointer);
|
|
// metadata:
|
|
this.tree.metadataKeys.forEach(key => {
|
|
const metadataValue = metadata[key];
|
|
const mdBytes = tree_1.BPlusTree.getBinaryKeyData(metadataValue); // metadata_value has same structure as key, so getBinaryKeyData comes in handy here
|
|
valueBytes.push(...mdBytes);
|
|
});
|
|
};
|
|
if (this.tree.uniqueKeys) {
|
|
// value:
|
|
writeValue(entry.values[0]);
|
|
}
|
|
else {
|
|
entry.values.forEach(entryValue => {
|
|
// value:
|
|
writeValue(entryValue);
|
|
});
|
|
}
|
|
if (config_1.WRITE_SMALL_LEAFS && valueBytes.length > config_1.MAX_SMALL_LEAF_VALUE_LENGTH) {
|
|
// Values too big for small leafs
|
|
// Store value bytes in ext_data block
|
|
if (!this.tree.uniqueKeys) {
|
|
// value_list_length:
|
|
(0, binary_1.writeByteLength)(bytes, bytes.length, entry.values.length);
|
|
}
|
|
// ext_data_ptr:
|
|
const extPointerIndex = bytes.length;
|
|
bytes.push(0, 0, 0, 0);
|
|
// update val_length:
|
|
bytes[valLengthIndex] = binary_tree_builder_1.FLAGS.ENTRY_HAS_EXT_DATA;
|
|
// add
|
|
moreDataBlocks.push({
|
|
pointerIndex: extPointerIndex,
|
|
bytes: valueBytes,
|
|
});
|
|
}
|
|
else {
|
|
// update val_length:
|
|
const valLength = valueBytes.length + (this.tree.uniqueKeys ? 0 : 4); // +4 to include value_list_length bytes //bytes.length - valLengthIndex - 4;
|
|
if (config_1.WRITE_SMALL_LEAFS) {
|
|
bytes[valLengthIndex] = valLength;
|
|
}
|
|
else {
|
|
(0, binary_1.writeByteLength)(bytes, valLengthIndex, valLength);
|
|
}
|
|
if (!this.tree.uniqueKeys) {
|
|
// value_list_length:
|
|
(0, binary_1.writeByteLength)(bytes, bytes.length, entry.values.length);
|
|
}
|
|
// add value bytes:
|
|
(0, utils_1._appendToArray)(bytes, valueBytes);
|
|
}
|
|
});
|
|
// Add free space
|
|
const entriesDataSize = bytes.length - entriesStartIndex;
|
|
const avgBytesPerEntry = this.entries.length === 0 ? 25 : Math.ceil(entriesDataSize / this.entries.length);
|
|
const availableEntries = this.tree.maxEntriesPerNode - this.entries.length;
|
|
const freeBytesLength = keepFreeSpace
|
|
? Math.ceil(availableEntries * avgBytesPerEntry * 1.1) // + 10%
|
|
: 0;
|
|
for (let i = 0; i < freeBytesLength; i++) {
|
|
bytes.push(0);
|
|
}
|
|
const hasExtData = moreDataBlocks.length > 0;
|
|
if (hasExtData) {
|
|
// update leaf_flags:
|
|
bytes[leafFlagsIndex] |= binary_tree_builder_1.FLAGS.LEAF_HAS_EXT_DATA;
|
|
}
|
|
else {
|
|
// remove ext_byte_length, ext_free_byte_length
|
|
bytes.splice(extDataHeaderIndex, 8);
|
|
}
|
|
// update byte_length:
|
|
const totalLeafSize = bytes.length;
|
|
(0, binary_1.writeByteLength)(bytes, 0, totalLeafSize);
|
|
// update free_byte_length
|
|
(0, binary_1.writeByteLength)(bytes, 5, freeBytesLength);
|
|
// Now, add any ext_data blocks
|
|
if (hasExtData) {
|
|
const leafEndIndex = bytes.length;
|
|
moreDataBlocks.forEach(block => {
|
|
const offset = bytes.length - leafEndIndex; // offset from leaf end index
|
|
(0, binary_1.writeByteLength)(bytes, block.pointerIndex, offset); // update ext_data_ptr
|
|
// Calculate free space
|
|
const free = keepFreeSpace ? Math.ceil(block.bytes.length * 0.1) : 0;
|
|
const blockLength = block.bytes.length + free;
|
|
// ext_block_length:
|
|
(0, binary_1.writeByteLength)(bytes, bytes.length, blockLength);
|
|
// ext_block_free_length:
|
|
(0, binary_1.writeByteLength)(bytes, bytes.length, free);
|
|
// ext_data_ptr: (not implemented yet)
|
|
bytes.push(0, 0, 0, 0);
|
|
// data:
|
|
(0, utils_1._appendToArray)(bytes, block.bytes);
|
|
// Add free space:
|
|
for (let i = 0; i < free; i++) {
|
|
bytes.push(0);
|
|
}
|
|
});
|
|
const extByteLength = bytes.length - leafEndIndex;
|
|
const extFreeByteLength = keepFreeSpace ? Math.ceil(extByteLength * 0.1) : 0;
|
|
// update ext_byte_length:
|
|
(0, binary_1.writeByteLength)(bytes, extDataHeaderIndex, extByteLength + extFreeByteLength);
|
|
// update ext_free_byte_length:
|
|
(0, binary_1.writeByteLength)(bytes, extDataHeaderIndex + 4, extFreeByteLength);
|
|
// Add free space:
|
|
for (let i = 0; i < extFreeByteLength; i++) {
|
|
bytes.push(0);
|
|
}
|
|
}
|
|
await writer.append(bytes);
|
|
return { references };
|
|
}
|
|
}
|
|
exports.BPlusTreeLeaf = BPlusTreeLeaf;
|
|
|
|
},{"../binary":212,"../detailed-error":238,"./binary-tree-builder":215,"./config":225,"./tree":233,"./tree-leaf-entry":229,"./tree-leaf-entry-value":228,"./tree-node":232,"./tree-node-entry":231,"./typesafe-compare":235,"./utils":236}],231:[function(require,module,exports){
|
|
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.BPlusTreeNodeEntry = void 0;
|
|
class BPlusTreeNodeEntry {
|
|
constructor(node, key) {
|
|
this.node = node;
|
|
this.key = key;
|
|
this.ltChild = null;
|
|
}
|
|
}
|
|
exports.BPlusTreeNodeEntry = BPlusTreeNodeEntry;
|
|
|
|
},{}],232:[function(require,module,exports){
|
|
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.BPlusTreeNode = void 0;
|
|
const tree_node_entry_1 = require("./tree-node-entry");
|
|
const tree_leaf_1 = require("./tree-leaf");
|
|
const tree_1 = require("./tree");
|
|
const typesafe_compare_1 = require("./typesafe-compare");
|
|
const detailed_error_1 = require("../detailed-error");
|
|
const binary_1 = require("../binary");
|
|
class BPlusTreeNode {
|
|
constructor(tree, parent) {
|
|
this.tree = tree;
|
|
this.parent = parent;
|
|
this.entries = [];
|
|
this.gtChild = null;
|
|
}
|
|
toString() {
|
|
let str = 'Node: [' + this.entries.map(entry => entry.key).join(' | ') + ']';
|
|
str += ' --> ';
|
|
str += this.entries.map(entry => entry.ltChild.toString()).join(', ');
|
|
str += ', ' + this.gtChild.toString();
|
|
return str;
|
|
}
|
|
insertKey(newKey, fromLeaf, newLeaf) {
|
|
// New key is being inserted from splitting leaf node
|
|
if (this.entries.findIndex(entry => (0, typesafe_compare_1._isEqual)(entry.key, newKey)) >= 0) {
|
|
throw new detailed_error_1.DetailedError('node-key-exists', `Key ${newKey} is already present in node`);
|
|
}
|
|
const newNodeEntry = new tree_node_entry_1.BPlusTreeNodeEntry(this, newKey);
|
|
if (this.gtChild === fromLeaf) {
|
|
newNodeEntry.ltChild = fromLeaf;
|
|
this.gtChild = newLeaf;
|
|
this.entries.push(newNodeEntry);
|
|
}
|
|
else {
|
|
const oldNodeEntry = this.entries.find(entry => entry.ltChild === fromLeaf);
|
|
const insertIndex = this.entries.indexOf(oldNodeEntry);
|
|
newNodeEntry.ltChild = fromLeaf;
|
|
oldNodeEntry.ltChild = newLeaf;
|
|
this.entries.splice(insertIndex, 0, newNodeEntry);
|
|
}
|
|
this._checkSize();
|
|
}
|
|
_checkSize() {
|
|
// Check if there are too many entries
|
|
if (this.entries.length > this.tree.maxEntriesPerNode) {
|
|
// Split this node
|
|
// A = [ 10, 20, 30, 40 ] becomes A = [ 10, 20 ], B = [ 40 ], C = 30 moves to parent
|
|
// B's gtChild (-) becomes A's gtChild (>=40)
|
|
// A's gtChild (>=40) becomes C's ltChild (<30)
|
|
// C's ltChild (<30) becomes A
|
|
// C's entry_index+1.ltChild (when inserted, or C's node.gtChild when appended) becomes B
|
|
const splitIndex = Math.ceil(this.tree.maxEntriesPerNode / 2);
|
|
const moveEntries = this.entries.splice(splitIndex);
|
|
const moveUpEntry = moveEntries.shift();
|
|
const ltChild = moveUpEntry.ltChild;
|
|
moveUpEntry.ltChild = this;
|
|
const gtChild = this.gtChild;
|
|
this.gtChild = ltChild;
|
|
if (this.parent === null) {
|
|
// Create new root node
|
|
const newRoot = new BPlusTreeNode(this.tree, null);
|
|
newRoot.entries = [moveUpEntry];
|
|
const newSibling = new BPlusTreeNode(this.tree, newRoot);
|
|
newSibling.entries = moveEntries;
|
|
moveEntries.forEach(entry => entry.ltChild.parent = newSibling);
|
|
newRoot.gtChild = newSibling;
|
|
newSibling.gtChild = gtChild;
|
|
gtChild.parent = newSibling;
|
|
this.parent = newRoot;
|
|
this.tree.root = newRoot;
|
|
this.tree.depth++;
|
|
}
|
|
else {
|
|
const newSibling = new BPlusTreeNode(this.tree, this.parent);
|
|
newSibling.entries = moveEntries;
|
|
moveEntries.forEach(entry => entry.ltChild.parent = newSibling);
|
|
newSibling.gtChild = gtChild;
|
|
gtChild.parent = newSibling;
|
|
// Find where to insert moveUp
|
|
const insertIndex = this.parent.entries.findIndex(entry => (0, typesafe_compare_1._isMore)(entry.key, moveUpEntry.key));
|
|
if (insertIndex < 0) {
|
|
// Add to the end
|
|
this.parent.entries.push(moveUpEntry);
|
|
this.parent.gtChild = newSibling;
|
|
}
|
|
else {
|
|
// Insert somewhere in between
|
|
const insertBefore = this.parent.entries[insertIndex];
|
|
insertBefore.ltChild = newSibling;
|
|
this.parent.entries.splice(insertIndex, 0, moveUpEntry);
|
|
}
|
|
this.parent._checkSize(); // Let it check its size
|
|
}
|
|
}
|
|
}
|
|
async toBinary(keepFreeSpace, writer) {
|
|
// EBNF layout:
|
|
// data = byte_length, index_type, max_node_entries, [fill_factor], [free_byte_length], [metadata_keys], root_node
|
|
// byte_length = 4 byte number (byte count)
|
|
// data_byte_length = byte_length: NEVER INCLUDES ITS OWN BYTE SIZE OR OTHER HEADER BYTE SIZES
|
|
// index_type = 1 byte = [0, 0, has_large_ptrs, has_small_leafs, has_fill_factor, has_free_space, has_metadata, is_unique]
|
|
// max_node_entries = 1 byte number
|
|
// fill_factor = 1 byte number (max 100)
|
|
// metadata_keys = has_metadata?
|
|
// 1: metadata_length, metadata_key_count, metadata_key, [metadata_key, [metadata_key...]]
|
|
// 0: not present
|
|
// metadata_length = byte_length
|
|
// metadata_key_count = 1 byte number
|
|
// metadata_key = metadata_key_length, metadata_key_name
|
|
// metadata_key_length = 1 byte number
|
|
// metadata_key_name = [metadata_key_length] bytes (TextEncoded char codes)
|
|
// root_node = node | leaf
|
|
// node* = byte_length***, is_leaf, free_byte_length, entries_length, entries, gt_child_ptr, free_bytes, children
|
|
// is_leaf = 1 byte leaf_flags
|
|
// >=1: yes, leaf
|
|
// 0: no, it's a node
|
|
// free_byte_length = byte_length (how many bytes are free for later additions)
|
|
// entries_length = 1 byte number (max 255 entries)
|
|
// entries = entry, [entry, [entry...]]
|
|
// entry = key, lt_child_ptr
|
|
// key = key_type, key_length, key_data
|
|
// key_type = 1 byte number
|
|
// 0: UNDEFINED (equiv to sql null values)
|
|
// 1: STRING
|
|
// 2: NUMBER
|
|
// 3: BOOLEAN
|
|
// 4: DATE
|
|
// key_length = 1 byte number
|
|
// key_data = [key_length] bytes (ASCII chars when key is string)
|
|
// lt_child_ptr = offset_ptr (byte offset to node | leaf)
|
|
// gt_child_ptr = offset_ptr (byte offset to node | leaf)
|
|
// children = node, [node, [node...]] | leaf, [leaf, [leaf...]]
|
|
// leaf** = byte_length***, leaf_flags, free_byte_length, prev_leaf_ptr, next_leaf_ptr, [ext_byte_length, ext_free_byte_length], entries_length, leaf_entries, free_bytes, [ext_data]
|
|
// leaf_flags = 1 byte = [0, 0, 0, 0, 0, 0, has_ext_data, is_leaf]
|
|
// prev_leaf_ptr = offset_ptr (byte offset to leaf)
|
|
// next_leaf_ptr = offset_ptr (byte offset to leaf)
|
|
// leaf_entries = leaf_entry, [leaf_entry, [leaf_entry...]]
|
|
// leaf_entry = key, val
|
|
// offset_ptr = has_large_ptrs?
|
|
// 0: signed_number
|
|
// 1: large_signed_number
|
|
// small_offset_ptr = signed_number
|
|
// signed_number = 4 bytes, 32 bits = [negative_flag, ...bits]
|
|
// large_signed_number = 6 bytes, 48 bits = [negative_flag, ...bits]
|
|
// val = val_length, val_data
|
|
// val_length = has_small_leafs?
|
|
// 1: 1 byte number: [1 bit has_ext_data, 7 bit byte count]
|
|
// 0: 4 byte number (byte count)
|
|
// val_data = has_ext_data?
|
|
// 1: is_unique?
|
|
// 1: ext_data_ptr
|
|
// 2: value_list_length, ext_data_ptr
|
|
// 0: is_unique?
|
|
// 1: value_list
|
|
// 0: value
|
|
// ext_data_ptr = byte_length (byte offset from leaf end to ext_data_block)
|
|
// value_list = value_list_length, value, [value, [value...]]
|
|
// value_list_length = 4 byte number
|
|
// value = value_length, value_data, metadata
|
|
// value_length = 1 byte number
|
|
// value_data = [value_length] bytes data
|
|
// metadata = metadata_value{metadata_key_count}
|
|
// metadata_value = metadata_value_type, metadata_value_length, metadata_value_data
|
|
// metadata_value_type = key_type
|
|
// metadata_value_length= key_length
|
|
// metadata_value_data = key_data
|
|
// ext_data = ext_data_block, [ext_data_block, [ext_data_block]]
|
|
// ext_data_block = ext_block_length, ext_block_free_length, data (value | value_list)
|
|
// ext_block_length = data_byte_length
|
|
// ext_block_free_length= free_byte_length
|
|
//
|
|
// * Written by BPlusTreeNode.toBinary
|
|
// ** Written by BPlusTreeLeaf.toBinary
|
|
// *** including free bytes (BUT excluding size of ext_data blocks for leafs)
|
|
const bytes = [];
|
|
const startIndex = writer.length; //bytes.length;
|
|
// byte_length:
|
|
bytes.push(0, 0, 0, 0);
|
|
// is_leaf:
|
|
bytes.push(0); // (no)
|
|
// free_byte_length:
|
|
bytes.push(0, 0, 0, 0); // Now used!
|
|
// entries_length:
|
|
bytes.push(this.entries.length);
|
|
const pointers = []; // pointers refer to an offset in the binary data where nodes/leafs can be found
|
|
const references = []; // references point to an index in the binary data where pointers are to be stored
|
|
this.entries.forEach(entry => {
|
|
const keyBytes = tree_1.BPlusTree.getBinaryKeyData(entry.key);
|
|
bytes.push(...keyBytes);
|
|
// lt_child_ptr:
|
|
const index = startIndex + bytes.length;
|
|
bytes.push(0, 0, 0, 0, 0, 0);
|
|
references.push({ name: `<${entry.key}`, index, target: entry.ltChild });
|
|
});
|
|
// gt_child_ptr:
|
|
const index = startIndex + bytes.length;
|
|
bytes.push(0, 0, 0, 0, 0, 0);
|
|
references.push({ name: `>${this.entries[this.entries.length - 1].key}`, index, target: this.gtChild });
|
|
let freeBytes = 0;
|
|
if (keepFreeSpace) {
|
|
// Add free space
|
|
const avgEntrySize = Math.ceil(bytes.length / this.entries.length);
|
|
const freeEntries = this.tree.maxEntriesPerNode - this.entries.length;
|
|
freeBytes = freeEntries * avgEntrySize;
|
|
for (let i = 0; i < freeBytes; i++) {
|
|
bytes.push(0);
|
|
}
|
|
// update free_byte_length:
|
|
(0, binary_1.writeByteLength)(bytes, 5, freeBytes);
|
|
}
|
|
// update byte_length:
|
|
(0, binary_1.writeByteLength)(bytes, 0, bytes.length);
|
|
// Flush bytes, continue async
|
|
await writer.append(bytes);
|
|
// Now add children (NOTE: loops to entries.length + 1 to include gtChild!)
|
|
for (let childIndex = 0; childIndex < this.entries.length + 1; childIndex++) {
|
|
const entry = this.entries[childIndex];
|
|
const childNode = entry ? entry.ltChild : this.gtChild;
|
|
const name = entry ? `<${entry.key}` : `>=${this.entries[this.entries.length - 1].key}`;
|
|
const index = writer.length;
|
|
const refIndex = references.findIndex(ref => ref.target === childNode);
|
|
const ref = references.splice(refIndex, 1)[0];
|
|
const offset = index - (ref.index + 5); // index - (ref.index + 3);
|
|
// Update child_ptr
|
|
const child_ptr = (0, binary_1.writeSignedOffset)([], 0, offset, true);
|
|
await writer.write(child_ptr, ref.index); // Update pointer
|
|
const child = await childNode.toBinary(keepFreeSpace, writer); // Write child
|
|
if (childNode instanceof tree_leaf_1.BPlusTreeLeaf) {
|
|
// Remember location we stored this leaf, we need it later
|
|
pointers.push({
|
|
name,
|
|
leaf: childNode,
|
|
index,
|
|
});
|
|
}
|
|
// Add node pointers added by the child
|
|
'pointers' in child && child.pointers.forEach(pointer => {
|
|
// pointer.index += index; // DISABLED: indexes must already be ok now we're using 1 bytes array
|
|
pointers.push(pointer);
|
|
});
|
|
// Add unresolved references added by the child
|
|
child.references.forEach(ref => {
|
|
// ref.index += index; // DISABLED: indexes must already be ok now we're using 1 bytes array
|
|
references.push(ref);
|
|
});
|
|
}
|
|
// Check if we can resolve any leaf references
|
|
await BPlusTreeNode.resolveBinaryReferences(writer, references, pointers);
|
|
return { references, pointers };
|
|
}
|
|
static async resolveBinaryReferences(writer, references, pointers) {
|
|
for (let pointerIndex = 0; pointerIndex < pointers.length; pointerIndex++) {
|
|
const pointer = pointers[pointerIndex];
|
|
let i;
|
|
while ((i = references.findIndex(ref => ref.target === pointer.leaf)) >= 0) {
|
|
const ref = references.splice(i, 1)[0]; // remove it from the references
|
|
const offset = pointer.index - ref.index;
|
|
const bytes = (0, binary_1.writeSignedOffset)([], 0, offset, true);
|
|
await writer.write(bytes, ref.index);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
exports.BPlusTreeNode = BPlusTreeNode;
|
|
|
|
},{"../binary":212,"../detailed-error":238,"./tree":233,"./tree-leaf":230,"./tree-node-entry":231,"./typesafe-compare":235}],233:[function(require,module,exports){
|
|
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.BPlusTree = void 0;
|
|
const acebase_core_1 = require("acebase-core");
|
|
const binary_1 = require("../binary");
|
|
const detailed_error_1 = require("../detailed-error");
|
|
const binary_tree_builder_1 = require("./binary-tree-builder");
|
|
const binary_writer_1 = require("./binary-writer");
|
|
const config_1 = require("./config");
|
|
const tree_leaf_1 = require("./tree-leaf");
|
|
const typesafe_compare_1 = require("./typesafe-compare");
|
|
const { bigintToBytes, bytesToBigint, bytesToNumber, decodeString, encodeString, numberToBytes } = acebase_core_1.Utils;
|
|
class BPlusTree {
|
|
/**
|
|
* @param maxEntriesPerNode max number of entries per tree node. Working with this instead of m for max number of children, because that makes less sense imho
|
|
* @param uniqueKeys whether the keys added must be unique
|
|
* @param metadataKeys (optional) names of metadata keys that will be included in tree
|
|
*/
|
|
constructor(maxEntriesPerNode, uniqueKeys, metadataKeys = []) {
|
|
this.maxEntriesPerNode = maxEntriesPerNode;
|
|
this.uniqueKeys = uniqueKeys;
|
|
this.metadataKeys = metadataKeys;
|
|
this.root = new tree_leaf_1.BPlusTreeLeaf(this);
|
|
this.depth = 1;
|
|
this.fillFactor = 100;
|
|
}
|
|
/**
|
|
* Adds a key/value pair to the tree
|
|
* @param key
|
|
* @param value data to store with the key, max size is 255
|
|
* @param metadata data to include, must contain all keys used in BPlusTree constructor
|
|
* @returns {BPlusTree} returns reference to this tree
|
|
*/
|
|
add(key, value, metadata) {
|
|
// key = _normalizeKey(key); // if (_isIntString(key)) { key = parseInt(key); }
|
|
// Find the leaf to insert to
|
|
let leaf;
|
|
if (this.root instanceof tree_leaf_1.BPlusTreeLeaf) {
|
|
// Root is leaf node (total entries <= maxEntriesPerNode)
|
|
leaf = this.root;
|
|
}
|
|
else {
|
|
// Navigate to the right leaf to add to
|
|
leaf = this.findLeaf(key);
|
|
}
|
|
leaf.add(key, value, metadata);
|
|
return this;
|
|
}
|
|
// TODO: Enable bulk adding of keys: throw away all nodes, append/insert all keys ordered. Upon commit, cut all data into leafs, construct the nodes up onto the root
|
|
// addBulk(arr, commit = false) {
|
|
// // Adds given items in bulk and reconstructs the tree
|
|
// let leaf = this.firstLeaf();
|
|
// while(leaf) {
|
|
// leaf = leaf.getNext()
|
|
// }
|
|
// }
|
|
/**
|
|
* Finds the relevant leaf for a key
|
|
* @param key
|
|
* @returns returns the leaf the key is in, or would be in when present
|
|
*/
|
|
findLeaf(key) {
|
|
const findLeaf = (node) => {
|
|
if (node instanceof tree_leaf_1.BPlusTreeLeaf) {
|
|
return node;
|
|
}
|
|
for (let i = 0; i < node.entries.length; i++) {
|
|
const entry = node.entries[i];
|
|
if ((0, typesafe_compare_1._isLess)(key, entry.key)) {
|
|
node = entry.ltChild;
|
|
if (!node) {
|
|
return null;
|
|
}
|
|
if (node instanceof tree_leaf_1.BPlusTreeLeaf) {
|
|
return node;
|
|
}
|
|
else {
|
|
return findLeaf(node);
|
|
}
|
|
}
|
|
}
|
|
// Still here? key must be >= last entry
|
|
console.assert((0, typesafe_compare_1._isMoreOrEqual)(key, node.entries[node.entries.length - 1].key));
|
|
return findLeaf(node.gtChild);
|
|
};
|
|
return findLeaf(this.root);
|
|
}
|
|
find(key) {
|
|
const leaf = this.findLeaf(key);
|
|
const entry = leaf.entries.find(entry => (0, typesafe_compare_1._isEqual)(entry.key, key));
|
|
if (!entry) {
|
|
return null;
|
|
}
|
|
if (this.uniqueKeys) {
|
|
return entry.values[0];
|
|
}
|
|
else {
|
|
return entry.values;
|
|
}
|
|
}
|
|
search(op, val) {
|
|
if (['in', '!in', 'between', '!between'].includes(op) && !(val instanceof Array)) {
|
|
// val must be an array
|
|
throw new TypeError(`val must be an array when using operator ${op}`);
|
|
}
|
|
else if (val instanceof Array) {
|
|
throw new TypeError(`val cannot be an array when using operator ${op}`);
|
|
}
|
|
if (['exists', '!exists'].includes(op)) {
|
|
// These operators are a bit strange: they return results for key [undefined] (op === "!exists"), or all other keys (op === "exists")
|
|
// search("exists", ..) executes ("!=", undefined)
|
|
// search("!exists", ..) executes ("==", undefined)
|
|
op = op === 'exists' ? '!=' : '==';
|
|
val = undefined;
|
|
}
|
|
if (val === null) {
|
|
val = undefined;
|
|
}
|
|
const results = [];
|
|
const add = (entry) => {
|
|
const obj = { key: entry.key };
|
|
if (this.uniqueKeys) { // if (this.uniqueValues) {
|
|
// Bug discovered during TS port
|
|
obj.value = entry.values[0];
|
|
}
|
|
else {
|
|
obj.values = entry.values;
|
|
}
|
|
results.push(obj);
|
|
};
|
|
if (['<', '<='].includes(op)) {
|
|
let leaf = this.findLeaf(val);
|
|
while (leaf) {
|
|
for (let i = leaf.entries.length - 1; i >= 0; i--) {
|
|
const entry = leaf.entries[i];
|
|
if (op === '<=' && (0, typesafe_compare_1._isLessOrEqual)(entry.key, val)) {
|
|
add(entry);
|
|
}
|
|
else if (op === '<' && (0, typesafe_compare_1._isLess)(entry.key, val)) {
|
|
add(entry);
|
|
}
|
|
}
|
|
leaf = leaf.prevLeaf;
|
|
}
|
|
}
|
|
else if (['>', '>='].includes(op)) {
|
|
let leaf = this.findLeaf(val);
|
|
while (leaf) {
|
|
for (let i = 0; i < leaf.entries.length; i++) {
|
|
const entry = leaf.entries[i];
|
|
if (op === '>=' && (0, typesafe_compare_1._isMoreOrEqual)(entry.key, val)) {
|
|
add(entry);
|
|
}
|
|
else if (op === '>' && (0, typesafe_compare_1._isMore)(entry.key, val)) {
|
|
add(entry);
|
|
}
|
|
}
|
|
leaf = leaf.nextLeaf;
|
|
}
|
|
}
|
|
else if (op === '==') {
|
|
const leaf = this.findLeaf(val);
|
|
const entry = leaf.entries.find(entry => (0, typesafe_compare_1._isEqual)(entry.key, val)); // entry.key === val
|
|
if (entry) {
|
|
add(entry);
|
|
}
|
|
}
|
|
else if (op === '!=') {
|
|
// Full index scan needed
|
|
let leaf = this.firstLeaf();
|
|
while (leaf) {
|
|
for (let i = 0; i < leaf.entries.length; i++) {
|
|
const entry = leaf.entries[i];
|
|
if ((0, typesafe_compare_1._isNotEqual)(entry.key, val)) {
|
|
add(entry);
|
|
} // entry.key !== val
|
|
}
|
|
leaf = leaf.nextLeaf;
|
|
}
|
|
}
|
|
else if (op === 'in') {
|
|
const sorted = val.slice().sort();
|
|
let searchKey = sorted.shift();
|
|
let leaf; // = this.findLeaf(searchKey);
|
|
let trySameLeaf = false;
|
|
while (searchKey) {
|
|
if (!trySameLeaf) {
|
|
leaf = this.findLeaf(searchKey);
|
|
}
|
|
const entry = leaf.entries.find(entry => (0, typesafe_compare_1._isEqual)(entry.key, val)); // entry.key === searchKey
|
|
if (!entry && trySameLeaf) {
|
|
trySameLeaf = false;
|
|
continue;
|
|
}
|
|
if (entry) {
|
|
add(entry);
|
|
}
|
|
searchKey = sorted.shift();
|
|
trySameLeaf = true;
|
|
}
|
|
}
|
|
else if (op === '!in') {
|
|
// Full index scan needed
|
|
const keys = val;
|
|
let leaf = this.firstLeaf();
|
|
while (leaf) {
|
|
for (let i = 0; i < leaf.entries.length; i++) {
|
|
const entry = leaf.entries[i];
|
|
if (keys.findIndex(val => (0, typesafe_compare_1._isEqual)(entry.key, val)) < 0) {
|
|
add(entry);
|
|
} //if (keys.indexOf(entry.key) < 0) { add(entry); }
|
|
}
|
|
leaf = leaf.nextLeaf;
|
|
}
|
|
}
|
|
else if (op === 'between') {
|
|
const keys = val;
|
|
let bottom = keys[0], top = keys[1];
|
|
if (top < bottom) {
|
|
const swap = top;
|
|
top = bottom;
|
|
bottom = swap;
|
|
}
|
|
let leaf = this.findLeaf(bottom);
|
|
let stop = false;
|
|
while (!stop && leaf) {
|
|
for (let i = 0; i < leaf.entries.length; i++) {
|
|
const entry = leaf.entries[i];
|
|
if ((0, typesafe_compare_1._isMoreOrEqual)(entry.key, bottom) && (0, typesafe_compare_1._isLessOrEqual)(entry.key, top)) {
|
|
add(entry);
|
|
}
|
|
if ((0, typesafe_compare_1._isMore)(entry.key, top)) {
|
|
stop = true;
|
|
break;
|
|
}
|
|
}
|
|
leaf = leaf.nextLeaf;
|
|
}
|
|
}
|
|
else if (op === '!between') {
|
|
// Equal to key < bottom || key > top
|
|
const keys = val;
|
|
let bottom = keys[0], top = keys[1];
|
|
if (top < bottom) {
|
|
const swap = top;
|
|
top = bottom;
|
|
bottom = swap;
|
|
}
|
|
// Add lower range first, lowest value < val < bottom
|
|
let leaf = this.firstLeaf();
|
|
let stop = false;
|
|
while (leaf && !stop) {
|
|
for (let i = 0; i < leaf.entries.length; i++) {
|
|
const entry = leaf.entries[i];
|
|
if ((0, typesafe_compare_1._isLess)(entry.key, bottom)) {
|
|
add(entry);
|
|
}
|
|
else {
|
|
stop = true;
|
|
break;
|
|
}
|
|
}
|
|
leaf = leaf.nextLeaf;
|
|
}
|
|
// Now add upper range, top < val < highest value
|
|
leaf = this.findLeaf(top);
|
|
while (leaf) {
|
|
for (let i = 0; i < leaf.entries.length; i++) {
|
|
const entry = leaf.entries[i];
|
|
if ((0, typesafe_compare_1._isMore)(entry.key, top)) {
|
|
add(entry);
|
|
}
|
|
}
|
|
leaf = leaf.nextLeaf;
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
/**
|
|
* @returns {BPlusTreeLeaf} the first leaf in the tree
|
|
*/
|
|
firstLeaf() {
|
|
// Get the very first leaf
|
|
let node = this.root;
|
|
while (!(node instanceof tree_leaf_1.BPlusTreeLeaf)) {
|
|
node = node.entries[0].ltChild;
|
|
}
|
|
return node;
|
|
}
|
|
/**
|
|
* @returns {BPlusTreeLeaf} the last leaf in the tree
|
|
*/
|
|
lastLeaf() {
|
|
// Get the very last leaf
|
|
let node = this.root;
|
|
while (!(node instanceof tree_leaf_1.BPlusTreeLeaf)) {
|
|
node = node.gtChild;
|
|
}
|
|
return node;
|
|
}
|
|
all() {
|
|
// Get the very first leaf
|
|
let leaf = this.firstLeaf();
|
|
// Now iterate through all the leafs
|
|
const all = [];
|
|
while (leaf) {
|
|
all.push(...leaf.entries.map(entry => entry.key));
|
|
leaf = leaf.nextLeaf; //leaf.next();
|
|
}
|
|
return all;
|
|
}
|
|
reverseAll() {
|
|
// Get the very last leaf
|
|
let leaf = this.lastLeaf();
|
|
// Now iterate through all the leafs (backwards)
|
|
const all = [];
|
|
while (leaf) {
|
|
all.push(...leaf.entries.map(entry => entry.key));
|
|
leaf = leaf.prevLeaf;
|
|
}
|
|
return all;
|
|
}
|
|
static get debugBinary() { return false; }
|
|
static addBinaryDebugString(str, byte) {
|
|
if (this.debugBinary) {
|
|
return [str, byte];
|
|
}
|
|
else {
|
|
return byte;
|
|
}
|
|
}
|
|
static getKeyFromBinary(bytes, index) {
|
|
// key_type:
|
|
const keyType = bytes[index];
|
|
index++;
|
|
// key_length:
|
|
const keyLength = bytes[index];
|
|
index++;
|
|
// key_data:
|
|
let keyData = bytes.slice(index, index + keyLength); // [];
|
|
index += keyLength;
|
|
if ([binary_tree_builder_1.KEY_TYPE.NUMBER, binary_tree_builder_1.KEY_TYPE.BIGINT, binary_tree_builder_1.KEY_TYPE.DATE].includes(keyType)) {
|
|
keyData = Array.from(keyData);
|
|
}
|
|
let key;
|
|
switch (keyType) {
|
|
case binary_tree_builder_1.KEY_TYPE.UNDEFINED: {
|
|
// no need to do this: key = undefined;
|
|
break;
|
|
}
|
|
case binary_tree_builder_1.KEY_TYPE.STRING: {
|
|
key = decodeString(keyData); // textDecoder.decode(Uint8Array.from(keyData));
|
|
// key = keyData.reduce((k, code) => k + String.fromCharCode(code), "");
|
|
break;
|
|
}
|
|
case binary_tree_builder_1.KEY_TYPE.NUMBER: {
|
|
if (keyData.length < 8) {
|
|
// Append trailing 0's
|
|
if (keyData instanceof Array) {
|
|
keyData.push(...[0, 0, 0, 0, 0, 0, 0, 0].slice(keyData.length));
|
|
}
|
|
else {
|
|
throw new Error(`Issue found during TS port: keyData is a Buffer, type is NUMBER and its length < 8, so it needs 0's padding`);
|
|
}
|
|
}
|
|
key = bytesToNumber(keyData);
|
|
break;
|
|
}
|
|
case binary_tree_builder_1.KEY_TYPE.BIGINT: {
|
|
key = bytesToBigint(keyData);
|
|
break;
|
|
}
|
|
case binary_tree_builder_1.KEY_TYPE.BOOLEAN: {
|
|
key = keyData[0] === 1;
|
|
break;
|
|
}
|
|
case binary_tree_builder_1.KEY_TYPE.DATE: {
|
|
key = new Date(bytesToNumber(keyData));
|
|
break;
|
|
}
|
|
default: {
|
|
throw new detailed_error_1.DetailedError('unknown-key-type', `Unknown key type ${keyType}`);
|
|
}
|
|
}
|
|
return { key, length: keyLength, byteLength: keyLength + 2 };
|
|
}
|
|
static getBinaryKeyData(key) {
|
|
// TODO: Deprecate, moved to BinaryBPlusTreeBuilder.getKeyBytes
|
|
let keyBytes = [];
|
|
let keyType = binary_tree_builder_1.KEY_TYPE.UNDEFINED;
|
|
switch (typeof key) {
|
|
case 'undefined': {
|
|
keyType = binary_tree_builder_1.KEY_TYPE.UNDEFINED;
|
|
break;
|
|
}
|
|
case 'string': {
|
|
keyType = binary_tree_builder_1.KEY_TYPE.STRING;
|
|
keyBytes = Array.from(encodeString(key)); // textEncoder.encode(key)
|
|
break;
|
|
}
|
|
case 'number': {
|
|
keyType = binary_tree_builder_1.KEY_TYPE.NUMBER;
|
|
keyBytes = numberToBytes(key);
|
|
// Remove trailing 0's to reduce size for smaller and integer values
|
|
while (keyBytes[keyBytes.length - 1] === 0) {
|
|
keyBytes.pop();
|
|
}
|
|
break;
|
|
}
|
|
case 'bigint': {
|
|
keyType = binary_tree_builder_1.KEY_TYPE.BIGINT;
|
|
keyBytes = bigintToBytes(key);
|
|
break;
|
|
}
|
|
case 'boolean': {
|
|
keyType = binary_tree_builder_1.KEY_TYPE.BOOLEAN;
|
|
keyBytes = [key ? 1 : 0];
|
|
break;
|
|
}
|
|
case 'object': {
|
|
if (key instanceof Date) {
|
|
keyType = binary_tree_builder_1.KEY_TYPE.DATE;
|
|
keyBytes = numberToBytes(key.getTime());
|
|
}
|
|
else {
|
|
throw new detailed_error_1.DetailedError('invalid-object-key-type', 'Unsupported object key type');
|
|
}
|
|
break;
|
|
}
|
|
default: {
|
|
throw new detailed_error_1.DetailedError('invalid-key-type', `Unsupported key type: ${typeof key}`);
|
|
}
|
|
}
|
|
const bytes = [];
|
|
// key_type:
|
|
bytes.push(keyType);
|
|
// key_length:
|
|
bytes.push(keyBytes.length);
|
|
// key_data:
|
|
bytes.push(...keyBytes);
|
|
return bytes;
|
|
}
|
|
async toBinary(keepFreeSpace = false, writer) {
|
|
// TODO: Refactor to use BinaryBPlusTreeBuilder, .getHeader()
|
|
if (!(writer instanceof binary_writer_1.BinaryWriter)) {
|
|
throw new Error('writer argument must be an instance of BinaryWriter');
|
|
}
|
|
// Return binary data
|
|
const indexTypeFlags = (this.uniqueKeys ? binary_tree_builder_1.FLAGS.UNIQUE_KEYS : 0)
|
|
| (this.metadataKeys.length > 0 ? binary_tree_builder_1.FLAGS.HAS_METADATA : 0)
|
|
| (keepFreeSpace ? binary_tree_builder_1.FLAGS.HAS_FREE_SPACE : 0)
|
|
| binary_tree_builder_1.FLAGS.HAS_FILL_FACTOR
|
|
| (config_1.WRITE_SMALL_LEAFS ? binary_tree_builder_1.FLAGS.HAS_SMALL_LEAFS : 0)
|
|
| binary_tree_builder_1.FLAGS.HAS_LARGE_PTRS;
|
|
const bytes = [
|
|
// byte_length:
|
|
0, 0, 0, 0,
|
|
// index_type:
|
|
indexTypeFlags,
|
|
// max_node_entries:
|
|
this.maxEntriesPerNode,
|
|
// fill_factor:
|
|
this.fillFactor,
|
|
];
|
|
if (keepFreeSpace) {
|
|
bytes.push(0, 0, 0, 0); // free_byte_length
|
|
}
|
|
if (this.metadataKeys.length > 0) {
|
|
// metadata_keys:
|
|
const index = bytes.length;
|
|
bytes.push(0, 0, 0, 0); // metadata_length
|
|
// metadata_key_count:
|
|
bytes.push(this.metadataKeys.length);
|
|
this.metadataKeys.forEach(key => {
|
|
// metadata_key:
|
|
bytes.push(key.length); // metadata_key_length
|
|
// metadata_key_name:
|
|
for (let i = 0; i < key.length; i++) {
|
|
bytes.push(key.charCodeAt(i));
|
|
}
|
|
});
|
|
// update metadata_length:
|
|
const length = bytes.length - index - 4;
|
|
(0, binary_1.writeByteLength)(bytes, index, length);
|
|
}
|
|
const headerLength = bytes.length;
|
|
await writer.append(bytes);
|
|
const { references } = await this.root.toBinary(keepFreeSpace, writer);
|
|
console.assert(references.length === 0, 'All references must be resolved now');
|
|
let freeBytesLength = 0;
|
|
if (keepFreeSpace) {
|
|
// Add 10% free space
|
|
freeBytesLength = Math.ceil((writer.length - headerLength) * 0.1);
|
|
const bytesPerWrite = 1024 * 100; // 100KB per write seems fair?
|
|
const writes = Math.ceil(freeBytesLength / bytesPerWrite);
|
|
for (let i = 0; i < writes; i++) {
|
|
const length = i + 1 < writes
|
|
? bytesPerWrite
|
|
: freeBytesLength % bytesPerWrite;
|
|
const zeroes = new Uint8Array(length);
|
|
await writer.append(zeroes);
|
|
}
|
|
}
|
|
// update byte_length:
|
|
const byteLength = writer.length; // - headerLength;
|
|
const lbytes = (0, binary_1.writeByteLength)([], 0, byteLength);
|
|
await writer.write(lbytes, 0);
|
|
if (keepFreeSpace) {
|
|
// update free_byte_length:
|
|
const fbytes = (0, binary_1.writeByteLength)([], 0, freeBytesLength);
|
|
await writer.write(fbytes, 7);
|
|
}
|
|
await writer.end();
|
|
}
|
|
static get typeSafeComparison() {
|
|
return {
|
|
isMore(val1, val2) { return (0, typesafe_compare_1._isMore)(val1, val2); },
|
|
isMoreOrEqual(val1, val2) { return (0, typesafe_compare_1._isMoreOrEqual)(val1, val2); },
|
|
isLess(val1, val2) { return (0, typesafe_compare_1._isLess)(val1, val2); },
|
|
isLessOrEqual(val1, val2) { return (0, typesafe_compare_1._isLessOrEqual)(val1, val2); },
|
|
isEqual(val1, val2) { return (0, typesafe_compare_1._isEqual)(val1, val2); },
|
|
isNotEqual(val1, val2) { return (0, typesafe_compare_1._isNotEqual)(val1, val2); },
|
|
};
|
|
}
|
|
}
|
|
exports.BPlusTree = BPlusTree;
|
|
|
|
},{"../binary":212,"../detailed-error":238,"./binary-tree-builder":215,"./binary-writer":224,"./config":225,"./tree-leaf":230,"./typesafe-compare":235,"acebase-core":12}],234:[function(require,module,exports){
|
|
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.TX = exports.TxDetailedError = void 0;
|
|
const detailed_error_1 = require("../detailed-error");
|
|
class TxDetailedError extends detailed_error_1.DetailedError {
|
|
constructor(code, msg, originalError) {
|
|
super(code, msg, originalError);
|
|
this.transactionErrors = null;
|
|
this.rollbackErrors = null;
|
|
}
|
|
}
|
|
exports.TxDetailedError = TxDetailedError;
|
|
class TX {
|
|
constructor() {
|
|
this._queue = [];
|
|
this._rollbackSteps = [];
|
|
}
|
|
// TODO: refactor to async
|
|
run(action, rollback) {
|
|
console.assert(this._queue.length === 0, 'queue must be empty');
|
|
typeof rollback === 'function' && this._rollbackSteps.push(rollback);
|
|
const p = action instanceof Promise ? action : action();
|
|
return p.catch((err) => {
|
|
console.error(`TX.run error: ${err.message}. Initiating rollback`);
|
|
// rollback
|
|
const steps = this._rollbackSteps.map(step => step());
|
|
return Promise.all(steps)
|
|
.then(() => {
|
|
// rollback successful
|
|
throw err; // run().catch will fire with the original error
|
|
})
|
|
.catch(err2 => {
|
|
// rollback failed!!
|
|
console.error(`Critical: could not rollback changes. Error: ${err2.message}`);
|
|
err.rollbackError = err2;
|
|
throw err;
|
|
});
|
|
});
|
|
}
|
|
/**
|
|
* For parallel transactions
|
|
*/
|
|
queue(step) {
|
|
this._queue.push({
|
|
name: step.name || `Step ${this._queue.length + 1}`,
|
|
action: step.action,
|
|
rollback: step.rollback,
|
|
state: 'idle',
|
|
error: null,
|
|
});
|
|
}
|
|
async execute(parallel = true) {
|
|
if (!parallel) {
|
|
// Sequentially run actions in queue
|
|
const rollbackSteps = [];
|
|
let result;
|
|
while (this._queue.length > 0) {
|
|
const step = this._queue.shift();
|
|
rollbackSteps.push(step.rollback);
|
|
try {
|
|
const prevResult = result;
|
|
result = await step.action(prevResult);
|
|
}
|
|
catch (err) {
|
|
// rollback
|
|
const actions = rollbackSteps.map(step => step());
|
|
await Promise.all(actions)
|
|
.catch(err2 => {
|
|
// rollback failed!!
|
|
console.error(`Critical: could not rollback changes. Error: ${err2.message}`);
|
|
err.rollbackError = err2;
|
|
throw err;
|
|
});
|
|
// rollback successful
|
|
throw err; // execute().catch will fire with the original error
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
// Run actions in parallel:
|
|
const executeStepAction = async (step, action) => {
|
|
try {
|
|
const promise = step[action]();
|
|
if (!(promise instanceof Promise)) {
|
|
throw new detailed_error_1.DetailedError('invalid-tx-step-code', `step "${step.name}" action "${action}" must return a promise`);
|
|
}
|
|
const result = await promise;
|
|
step.state = 'success';
|
|
step.result = result;
|
|
}
|
|
catch (err) {
|
|
step.state = 'failed';
|
|
step.error = err;
|
|
}
|
|
return step;
|
|
};
|
|
const actions = this._queue.map(step => executeStepAction(step, 'action'));
|
|
let results = await Promise.all(actions);
|
|
// Check if they were all successful
|
|
let success = results.every(step => step.state === 'success');
|
|
if (success) {
|
|
return;
|
|
}
|
|
// Rollback
|
|
const transactionErrors = results.filter(step => step.state === 'failed').map(result => result.error);
|
|
// console.warn(`Rolling back tx: `, transactionErrors);
|
|
const rollbackSteps = this._queue.filter(step => typeof step.rollback === 'function').map(step => executeStepAction(step, 'rollback')); // this._queue.map(step => step.state === 'failed' || typeof step.rollback !== 'function' ? null : step.rollback());
|
|
results = await Promise.all(rollbackSteps);
|
|
// Check if rollback was successful
|
|
success = results.every(step => step.state === 'success');
|
|
if (success) {
|
|
const err = new TxDetailedError('tx-failed', 'Tx failed, rolled back. See .info for details');
|
|
err.transactionErrors = transactionErrors;
|
|
throw err;
|
|
}
|
|
// rollback failed!!
|
|
const err = new TxDetailedError('tx-rollback-failed', 'Critical: could not rollback failed transaction. See transactionErrors and rollbackErrors for details');
|
|
err.transactionErrors = transactionErrors;
|
|
err.rollbackErrors = results.filter(step => step.state === 'failed').map(result => result.error);
|
|
console.error('Critical: could not rollback transaction. Errors:', err.rollbackErrors);
|
|
throw err;
|
|
}
|
|
}
|
|
exports.TX = TX;
|
|
|
|
},{"../detailed-error":238}],235:[function(require,module,exports){
|
|
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports._compareBinary = exports._sortCompare = exports._isMoreOrEqual = exports._isMore = exports._isLessOrEqual = exports._isLess = exports._isNotEqual = exports._isEqual = exports._getComparibleValue = void 0;
|
|
function _getComparibleValue(val) {
|
|
if (typeof val === 'undefined' || val === null) {
|
|
val = null;
|
|
}
|
|
else if (val instanceof Date) {
|
|
val = val.getTime();
|
|
}
|
|
return val;
|
|
}
|
|
exports._getComparibleValue = _getComparibleValue;
|
|
function _isEqual(val1, val2) {
|
|
val1 = _getComparibleValue(val1);
|
|
val2 = _getComparibleValue(val2);
|
|
if (typeof val1 !== typeof val2) {
|
|
return false;
|
|
}
|
|
return val1 === val2;
|
|
}
|
|
exports._isEqual = _isEqual;
|
|
function _isNotEqual(val1, val2) {
|
|
val1 = _getComparibleValue(val1);
|
|
val2 = _getComparibleValue(val2);
|
|
if (typeof val1 !== typeof val2) {
|
|
return true;
|
|
}
|
|
return val1 != val2;
|
|
}
|
|
exports._isNotEqual = _isNotEqual;
|
|
function _isLess(val1, val2) {
|
|
val1 = _getComparibleValue(val1);
|
|
val2 = _getComparibleValue(val2);
|
|
if (val2 === null) {
|
|
return false;
|
|
}
|
|
if (val1 === null) {
|
|
return val2 !== null;
|
|
}
|
|
if (typeof val1 !== typeof val2) {
|
|
return typeof val1 < typeof val2;
|
|
} // boolean, number (+Dates), string
|
|
return val1 < val2;
|
|
}
|
|
exports._isLess = _isLess;
|
|
function _isLessOrEqual(val1, val2) {
|
|
val1 = _getComparibleValue(val1);
|
|
val2 = _getComparibleValue(val2);
|
|
if (val1 === null) {
|
|
return true;
|
|
}
|
|
else if (val2 === null) {
|
|
return false;
|
|
}
|
|
if (typeof val1 !== typeof val2) {
|
|
return typeof val1 < typeof val2;
|
|
} // boolean, number (+Dates), string
|
|
return val1 <= val2;
|
|
}
|
|
exports._isLessOrEqual = _isLessOrEqual;
|
|
function _isMore(val1, val2) {
|
|
val1 = _getComparibleValue(val1);
|
|
val2 = _getComparibleValue(val2);
|
|
if (val1 === null) {
|
|
return false;
|
|
}
|
|
else if (val2 === null) {
|
|
return true;
|
|
}
|
|
if (typeof val1 !== typeof val2) {
|
|
return typeof val1 > typeof val2;
|
|
} // boolean, number (+Dates), string
|
|
return val1 > val2;
|
|
}
|
|
exports._isMore = _isMore;
|
|
function _isMoreOrEqual(val1, val2) {
|
|
val1 = _getComparibleValue(val1);
|
|
val2 = _getComparibleValue(val2);
|
|
if (val1 === null) {
|
|
return val2 === null;
|
|
}
|
|
else if (val2 === null) {
|
|
return true;
|
|
}
|
|
if (typeof val1 !== typeof val2) {
|
|
return typeof val1 > typeof val2;
|
|
} // boolean, number (+Dates), string
|
|
return val1 >= val2;
|
|
}
|
|
exports._isMoreOrEqual = _isMoreOrEqual;
|
|
function _sortCompare(val1, val2) {
|
|
val1 = _getComparibleValue(val1);
|
|
val2 = _getComparibleValue(val2);
|
|
if (val1 === null && val2 !== null) {
|
|
return -1;
|
|
}
|
|
if (val1 !== null && val2 === null) {
|
|
return 1;
|
|
}
|
|
if (typeof val1 !== typeof val2) {
|
|
// boolean, number (+Dates), string
|
|
if (typeof val1 < typeof val2) {
|
|
return -1;
|
|
}
|
|
if (typeof val1 > typeof val2) {
|
|
return 1;
|
|
}
|
|
}
|
|
if (val1 < val2) {
|
|
return -1;
|
|
}
|
|
if (val1 > val2) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
exports._sortCompare = _sortCompare;
|
|
function _compareBinary(val1, val2) {
|
|
return val1.length === val2.length && val1.every((byte, index) => val2[index] === byte);
|
|
}
|
|
exports._compareBinary = _compareBinary;
|
|
|
|
},{}],236:[function(require,module,exports){
|
|
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports._appendToArray = exports._checkNewEntryArgs = void 0;
|
|
function _checkNewEntryArgs(key, recordPointer, metadataKeys, metadata) {
|
|
const storageTypesText = 'supported types are string, number, boolean, Date and undefined';
|
|
const isStorableType = (val) => {
|
|
return ['number', 'string', 'boolean', 'bigint', 'undefined'].includes(typeof val) || val instanceof Date;
|
|
};
|
|
if (!isStorableType(key)) {
|
|
return new TypeError(`key contains a value that cannot be stored. ${storageTypesText}`);
|
|
}
|
|
if (!(recordPointer instanceof Array || recordPointer instanceof Uint8Array)) {
|
|
return new TypeError('recordPointer must be a byte array or Uint8Array');
|
|
}
|
|
if (recordPointer.length > 255) {
|
|
return new Error('Unable to store recordPointers larger than 255 bytes'); // binary restriction
|
|
}
|
|
// Check if all metadata keys are present and have valid data
|
|
try {
|
|
metadataKeys && metadataKeys.forEach(key => {
|
|
if (!(key in metadata)) {
|
|
throw new TypeError(`metadata must include key "${key}"`);
|
|
}
|
|
if (!isStorableType(typeof metadata[key])) {
|
|
throw new TypeError(`metadata "${key}" contains a value that cannot be stored. ${storageTypesText}`);
|
|
}
|
|
});
|
|
}
|
|
catch (err) {
|
|
return err;
|
|
}
|
|
}
|
|
exports._checkNewEntryArgs = _checkNewEntryArgs;
|
|
const _appendToArray = (targetArray, arr2) => {
|
|
const n = 255;
|
|
let start = 0;
|
|
while (start < arr2.length) {
|
|
targetArray.push(...arr2.slice(start, start + n));
|
|
start += n;
|
|
}
|
|
};
|
|
exports._appendToArray = _appendToArray;
|
|
|
|
},{}],237:[function(require,module,exports){
|
|
(function (Buffer){(function (){
|
|
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.GeoIndex = exports.FullTextIndex = exports.ArrayIndex = exports.IndexQueryResults = exports.IndexQueryResult = exports.DataIndex = void 0;
|
|
const acebase_core_1 = require("acebase-core");
|
|
const unidecode_1 = require("unidecode");
|
|
const node_1 = require("../node");
|
|
const btree_1 = require("../btree");
|
|
const Geohash = require("../geohash");
|
|
const promise_fs_1 = require("../promise-fs");
|
|
const thread_safe_1 = require("../thread-safe");
|
|
const node_value_types_1 = require("../node-value-types");
|
|
const quicksort_1 = require("../quicksort");
|
|
const { compareValues, getChildValues, numberToBytes, bigintToBytes, bytesToNumber, bytesToBigint, encodeString, decodeString } = acebase_core_1.Utils;
|
|
const DISK_BLOCK_SIZE = 4096; // use 512 for older disks
|
|
const FILL_FACTOR = 50; // leave room for inserts
|
|
const INDEX_INFO_VALUE_TYPE = {
|
|
UNDEFINED: 0,
|
|
STRING: 1,
|
|
NUMBER: 2,
|
|
BOOLEAN: 3,
|
|
ARRAY: 4,
|
|
// Maybe in the future:
|
|
// OBJECT: 5,
|
|
};
|
|
function _createRecordPointer(wildcards, keyOrIndex) {
|
|
// binary layout:
|
|
// record_pointer = wildcards_info, key_info, DEPRECATED: record_location
|
|
// wildcards_info = wildcards_length, wildcards
|
|
// wildcards_length = 1 byte (nr of wildcard values)
|
|
// wildcards = wilcard[wildcards_length]
|
|
// wildcard = wilcard_length, wilcard_bytes
|
|
// wildcard_length = 1 byte
|
|
// wildcard_value = byte[wildcard_length] (ASCII char codes)
|
|
// key_info = key_length, key_bytes
|
|
// key_length = 1 byte
|
|
// key_bytes = byte[key_length] (ASCII char codes)
|
|
// NOT USED, DEPRECATED:
|
|
// record_location = page_nr, record_nr
|
|
// page_nr = 4 byte number
|
|
// record_nr = 2 byte number
|
|
const recordPointer = [wildcards.length]; // wildcards_length
|
|
for (let i = 0; i < wildcards.length; i++) {
|
|
const wildcard = wildcards[i];
|
|
recordPointer.push(wildcard.length); // wildcard_length
|
|
// wildcard_bytes:
|
|
for (let j = 0; j < wildcard.length; j++) {
|
|
recordPointer.push(wildcard.charCodeAt(j));
|
|
}
|
|
}
|
|
const key = typeof keyOrIndex === 'number' ? `[${keyOrIndex}]` : keyOrIndex;
|
|
recordPointer.push(key.length); // key_length
|
|
// key_bytes:
|
|
for (let i = 0; i < key.length; i++) {
|
|
recordPointer.push(key.charCodeAt(i));
|
|
}
|
|
// // page_nr:
|
|
// recordPointer.push((address.pageNr >> 24) & 0xff);
|
|
// recordPointer.push((address.pageNr >> 16) & 0xff);
|
|
// recordPointer.push((address.pageNr >> 8) & 0xff);
|
|
// recordPointer.push(address.pageNr & 0xff);
|
|
// // record_nr:
|
|
// recordPointer.push((address.recordNr >> 8) & 0xff);
|
|
// recordPointer.push(address.recordNr & 0xff);
|
|
return recordPointer;
|
|
}
|
|
function _parseRecordPointer(path, recordPointer) {
|
|
if (recordPointer.length === 0) {
|
|
throw new Error('Invalid record pointer length');
|
|
}
|
|
const wildcardsLength = recordPointer[0];
|
|
const wildcards = [];
|
|
let index = 1;
|
|
for (let i = 0; i < wildcardsLength; i++) {
|
|
let wildcard = '';
|
|
const length = recordPointer[index];
|
|
for (let j = 0; j < length; j++) {
|
|
wildcard += String.fromCharCode(recordPointer[index + j + 1]);
|
|
}
|
|
wildcards.push(wildcard);
|
|
index += length + 1;
|
|
}
|
|
const keyLength = recordPointer[index];
|
|
let key = '';
|
|
for (let i = 0; i < keyLength; i++) {
|
|
key += String.fromCharCode(recordPointer[index + i + 1]);
|
|
}
|
|
index += keyLength + 1;
|
|
// const pageNr = recordPointer[index] << 24 | recordPointer[index+1] << 16 | recordPointer[index+2] << 8 | recordPointer[index+3];
|
|
// index += 4;
|
|
// const recordNr = recordPointer[index] << 8 | recordPointer[index+1];
|
|
if (wildcards.length > 0) {
|
|
let i = 0;
|
|
path = path.replace(/\*/g, () => {
|
|
const wildcard = wildcards[i];
|
|
i++;
|
|
return wildcard;
|
|
});
|
|
}
|
|
// return { key, pageNr, recordNr, address: new NodeAddress(`${path}/${key}`, pageNr, recordNr) };
|
|
const keyOrIndex = key[0] === '[' && key.slice(-1) === ']' ? parseInt(key.slice(1, -1)) : key;
|
|
return { key: keyOrIndex, path: `${path}/${key}`, wildcards };
|
|
}
|
|
// const _debounceTimeouts = {};
|
|
// function debounce(id, ms, callback) {
|
|
// if (_debounceTimeouts[id]) {
|
|
// clearTimeout(_debounceTimeouts[id]);
|
|
// }
|
|
// _debounceTimeouts[id] = setTimeout(() => {
|
|
// delete _debounceTimeouts[id];
|
|
// callback();
|
|
// }, ms);
|
|
// }
|
|
class DataIndex {
|
|
/**
|
|
* Creates a new index
|
|
*/
|
|
constructor(storage, path, key, options = {}) {
|
|
this.storage = storage;
|
|
this.state = DataIndex.STATE.INIT;
|
|
this._buildError = null;
|
|
this._cache = new Map();
|
|
this._cacheTimeoutSettings = {
|
|
// default: 1 minute query cache
|
|
duration: 60 * 1000,
|
|
sliding: true,
|
|
};
|
|
if (['string', 'undefined'].indexOf(typeof options.include) < 0 && !(options.include instanceof Array)) {
|
|
throw new Error(`includeKeys argument must be a string, an Array of strings, or undefined. Passed type=${typeof options.include}`);
|
|
}
|
|
if (typeof options.include === 'string') {
|
|
options.include = [options.include];
|
|
}
|
|
const pathKeys = acebase_core_1.PathInfo.getPathKeys(path).map(key => typeof key === 'string' && key.startsWith('$') ? '*' : key);
|
|
this.path = (new acebase_core_1.PathInfo(pathKeys)).path; // path.replace(/\/\*$/, ""); // Remove optional trailing "/*"
|
|
this.key = key;
|
|
this.caseSensitive = options.caseSensitive === true;
|
|
this.textLocale = options.textLocale || 'en';
|
|
this.textLocaleKey = options.textLocaleKey;
|
|
this.includeKeys = options.include || [];
|
|
// this.enableReverseLookup = false;
|
|
this.indexMetadataKeys = [];
|
|
this._buildError = null;
|
|
this._updateQueue = [];
|
|
this.trees = {
|
|
'default': {
|
|
fileIndex: 0,
|
|
byteLength: 0,
|
|
class: 'BPlusTree',
|
|
version: 1,
|
|
entries: 0,
|
|
values: 0,
|
|
},
|
|
};
|
|
}
|
|
static get STATE() {
|
|
return {
|
|
INIT: 'init',
|
|
READY: 'ready',
|
|
BUILD: 'build',
|
|
REBUILD: 'rebuild',
|
|
ERROR: 'error',
|
|
REMOVED: 'removed',
|
|
CLOSED: 'closed',
|
|
};
|
|
}
|
|
get allMetadataKeys() {
|
|
return this.includeKeys.concat(this.indexMetadataKeys);
|
|
}
|
|
setCacheTimeout(seconds, sliding = false) {
|
|
this._cacheTimeoutSettings = {
|
|
duration: seconds * 1000,
|
|
sliding,
|
|
};
|
|
}
|
|
cache(op, param, results) {
|
|
const val = JSON.stringify(param); // Make object and array params cachable too
|
|
if (typeof results === 'undefined') {
|
|
// Get from cache
|
|
let cache;
|
|
if (this._cache.has(op) && this._cache.get(op).has(val)) {
|
|
cache = this._cache.get(op).get(val);
|
|
}
|
|
if (cache) {
|
|
cache.reads++;
|
|
if (this._cacheTimeoutSettings.sliding) {
|
|
cache.extendLife();
|
|
}
|
|
return cache.results;
|
|
}
|
|
return null;
|
|
}
|
|
else {
|
|
// Set cache
|
|
let opCache = this._cache.get(op);
|
|
if (!opCache) {
|
|
opCache = new Map();
|
|
this._cache.set(op, opCache);
|
|
}
|
|
// let clear = () => {
|
|
// // this.storage.debug.log(`Index ${this.description}, cache clean for ${op} "${val}"`);
|
|
// opCache.delete(val);
|
|
// }
|
|
const scheduleClear = () => {
|
|
const timeout = setTimeout(() => opCache.delete(val), this._cacheTimeoutSettings.duration);
|
|
timeout.unref && timeout.unref();
|
|
return timeout;
|
|
};
|
|
const cache = {
|
|
results,
|
|
added: Date.now(),
|
|
reads: 0,
|
|
timeout: scheduleClear(),
|
|
extendLife: () => {
|
|
// this.storage.debug.log(`Index ${this.description}, cache lifetime extended for ${op} "${val}". reads: ${cache.reads}`);
|
|
clearTimeout(cache.timeout);
|
|
cache.timeout = scheduleClear();
|
|
},
|
|
};
|
|
opCache.set(val, cache);
|
|
// this.storage.debug.log(`Index ${this.description}, cached ${results.length} results for ${op} "${val}"`);
|
|
}
|
|
}
|
|
async delete() {
|
|
const idx = await this._getTree('exclusive');
|
|
await idx.close();
|
|
const filePath = this.fileName; // `${this.storage.settings.path}/${this.storage.name}.acebase/${this.fileName}`;
|
|
await promise_fs_1.pfs.rm(filePath);
|
|
this.state = DataIndex.STATE.REMOVED;
|
|
idx.release();
|
|
}
|
|
async close() {
|
|
const idx = await this._getTree('exclusive');
|
|
await idx.close();
|
|
this.state = DataIndex.STATE.CLOSED;
|
|
idx.release();
|
|
}
|
|
/**
|
|
* Reads an existing index from a file
|
|
* @param storage Used storage engine
|
|
* @param fileName
|
|
*/
|
|
static async readFromFile(storage, fileName) {
|
|
// Read an index from file
|
|
const filePath = fileName.includes('/') ? fileName : `${storage.settings.path}/${storage.name}.acebase/${fileName}`;
|
|
const fd = await promise_fs_1.pfs.open(filePath, promise_fs_1.pfs.flags.read);
|
|
try {
|
|
// Read signature
|
|
let result = await promise_fs_1.pfs.read(fd, Buffer.alloc(10));
|
|
// Check signature
|
|
if (result.buffer.toString() !== 'ACEBASEIDX') {
|
|
throw new Error(`File "${filePath}" is not an AceBase index. If you get this error after updating acebase, delete the index file and rebuild it`);
|
|
}
|
|
// Read layout_version
|
|
result = await promise_fs_1.pfs.read(fd, Buffer.alloc(1));
|
|
const versionNr = result.buffer[0];
|
|
if (versionNr !== 1) {
|
|
throw new Error(`Index "${filePath}" version ${versionNr} is not supported by this version of AceBase. npm update your acebase packages`);
|
|
}
|
|
// Read header_length
|
|
result = await promise_fs_1.pfs.read(fd, Buffer.alloc(4));
|
|
const headerLength = (result.buffer[0] << 24) | (result.buffer[1] << 16) | (result.buffer[2] << 8) | result.buffer[3];
|
|
// Read header
|
|
result = await promise_fs_1.pfs.read(fd, Buffer.alloc(headerLength - 11));
|
|
// Process header
|
|
const header = Uint8Array.from(result.buffer);
|
|
let index = 0;
|
|
const readKey = () => {
|
|
const keyLength = header[index];
|
|
let keyName = '';
|
|
index++;
|
|
for (let j = 0; j < keyLength; j++) {
|
|
keyName += String.fromCharCode(header[index + j]);
|
|
}
|
|
index += keyLength;
|
|
return keyName;
|
|
};
|
|
const readValue = () => {
|
|
const valueType = header[index];
|
|
index++;
|
|
let valueLength = 0;
|
|
if (valueType === INDEX_INFO_VALUE_TYPE.UNDEFINED) {
|
|
valueLength = 0;
|
|
}
|
|
else if (valueType === INDEX_INFO_VALUE_TYPE.BOOLEAN) {
|
|
// boolean has no value_length
|
|
valueLength = 1;
|
|
}
|
|
else {
|
|
valueLength = (header[index] << 8) | header[index + 1];
|
|
index += 2;
|
|
}
|
|
let value;
|
|
if (valueType === INDEX_INFO_VALUE_TYPE.STRING) {
|
|
value = decodeString(header.slice(index, index + valueLength));
|
|
}
|
|
else if (valueType === INDEX_INFO_VALUE_TYPE.NUMBER) {
|
|
value = bytesToNumber(header.slice(index, index + valueLength));
|
|
}
|
|
else if (valueType === INDEX_INFO_VALUE_TYPE.BOOLEAN) {
|
|
value = header[index] === 1;
|
|
}
|
|
else if (valueType === INDEX_INFO_VALUE_TYPE.ARRAY) {
|
|
const arr = [];
|
|
for (let j = 0; j < valueLength; j++) {
|
|
arr.push(readValue());
|
|
}
|
|
return arr;
|
|
}
|
|
// Maybe in the future:
|
|
// else if (valueType === INDEX_INFO_VALUE_TYPE.OBJECT) {
|
|
// const obj = {} as Record<string, IndexInfoPrimitiveValue>;
|
|
// for (let j = 0; j < valueLength; j++) {
|
|
// const prop = readKey();
|
|
// const val = readValue() as IndexInfoPrimitiveValue;
|
|
// obj[prop] = val;
|
|
// }
|
|
// return obj;
|
|
// }
|
|
index += valueLength;
|
|
return value;
|
|
};
|
|
const readInfo = () => {
|
|
const infoCount = header[index];
|
|
index++;
|
|
const info = {};
|
|
for (let i = 0; i < infoCount; i++) {
|
|
const key = readKey();
|
|
const value = readValue();
|
|
info[key] = value;
|
|
}
|
|
return info;
|
|
};
|
|
const indexInfo = readInfo();
|
|
const indexOptions = {
|
|
caseSensitive: indexInfo.cs,
|
|
textLocale: indexInfo.locale,
|
|
textLocaleKey: indexInfo.localeKey,
|
|
include: indexInfo.include,
|
|
};
|
|
let dataIndex;
|
|
switch (indexInfo.type) {
|
|
case 'normal': {
|
|
dataIndex = new DataIndex(storage, indexInfo.path, indexInfo.key, indexOptions);
|
|
break;
|
|
}
|
|
case 'array': {
|
|
dataIndex = new ArrayIndex(storage, indexInfo.path, indexInfo.key, indexOptions);
|
|
break;
|
|
}
|
|
case 'fulltext': {
|
|
dataIndex = new FullTextIndex(storage, indexInfo.path, indexInfo.key, indexOptions);
|
|
break;
|
|
}
|
|
case 'geo': {
|
|
dataIndex = new GeoIndex(storage, indexInfo.path, indexInfo.key, indexOptions);
|
|
break;
|
|
}
|
|
default: {
|
|
throw new Error(`Unknown index type ${indexInfo.type}`);
|
|
}
|
|
}
|
|
dataIndex._fileName = filePath;
|
|
// trees_info:
|
|
const treesCount = header[index];
|
|
index++;
|
|
for (let i = 0; i < treesCount; i++) {
|
|
// tree_name:
|
|
const treeName = readKey();
|
|
// treeName is "default"
|
|
const treeInfo = dataIndex.trees[treeName] = {};
|
|
// file_index:
|
|
treeInfo.fileIndex = (header[index] << 24) | (header[index + 1] << 16) | (header[index + 2] << 8) | header[index + 3];
|
|
index += 4;
|
|
// byte_length:
|
|
treeInfo.byteLength = (header[index] << 24) | (header[index + 1] << 16) | (header[index + 2] << 8) | header[index + 3];
|
|
index += 4;
|
|
const info = readInfo();
|
|
// info has: class, version, entries, values
|
|
Object.assign(treeInfo, info); //treeInfo.info = info;
|
|
}
|
|
await promise_fs_1.pfs.close(fd);
|
|
dataIndex.state = DataIndex.STATE.READY;
|
|
return dataIndex;
|
|
}
|
|
catch (err) {
|
|
storage.debug.error(err);
|
|
promise_fs_1.pfs.close(fd);
|
|
throw err;
|
|
}
|
|
}
|
|
get type() {
|
|
return 'normal';
|
|
}
|
|
get fileName() {
|
|
if (this._fileName) {
|
|
// Set by readFromFile
|
|
return this._fileName;
|
|
}
|
|
const dir = `${this.storage.settings.path}/${this.storage.name}.acebase`;
|
|
const storagePrefix = this.storage.settings.type !== 'data' ? `[${this.storage.settings.type}]-` : '';
|
|
const escape = (key) => key.replace(/\//g, '~').replace(/\*/g, '#');
|
|
const escapedPath = escape(this.path);
|
|
const escapedKey = escape(this.key);
|
|
const includes = this.includeKeys.length > 0
|
|
? ',' + this.includeKeys.map(key => escape(key)).join(',')
|
|
: '';
|
|
const extension = (this.type !== 'normal' ? `${this.type}.` : '') + 'idx';
|
|
return `${dir}/${storagePrefix}${escapedPath}-${escapedKey}${includes}.${extension}`;
|
|
}
|
|
get description() {
|
|
const keyPath = `/${this.path}/*/${this.key}`;
|
|
const includedKeys = this.includeKeys.length > 0 ? '+' + this.includeKeys.join(',') : '';
|
|
let description = `${keyPath}${includedKeys}`;
|
|
if (this.type !== 'normal') {
|
|
description += ` (${this.type})`;
|
|
}
|
|
return description;
|
|
}
|
|
_getWildcardKeys(path) {
|
|
const pathKeys = acebase_core_1.PathInfo.getPathKeys(path);
|
|
const indexKeys = acebase_core_1.PathInfo.getPathKeys(this.path);
|
|
return indexKeys.reduce((wildcards, key, i) => {
|
|
if (key === '*') {
|
|
wildcards.push(pathKeys[i]);
|
|
}
|
|
return wildcards;
|
|
}, []);
|
|
}
|
|
// _getRevLookupKey(path) {
|
|
// const key = getPathInfo(path).key;
|
|
// const wildcardKeys = this._getWildcardKeys(path);
|
|
// return `:${wildcardKeys.join(':')}${wildcardKeys.length > 0 ? ':' : ''}${key}:`;
|
|
// }
|
|
// _updateReverseLookupKey(path, oldData, newData, metadata) {
|
|
// if (!this.enableReverseLookup) {
|
|
// throw new Error(`This index does not support reverse lookups`)
|
|
// }
|
|
// function areEqual(val1, val2) {
|
|
// return val1.length === val2.length && val1.every((byte, index) => val2[index] === byte);
|
|
// }
|
|
// if (areEqual(oldData, newData)) {
|
|
// // Everything remains the same
|
|
// return;
|
|
// }
|
|
// const revLookupKey = this._getRevLookupKey(path);
|
|
// return this._updateTree(path, revLookupKey, revLookupKey, oldData, newData, metadata);
|
|
// }
|
|
_updateTree(path, oldValue, newValue, oldRecordPointer, newRecordPointer, metadata) {
|
|
const canBeIndexed = ['number', 'boolean', 'string'].indexOf(typeof newValue) >= 0 || newValue instanceof Date;
|
|
const operations = [];
|
|
if (oldValue !== null) {
|
|
const op = btree_1.BinaryBPlusTree.TransactionOperation.remove(oldValue, oldRecordPointer);
|
|
operations.push(op);
|
|
}
|
|
if (newValue !== null && canBeIndexed) {
|
|
const op = btree_1.BinaryBPlusTree.TransactionOperation.add(newValue, newRecordPointer, metadata);
|
|
operations.push(op);
|
|
}
|
|
return this._processTreeOperations(path, operations);
|
|
}
|
|
async _rebuild(idx) {
|
|
// Rebuild by writing to temp file
|
|
const newIndexFile = this.fileName + '.tmp';
|
|
const fd = await promise_fs_1.pfs.open(newIndexFile, promise_fs_1.pfs.flags.write);
|
|
const treeStatistics = {
|
|
byteLength: 0,
|
|
totalEntries: 0,
|
|
totalValues: 0,
|
|
};
|
|
const headerStats = {
|
|
written: false,
|
|
length: 0,
|
|
promise: null,
|
|
updateTreeLength: undefined, //Awaited<ReturnType<this['_writeIndexHeader']>>['treeLengthCallback'],
|
|
};
|
|
const writer = async (data, index) => {
|
|
if (!headerStats.written) {
|
|
// Write header first, or wait until done
|
|
if (!headerStats.promise) {
|
|
headerStats.promise = this._writeIndexHeader(fd, treeStatistics).then(result => {
|
|
headerStats.written = true;
|
|
headerStats.length = result.length;
|
|
headerStats.updateTreeLength = result.treeLengthCallback;
|
|
});
|
|
}
|
|
await headerStats.promise;
|
|
}
|
|
await promise_fs_1.pfs.write(fd, data, 0, data.length, headerStats.length + index);
|
|
};
|
|
this.state = DataIndex.STATE.REBUILD;
|
|
try {
|
|
// this._fst = []; // Reset fst memory
|
|
await idx.tree.rebuild(btree_1.BinaryWriter.forFunction(writer), { treeStatistics });
|
|
await idx.close();
|
|
await headerStats.updateTreeLength(treeStatistics.byteLength);
|
|
await promise_fs_1.pfs.close(fd);
|
|
const renameFile = async (retry = 0) => {
|
|
try {
|
|
// rename new file, overwriting the old file
|
|
await promise_fs_1.pfs.rename(newIndexFile, this.fileName);
|
|
}
|
|
catch (err) {
|
|
// Occasionally getting EPERM "operation not permitted" errors lately with Node 16.
|
|
// Fix: try again after 100ms, up to 10 times
|
|
if (err.code === 'EPERM' && retry < 10) {
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
await renameFile(retry + 1);
|
|
}
|
|
throw err;
|
|
}
|
|
};
|
|
await renameFile();
|
|
this.state = DataIndex.STATE.READY;
|
|
idx.release();
|
|
}
|
|
catch (err) {
|
|
this.storage.debug.error('Index rebuild error: ', err);
|
|
this.state = DataIndex.STATE.ERROR;
|
|
this._buildError = err;
|
|
idx.release();
|
|
throw err;
|
|
}
|
|
}
|
|
async _processTreeOperations(path, operations) {
|
|
const startTime = Date.now();
|
|
if (this._buildError) {
|
|
throw new Error('Cannot update index because there was an error building it');
|
|
}
|
|
let idx = await this._getTree('exclusive');
|
|
// const oldEntry = tree.find(keyValues.oldValue);
|
|
const go = async (retry = 0) => {
|
|
const opsCount = operations.length;
|
|
try {
|
|
await idx.tree.transaction(operations);
|
|
// Index updated
|
|
idx.release();
|
|
return false; // "not rebuilt"
|
|
}
|
|
catch (err) {
|
|
// Could not update index --> leaf full?
|
|
this.storage.debug.verbose(`Could not update index ${this.description}: ${err.message}`.colorize(acebase_core_1.ColorStyle.yellow));
|
|
if (retry > 0 && opsCount === operations.length) {
|
|
throw new Error(`DEV ERROR: unable to process operations because tree was rebuilt, and that didn't help?! --> ${err.stack}`);
|
|
}
|
|
await this._rebuild(idx); // rebuild calls idx.close() and .release()
|
|
// Process left-over operations
|
|
this.storage.debug.verbose('Index was rebuilt, retrying pending operations');
|
|
idx = await this._getTree('exclusive');
|
|
await go(retry + 1);
|
|
return true; // "rebuilt"
|
|
}
|
|
};
|
|
const rebuilt = await go();
|
|
// this.storage.debug.log(`Released update lock on index ${this.description}`.colorize(ColorStyle.blue));
|
|
const doneTime = Date.now();
|
|
const ms = doneTime - startTime;
|
|
const duration = ms < 5000 ? ms + 'ms' : Math.round(ms / 1000) + 's';
|
|
this.storage.debug.verbose(`Index ${this.description} was ${rebuilt ? 'rebuilt' : 'updated'} successfully for "/${path}", took ${duration}`.colorize(acebase_core_1.ColorStyle.green));
|
|
// Process any queued updates
|
|
return await this._processUpdateQueue();
|
|
}
|
|
async _processUpdateQueue() {
|
|
const queue = this._updateQueue.splice(0);
|
|
if (queue.length === 0) {
|
|
return;
|
|
}
|
|
// Invalidate query cache
|
|
this._cache.clear(); // TODO: check which cache results should be adjusted intelligently
|
|
// Process all queued items
|
|
const promises = queue.map(update => {
|
|
return this._updateTree(update.path, update.oldValue, update.newValue, update.recordPointer, update.recordPointer, update.metadata)
|
|
.then(() => {
|
|
update.resolve(); // Resolve waiting promise
|
|
})
|
|
.catch(err => {
|
|
update.reject(err); // Reject waiting promise
|
|
// Do not throw again
|
|
});
|
|
});
|
|
await Promise.all(promises);
|
|
}
|
|
async handleRecordUpdate(path, oldValue, newValue, indexMetadata) {
|
|
var _a;
|
|
const getValues = (key, oldValue, newValue) => acebase_core_1.PathInfo.getPathKeys(key).reduce((values, key) => getChildValues(key, values.oldValue, values.newValue), { oldValue, newValue });
|
|
const updatedKey = acebase_core_1.PathInfo.get(path).key;
|
|
if (typeof updatedKey === 'number') {
|
|
throw new Error('Not implemented: updated key is a number!');
|
|
}
|
|
const keyValues = this.key === '{key}'
|
|
? { oldValue: oldValue === null ? null : updatedKey, newValue: newValue === null ? null : updatedKey }
|
|
: getValues(this.key, oldValue, newValue);
|
|
const includedValues = this.includeKeys.map(key => getValues(key, oldValue, newValue));
|
|
if (!this.caseSensitive) {
|
|
// Convert to locale aware lowercase
|
|
const allValues = [keyValues].concat(includedValues);
|
|
allValues.forEach(values => {
|
|
if (typeof values.oldValue === 'string') {
|
|
values.oldValue = values.oldValue.toLocaleLowerCase(this.textLocale);
|
|
}
|
|
if (typeof values.newValue === 'string') {
|
|
values.newValue = values.newValue.toLocaleLowerCase(this.textLocale);
|
|
}
|
|
});
|
|
}
|
|
const keyValueChanged = compareValues(keyValues.oldValue, keyValues.newValue) !== 'identical';
|
|
const includedValuesChanged = includedValues.some(values => compareValues(values.oldValue, values.newValue) !== 'identical');
|
|
if (!keyValueChanged && !includedValuesChanged) {
|
|
return;
|
|
}
|
|
const wildcardKeys = this._getWildcardKeys(path);
|
|
const recordPointer = _createRecordPointer(wildcardKeys, updatedKey);
|
|
const metadata = (() => {
|
|
const obj = {};
|
|
indexMetadata && Object.assign(obj, indexMetadata);
|
|
if (typeof newValue === 'object' && newValue !== null) {
|
|
this.includeKeys.forEach(key => obj[key] = newValue[key]);
|
|
}
|
|
return obj;
|
|
})();
|
|
if (this.state === DataIndex.STATE.ERROR) {
|
|
throw new Error(`Cannot update index ${this.description}: it's in the error state: ${(_a = this._buildError) === null || _a === void 0 ? void 0 : _a.stack}`);
|
|
}
|
|
else if (this.state === DataIndex.STATE.READY) {
|
|
// Invalidate query cache
|
|
this._cache.clear();
|
|
// Update the tree
|
|
return await this._updateTree(path, keyValues.oldValue, keyValues.newValue, recordPointer, recordPointer, metadata);
|
|
}
|
|
else {
|
|
this.storage.debug.verbose(`Queueing index ${this.description} update for "/${path}"`);
|
|
// Queue the update
|
|
const update = {
|
|
path,
|
|
oldValue: keyValues.oldValue,
|
|
newValue: keyValues.newValue,
|
|
recordPointer,
|
|
metadata,
|
|
resolve: null,
|
|
reject: null,
|
|
};
|
|
// Create a promise that resolves once the queued item has processed
|
|
const p = new Promise((resolve, reject) => {
|
|
update.resolve = resolve;
|
|
update.reject = reject;
|
|
})
|
|
.catch(err => {
|
|
this.storage.debug.error(`Unable to process queued update for "/${path}" on index ${this.description}:`, err);
|
|
});
|
|
this._updateQueue.push(update);
|
|
//return p; // Don't wait for p, prevents deadlock when tree is rebuilding
|
|
}
|
|
}
|
|
async _lock(mode = 'exclusive', timeout = 60000) {
|
|
return thread_safe_1.ThreadSafe.lock(this.fileName, { shared: mode === 'shared', timeout }); //, timeout: 15 * 60000 (for debugging)
|
|
}
|
|
async count(op, val) {
|
|
if (!this.caseSensitive) {
|
|
// Convert to locale aware lowercase
|
|
if (typeof val === 'string') {
|
|
val = val.toLocaleLowerCase(this.textLocale);
|
|
}
|
|
else if (val instanceof Array) {
|
|
val = val.map(val => {
|
|
if (typeof val === 'string') {
|
|
return val.toLocaleLowerCase(this.textLocale);
|
|
}
|
|
return val;
|
|
});
|
|
}
|
|
}
|
|
const cacheKey = op + '{count}';
|
|
const cache = this.cache(cacheKey, val);
|
|
if (cache) {
|
|
// Cached count, saves time!
|
|
return cache;
|
|
}
|
|
const idx = await this._getTree('shared');
|
|
const result = await idx.tree.search(op, val, { count: true, keys: true, values: false });
|
|
idx.release();
|
|
this.cache(cacheKey, val, result.valueCount);
|
|
return result.valueCount;
|
|
}
|
|
async take(skip, take, ascending) {
|
|
const cacheKey = `${skip}+${take}-${ascending ? 'asc' : 'desc'}`;
|
|
const cache = this.cache('take', cacheKey);
|
|
if (cache) {
|
|
return cache;
|
|
}
|
|
const stats = new IndexQueryStats('take', { skip, take, ascending }, true);
|
|
const idx = await this._getTree('shared');
|
|
const results = new IndexQueryResults(); //[];
|
|
results.filterKey = this.key;
|
|
let skipped = 0;
|
|
const processLeaf = async (leaf) => {
|
|
if (!ascending) {
|
|
leaf.entries.reverse();
|
|
}
|
|
for (let i = 0; i < leaf.entries.length; i++) {
|
|
const entry = leaf.entries[i];
|
|
const value = entry.key;
|
|
for (let j = 0; j < entry.totalValues; j++) { //entry.values.length
|
|
if (skipped < skip) {
|
|
skipped++;
|
|
continue;
|
|
}
|
|
if (leaf.hasExtData && !leaf.extData.loaded) {
|
|
await leaf.extData.load();
|
|
}
|
|
const entryValue = entry.values[j];
|
|
const recordPointer = _parseRecordPointer(this.path, entryValue.recordPointer);
|
|
const metadata = entryValue.metadata;
|
|
const result = new IndexQueryResult(recordPointer.key, recordPointer.path, value, metadata);
|
|
results.push(result);
|
|
if (results.length === take) {
|
|
return results;
|
|
}
|
|
}
|
|
}
|
|
if (ascending && leaf.hasNext) {
|
|
return leaf.getNext().then(processLeaf);
|
|
}
|
|
else if (!ascending && leaf.hasPrevious) {
|
|
return leaf.getPrevious().then(processLeaf);
|
|
}
|
|
else {
|
|
return results;
|
|
}
|
|
};
|
|
if (ascending) {
|
|
await idx.tree.getFirstLeaf().then(processLeaf);
|
|
}
|
|
else {
|
|
await idx.tree.getLastLeaf().then(processLeaf);
|
|
}
|
|
idx.release();
|
|
stats.stop(results.length);
|
|
results.stats = stats;
|
|
this.cache('take', cacheKey, results);
|
|
return results;
|
|
}
|
|
static get validOperators() {
|
|
return ['<', '<=', '==', '!=', '>=', '>', 'exists', '!exists', 'between', '!between', 'like', '!like', 'matches', '!matches', 'in', '!in'];
|
|
}
|
|
get validOperators() {
|
|
return DataIndex.validOperators;
|
|
}
|
|
async query(op, val, options = {}) {
|
|
if (!(op instanceof btree_1.BlacklistingSearchOperator) && !DataIndex.validOperators.includes(op)) {
|
|
throw new TypeError(`Cannot use operator "${op}" to query index "${this.description}"`);
|
|
}
|
|
if (!this.caseSensitive) {
|
|
// Convert to locale aware lowercase
|
|
if (typeof val === 'string') {
|
|
val = val.toLocaleLowerCase(this.textLocale);
|
|
}
|
|
else if (val instanceof Array) {
|
|
val = val.map(val => {
|
|
if (typeof val === 'string') {
|
|
return val.toLocaleLowerCase(this.textLocale);
|
|
}
|
|
return val;
|
|
});
|
|
}
|
|
}
|
|
const stats = new IndexQueryStats('query', { op, val }, true);
|
|
let entries; // ;
|
|
const isCacheable = !(op instanceof btree_1.BlacklistingSearchOperator);
|
|
const cache = isCacheable && this.cache(op, val);
|
|
if (cache) {
|
|
entries = cache;
|
|
}
|
|
else {
|
|
const idx = await this._getTree('shared');
|
|
const searchOptions = {
|
|
entries: true,
|
|
// filter: options.filter && options.filter.treeEntries // Don't let tree apply filter, so we can cache results before filtering ourself
|
|
};
|
|
const result = await idx.tree.search(op, val, searchOptions);
|
|
entries = result.entries;
|
|
idx.release();
|
|
// Cache entries
|
|
isCacheable && this.cache(op, val, entries);
|
|
}
|
|
const results = new IndexQueryResults();
|
|
results.filterKey = this.key;
|
|
results.entryValues = [];
|
|
if (options.filter) {
|
|
const filterStep = new IndexQueryStats('filter', {
|
|
entries: entries.length,
|
|
entryValues: entries.reduce((total, entry) => total + entry.values.length, 0),
|
|
filterValues: options.filter.entryValues.length,
|
|
}, true);
|
|
stats.steps.push(filterStep);
|
|
let values = [];
|
|
const valueEntryIndexes = [];
|
|
entries.forEach(entry => {
|
|
valueEntryIndexes.push(values.length);
|
|
values = values.concat(entry.values);
|
|
});
|
|
const filterValues = options.filter.entryValues;
|
|
// Pre-process recordPointers to speed up matching
|
|
const preProcess = (values, tree = false) => {
|
|
if (tree && values.rpTree) {
|
|
return;
|
|
}
|
|
const builder = tree ? new btree_1.BPlusTreeBuilder(true, 100) : null;
|
|
for (let i = 0; i < values.length; i++) {
|
|
const val = values[i];
|
|
let rp = val.rp || '';
|
|
if (rp === '') {
|
|
for (let j = 0; j < val.recordPointer.length; j++) {
|
|
rp += val.recordPointer[j].toString(36);
|
|
}
|
|
val.rp = rp;
|
|
}
|
|
if (tree && !builder.list.has(rp)) {
|
|
builder.add(rp, [i]);
|
|
}
|
|
}
|
|
if (tree) {
|
|
values.rpTree = builder.create();
|
|
}
|
|
};
|
|
// preProcess(values);
|
|
// preProcess(filterValues);
|
|
// Loop through smallest set
|
|
const smallestSet = filterValues.length < values.length ? filterValues : values;
|
|
preProcess(smallestSet, false);
|
|
const otherSet = smallestSet === filterValues ? values : filterValues;
|
|
preProcess(otherSet, true);
|
|
// TODO: offload filtering from event loop to stay responsive
|
|
for (let i = 0; i < smallestSet.length; i++) {
|
|
const value = smallestSet[i];
|
|
// Find in other set
|
|
let match = null;
|
|
let matchIndex;
|
|
const tree = otherSet.rpTree;
|
|
const rpEntryValue = tree.find(value.rp);
|
|
if (rpEntryValue) {
|
|
const j = rpEntryValue.recordPointer[0];
|
|
match = smallestSet === values ? value : otherSet[j];
|
|
matchIndex = match === value ? i : j;
|
|
}
|
|
if (match) {
|
|
const recordPointer = _parseRecordPointer(this.path, match.recordPointer);
|
|
const metadata = match.metadata;
|
|
const entry = entries[valueEntryIndexes.findIndex((entryIndex, i, arr) => i + 1 === arr.length || (entryIndex <= matchIndex && arr[i + 1] > matchIndex))];
|
|
const result = new IndexQueryResult(recordPointer.key, recordPointer.path, entry.key, metadata);
|
|
// result.entry = entry;
|
|
results.push(result);
|
|
results.entryValues.push(match);
|
|
}
|
|
}
|
|
filterStep.stop({ results: results.length, values: results.entryValues.length });
|
|
}
|
|
else {
|
|
// No filter, add all (unique) results
|
|
const uniqueRecordPointers = new Set();
|
|
entries.forEach(entry => {
|
|
entry.values.forEach(value => {
|
|
const recordPointer = _parseRecordPointer(this.path, value.recordPointer);
|
|
if (!uniqueRecordPointers.has(recordPointer.path)) {
|
|
// If a single recordPointer exists in multiple entries (can happen with eg 'like' queries),
|
|
// only add the first one, ignore others (prevents duplicate results!)
|
|
uniqueRecordPointers.add(recordPointer.path);
|
|
const metadata = value.metadata;
|
|
const result = new IndexQueryResult(recordPointer.key, recordPointer.path, entry.key, metadata);
|
|
// result.entry = entry;
|
|
results.push(result);
|
|
results.entryValues.push(value);
|
|
}
|
|
});
|
|
});
|
|
uniqueRecordPointers.clear(); // Help GC
|
|
}
|
|
stats.stop(results.length);
|
|
results.stats = stats;
|
|
return results;
|
|
}
|
|
async build(options) {
|
|
if ([DataIndex.STATE.BUILD, DataIndex.STATE.REBUILD].includes(this.state)) {
|
|
throw new Error('Index is already being built');
|
|
}
|
|
this.state = this.state === DataIndex.STATE.READY
|
|
? DataIndex.STATE.REBUILD // Existing index file has to be overwritten in the last phase
|
|
: DataIndex.STATE.BUILD;
|
|
this._buildError = null;
|
|
const path = this.path;
|
|
const wildcardNames = path.match(/\*|\$[a-z0-9_]+/gi) || [];
|
|
// const hasWildcards = wildcardNames.length > 0;
|
|
const wildcardsPattern = '^' + path.replace(/\*|\$[a-z0-9_]+/gi, '([a-z0-9_]+)') + '/';
|
|
const wildcardRE = new RegExp(wildcardsPattern, 'i');
|
|
// let treeBuilder = new BPlusTreeBuilder(false, FILL_FACTOR, this.allMetadataKeys); //(30, false);
|
|
// let idx; // Once using binary file to write to
|
|
const tid = acebase_core_1.ID.generate();
|
|
const keys = acebase_core_1.PathInfo.getPathKeys(path);
|
|
const indexableTypes = [node_1.Node.VALUE_TYPES.STRING, node_1.Node.VALUE_TYPES.NUMBER, node_1.Node.VALUE_TYPES.BOOLEAN, node_1.Node.VALUE_TYPES.DATETIME];
|
|
const allowedKeyValueTypes = options && options.valueTypes
|
|
? options.valueTypes
|
|
: indexableTypes;
|
|
this.storage.debug.log(`Index build ${this.description} started`.colorize(acebase_core_1.ColorStyle.blue));
|
|
let indexedValues = 0;
|
|
// let addPromise;
|
|
// let flushed = false;
|
|
// const __DEV_UNIQUE_SET = new Set();
|
|
// const __DEV_CHECK_UNIQUE = (key) => {
|
|
// if (__DEV_UNIQUE_SET.has(key)) {
|
|
// console.warn(`Duplicate key: ${key}`);
|
|
// }
|
|
// else {
|
|
// __DEV_UNIQUE_SET.add(key);
|
|
// }
|
|
// };
|
|
const buildFile = this.fileName + '.build';
|
|
const createBuildFile = () => {
|
|
return new Promise((resolve, reject) => {
|
|
const buildWriteStream = promise_fs_1.pfs.fs.createWriteStream(buildFile, { flags: promise_fs_1.pfs.flags.readAndAppendAndCreate });
|
|
const streamState = { wait: false, chunks: [] };
|
|
buildWriteStream.on('error', (err) => {
|
|
console.error(err);
|
|
reject(err);
|
|
});
|
|
buildWriteStream.on('open', async () => {
|
|
await getAll('', 0);
|
|
// if (indexedValues === 0) {
|
|
// const err = new Error('No values found to index');
|
|
// err.code = 'NO_DATA';
|
|
// buildWriteStream.close(() => {
|
|
// pfs.rm(buildFile)
|
|
// .then(() => {
|
|
// reject(err);
|
|
// })
|
|
// return reject(err);
|
|
// });
|
|
// return;
|
|
// }
|
|
this.storage.debug.log(`done writing values to ${buildFile}`);
|
|
if (streamState.wait) {
|
|
buildWriteStream.once('drain', () => {
|
|
buildWriteStream.end(resolve);
|
|
});
|
|
}
|
|
else {
|
|
buildWriteStream.end(resolve);
|
|
}
|
|
});
|
|
buildWriteStream.on('drain', () => {
|
|
// Write queued chunks
|
|
const totalBytes = streamState.chunks.reduce((total, bytes) => total + bytes.length, 0);
|
|
const buffer = new Uint8Array(totalBytes);
|
|
let offset = 0;
|
|
streamState.chunks.forEach(bytes => {
|
|
buffer.set(bytes, offset);
|
|
offset += bytes.length;
|
|
});
|
|
// Write!
|
|
streamState.chunks = [];
|
|
streamState.wait = !buildWriteStream.write(buffer, err => {
|
|
console.assert(!err, `Failed to write to stream: ${err && err.message}`);
|
|
});
|
|
});
|
|
const writeToStream = (bytes) => {
|
|
if (streamState.wait) {
|
|
streamState.chunks.push(bytes);
|
|
console.assert(streamState.chunks.length < 100000, 'Something going wrong here');
|
|
}
|
|
else {
|
|
streamState.wait = !buildWriteStream.write(Buffer.from(bytes), err => {
|
|
console.assert(!err, `Failed to write to stream: ${err && err.message}`);
|
|
});
|
|
}
|
|
};
|
|
const isWildcardKey = (key) => typeof key === 'string' && (key === '*' || key.startsWith('$'));
|
|
const getAll = async (currentPath, keyIndex) => {
|
|
// "users/*/posts"
|
|
// --> Get all children of "users",
|
|
// --> get their "posts" children,
|
|
// --> get their children to index
|
|
let path = currentPath;
|
|
while (keys[keyIndex] && !isWildcardKey(keys[keyIndex])) {
|
|
path = acebase_core_1.PathInfo.getChildPath(path, keys[keyIndex]); // += keys[keyIndex];
|
|
keyIndex++;
|
|
}
|
|
const isTargetNode = keyIndex === keys.length;
|
|
const getChildren = async () => {
|
|
const childKeys = [];
|
|
try {
|
|
await this.storage.getChildren(path).next(child => {
|
|
const keyOrIndex = typeof child.index === 'number' ? child.index : child.key;
|
|
if (!child.address || child.type !== node_1.Node.VALUE_TYPES.OBJECT) {
|
|
return; // This child cannot be indexed because it is not an object with properties
|
|
}
|
|
else {
|
|
childKeys.push(keyOrIndex);
|
|
}
|
|
});
|
|
}
|
|
catch (reason) {
|
|
// Record doesn't exist? No biggy
|
|
this.storage.debug.warn(`Could not get children of "/${path}": ${reason.message}`);
|
|
}
|
|
// Iterate through the children in batches of max n nodes
|
|
// should be determined by amount of * wildcards in index path
|
|
// If there are 0 wildcards, batch size of 500 is ok
|
|
// if there is 1 wildcard, use batch size 22 (sqrt of 500, 500^0.5),
|
|
// 2 wildcards: batch size 5 (2v500 or 500^0.25),
|
|
// 3 wildcards: batch size 2 (3v500 or 500^00.125)
|
|
// Algebra refresh:
|
|
// a = Math.pow(b, c)
|
|
// c = Math.log(a) / Math.log(b)
|
|
// b = Math.pow(a, Math.pow(0.5, c))
|
|
// a is our max batch size, we'll use 500
|
|
// c is our depth (nrOfWildcards) so we know this
|
|
// b is our unknown start number
|
|
const maxBatchSize = Math.round(Math.pow(500, Math.pow(0.5, wildcardNames.length)));
|
|
const batches = [];
|
|
while (childKeys.length > 0) {
|
|
const batchChildren = childKeys.splice(0, maxBatchSize);
|
|
batches.push(batchChildren);
|
|
}
|
|
while (batches.length > 0) {
|
|
const batch = batches.shift();
|
|
await Promise.all(batch.map(async (childKey) => {
|
|
const childPath = acebase_core_1.PathInfo.getChildPath(path, childKey);
|
|
// do it
|
|
if (!isTargetNode) {
|
|
// Go deeper
|
|
return getAll(childPath, keyIndex + 1);
|
|
}
|
|
else {
|
|
// We have to index this child, get all required values for the entry
|
|
const wildcardValues = childPath.match(wildcardRE).slice(1);
|
|
const neededKeys = [this.key].concat(this.includeKeys);
|
|
const keyFilter = neededKeys.filter(key => key !== '{key}' && !wildcardNames.includes(key));
|
|
if (this.textLocaleKey) {
|
|
keyFilter.push(this.textLocaleKey);
|
|
}
|
|
let keyValue = null; // initialize to null so we can check if it had a valid indexable value
|
|
let locale = this.textLocale;
|
|
const metadata = (() => {
|
|
// create properties for each included key, if they are not set by the loop they will still be in the metadata (which is required for B+Tree metadata)
|
|
const obj = {};
|
|
this.includeKeys.forEach(key => obj[key] = undefined);
|
|
return obj;
|
|
})();
|
|
const addValue = (key, value) => {
|
|
if (key === this.key) {
|
|
keyValue = value;
|
|
}
|
|
else if (key === this.textLocaleKey && typeof value === 'string') {
|
|
locale = value;
|
|
}
|
|
else {
|
|
metadata[key] = value;
|
|
}
|
|
};
|
|
const gotNamedWildcardKeys = ['{key}'].concat(wildcardNames).filter(key => key !== '*');
|
|
// Add special indexable key values from the current path, such as '{key}' for the current key,
|
|
// and named wildcards such as '$id'. This allows parts of the path to be indexed, or included in the index.
|
|
//
|
|
// Imagine an index on path 'users/$userId/posts/$postId/comments'
|
|
//
|
|
// - indexing on special key '{key}' allows quick lookups on a specific commentId without the need to know
|
|
// the userId or postId it belongs to:
|
|
//
|
|
// db.query('users/*/posts/*/comments').filter('{key}', '==', 'l6ukhzd6000009lgcuvm7nef');
|
|
//
|
|
// - indexing on wildcard key '$postId' allows quick lookups on all comments of a specific postId
|
|
// without the need to know the userId it belongs to:
|
|
//
|
|
// db.query('users/*/posts/$postId/comments').filter('$postId', '==', 'l6ukv0ru000009l42rbf7hn5');
|
|
//
|
|
// - including any of the special keys to the index allows quick filtering in queries:
|
|
// (assume key 'text' was indexed:)
|
|
//
|
|
// db.query('users/*/posts/$postId/comments')
|
|
// .filter('text', 'like', '*hello*')
|
|
// .filter('$postId', '==', 'l6ukv0ru000009l42rbf7hn5')
|
|
//
|
|
neededKeys.filter(key => gotNamedWildcardKeys.includes(key)).forEach(key => {
|
|
if (key === '{key}') {
|
|
keyValue = childKey;
|
|
}
|
|
else {
|
|
const index = wildcardNames.indexOf(key);
|
|
if (index < 0) {
|
|
throw new Error(`Requested key variable "${key}" not found in index path`);
|
|
}
|
|
const value = wildcardValues[index];
|
|
addValue(key, value);
|
|
}
|
|
});
|
|
const gotAllData = neededKeys.every(key => gotNamedWildcardKeys.includes(key));
|
|
if (!gotAllData) {
|
|
// Fetch node value, we need more data
|
|
// Get child values
|
|
const keyPromises = [];
|
|
const seenKeys = gotNamedWildcardKeys.slice();
|
|
// NEW: Use getNode to get data, enables indexing of subkeys
|
|
const { value: obj } = await this.storage.getNode(childPath, { include: keyFilter, tid });
|
|
keyFilter.forEach(key => {
|
|
// What can be indexed?
|
|
// strings, numbers, booleans, dates, undefined
|
|
const val = acebase_core_1.PathInfo.getPathKeys(key).reduce((val, key) => typeof val === 'object' && key in val ? val[key] : undefined, obj);
|
|
if (typeof val === 'undefined') {
|
|
// Key not present
|
|
return;
|
|
}
|
|
seenKeys.push(key);
|
|
const type = (0, node_value_types_1.getValueType)(val);
|
|
if (key === this.key && !allowedKeyValueTypes.includes(type)) {
|
|
// Key value isn't allowed to be this type, mark it as null so it won't be indexed
|
|
keyValue = null;
|
|
return;
|
|
}
|
|
else if (key !== this.key && !indexableTypes.includes(type)) {
|
|
// Metadata that can't be indexed because it has the wrong type
|
|
return;
|
|
}
|
|
// Index this value
|
|
addValue(key, val);
|
|
});
|
|
// If the key value wasn't present, set it to undefined (so it'll be indexed)
|
|
if (!seenKeys.includes(this.key)) {
|
|
keyValue = undefined;
|
|
}
|
|
await Promise.all(keyPromises);
|
|
}
|
|
const addIndexValue = (value, recordPointer, metadata) => {
|
|
if (typeof value === 'string' && value.length > 255) {
|
|
// Make sure strings are not too large to store. Use first 255 chars only
|
|
console.warn(`Truncating key value "${value}" because it is too large to index`);
|
|
value = value.slice(0, 255);
|
|
}
|
|
if (!this.caseSensitive) {
|
|
// Store lowercase key and metadata values
|
|
if (typeof value === 'string') {
|
|
value = value.toLocaleLowerCase(locale);
|
|
}
|
|
Object.keys(metadata).forEach(key => {
|
|
const value = metadata[key];
|
|
if (typeof value === 'string') {
|
|
metadata[key] = value.toLocaleLowerCase(locale);
|
|
}
|
|
});
|
|
}
|
|
// NEW: write value to buildStream
|
|
const bytes = [
|
|
0, 0, 0, 0,
|
|
0, // processed
|
|
];
|
|
// key:
|
|
const keyBytes = btree_1.BinaryWriter.getBytes(value);
|
|
bytes.push(...keyBytes);
|
|
// rp_length:
|
|
bytes.push(recordPointer.length);
|
|
// rp_data:
|
|
bytes.push(...recordPointer);
|
|
// metadata:
|
|
this.allMetadataKeys && this.allMetadataKeys.forEach(key => {
|
|
let metadataValue = metadata[key];
|
|
if (typeof metadataValue === 'string' && metadataValue.length > 255) {
|
|
// Make sure strings are not too large to store. Use first 255 chars only
|
|
console.warn(`Truncating "${key}" metadata value "${metadataValue}" because it is too large to index`);
|
|
metadataValue = metadataValue.slice(0, 255);
|
|
}
|
|
const valueBytes = btree_1.BinaryWriter.getBytes(metadataValue); // metadata_value
|
|
bytes.push(...valueBytes);
|
|
});
|
|
// update entry_length:
|
|
btree_1.BinaryWriter.writeUint32(bytes.length, bytes, 0);
|
|
writeToStream(bytes);
|
|
indexedValues++;
|
|
};
|
|
if (keyValue !== null) {
|
|
// Add it to the index, using value as the index key, a record pointer as the value
|
|
// Create record pointer
|
|
const recordPointer = _createRecordPointer(wildcardValues, childKey); //, child.address);
|
|
// const entryValue = new BinaryBPlusTree.EntryValue(recordPointer, metadata)
|
|
// Add it to the index
|
|
if (options === null || options === void 0 ? void 0 : options.addCallback) {
|
|
keyValue = options.addCallback(addIndexValue, keyValue, recordPointer, metadata, { path: childPath, wildcards: wildcardValues, key: childKey, locale });
|
|
}
|
|
else {
|
|
addIndexValue(keyValue, recordPointer, metadata);
|
|
}
|
|
this.storage.debug.log(`Indexed "/${childPath}/${this.key}" value: '${keyValue}' (${typeof keyValue})`.colorize(acebase_core_1.ColorStyle.cyan));
|
|
}
|
|
// return addPromise; // Do we really have to wait for this?
|
|
}
|
|
}));
|
|
}
|
|
};
|
|
return getChildren();
|
|
};
|
|
});
|
|
};
|
|
const mergeFile = `${buildFile}.merge`;
|
|
const createMergeFile = async () => {
|
|
// start by grouping the keys:
|
|
// take the first n keys in the .build file, read through the entire file
|
|
// to find other occurences of the same key.
|
|
// Group them and write to .build.n files in batches of 10.000 keys
|
|
if (indexedValues === 0) {
|
|
// Remove build file, nothing else to do
|
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
return await promise_fs_1.pfs.rm(buildFile).catch(err => { });
|
|
}
|
|
try {
|
|
const exists = await promise_fs_1.pfs.exists(mergeFile);
|
|
if (exists) {
|
|
const err = new Error('File already exists');
|
|
err.code = 'EEXIST';
|
|
throw err;
|
|
}
|
|
const fd = await promise_fs_1.pfs.open(buildFile, promise_fs_1.pfs.flags.readAndWrite);
|
|
const writer = btree_1.BinaryWriter.forFunction(async (data, position) => {
|
|
const buffer = data instanceof Buffer ? data : Buffer.from(data);
|
|
await promise_fs_1.pfs.write(fd, buffer, 0, buffer.byteLength, position);
|
|
});
|
|
const reader = new btree_1.BinaryReader(fd, 512 * 1024); // Read 512KB chunks
|
|
await reader.init();
|
|
// const maxKeys = 10000; // Work with max 10.000 in-memory keys at a time
|
|
const maxValues = 100000; // Max 100K in-memory values
|
|
const readNext = async () => {
|
|
// Read next from file
|
|
try {
|
|
let processed = true;
|
|
/** @type {Buffer} */
|
|
let buffer;
|
|
/** @type {number} */
|
|
let entryIndex;
|
|
while (processed) {
|
|
entryIndex = reader.sourceIndex;
|
|
const entryLength = await reader.getUint32(); // entry_length
|
|
if (entryLength < 4) {
|
|
throw new Error(`Invalid entry length ${entryLength} at build file index ${entryIndex}`);
|
|
}
|
|
buffer = await reader.get(entryLength - 4);
|
|
// processed:
|
|
processed = buffer[0] === 1;
|
|
}
|
|
// key:
|
|
let index = 1;
|
|
const keyValue = btree_1.BinaryReader.readValue(buffer, index);
|
|
index += keyValue.byteLength;
|
|
// value: (combine rp_length, rp_data, metadata)
|
|
const len = buffer.byteLength - index;
|
|
const val = buffer.slice(index, index + len); // Buffer.from(buffer.buffer, index, len);
|
|
// console.log(`Read "${keyValue.value}" @${entryIndex} with value length ${len}`);
|
|
return {
|
|
key: keyValue.value,
|
|
value: val,
|
|
index: entryIndex,
|
|
length: buffer.byteLength + 4,
|
|
flagProcessed() {
|
|
buffer[0] = 1;
|
|
buffer = null;
|
|
return writer.write([1], this.index + 4); // flag file
|
|
},
|
|
// flagProcessed() {
|
|
// // __DEV_CHECK_UNIQUE(this.index);
|
|
// buffer[0] = 1; // make sure the in-memory cache is also flagged
|
|
// buffer = null; // release memory if not referenced anywhere else
|
|
// return writer.write([1], this.index + 4); // flag file
|
|
// }
|
|
};
|
|
}
|
|
catch (err) {
|
|
if (err.code === 'EOF') {
|
|
return null;
|
|
}
|
|
throw err;
|
|
}
|
|
};
|
|
// Write batch files
|
|
let batchNr = 0;
|
|
let batchStartEntry = null;
|
|
// Find out how many written batches there are already (if process was terminated while building, we can resume)
|
|
const path = buildFile.slice(0, buildFile.lastIndexOf('/'));
|
|
const entries = await promise_fs_1.pfs.readdir(path);
|
|
let high = 0;
|
|
const checkFile = buildFile.slice(path.length + 1) + '.';
|
|
entries.forEach(entry => {
|
|
if (typeof entry === 'string' && entry.startsWith(checkFile)) {
|
|
const match = /\.([0-9]+)$/.exec(entry);
|
|
if (!match) {
|
|
return;
|
|
}
|
|
const nr = parseInt(match[1]);
|
|
high = Math.max(high, nr);
|
|
}
|
|
});
|
|
batchNr = high;
|
|
let more = true;
|
|
while (more) {
|
|
batchNr++;
|
|
const map = new Map();
|
|
let processedValues = 0;
|
|
if (batchStartEntry !== null) {
|
|
// Skip already processed entries
|
|
await reader.go(batchStartEntry.index);
|
|
batchStartEntry = null;
|
|
}
|
|
let next;
|
|
while ((next = await readNext()) !== null) {
|
|
processedValues++;
|
|
const isDate = next.key instanceof Date;
|
|
const key = isDate ? next.key.getTime() : next.key;
|
|
let values = map.get(key);
|
|
if (values) {
|
|
values.push(next.value);
|
|
next.flagProcessed();
|
|
}
|
|
else if (processedValues < maxValues) {
|
|
values = [next.value];
|
|
if (isDate) {
|
|
values.dateKey = true;
|
|
}
|
|
map.set(key, values);
|
|
next.flagProcessed();
|
|
}
|
|
else {
|
|
more = true;
|
|
batchStartEntry = next;
|
|
break; // Stop adding values to this batch
|
|
}
|
|
}
|
|
if (map.size === 0) {
|
|
// no entries
|
|
batchNr--;
|
|
break;
|
|
}
|
|
// sort the map keys
|
|
const sortedKeys = (0, quicksort_1.default)([...map.keys()], (a, b) => {
|
|
if (btree_1.BPlusTree.typeSafeComparison.isLess(a, b)) {
|
|
return -1;
|
|
}
|
|
if (btree_1.BPlusTree.typeSafeComparison.isMore(a, b)) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
});
|
|
// write batch
|
|
const batchStream = promise_fs_1.pfs.fs.createWriteStream(`${buildFile}.${batchNr}`, { flags: promise_fs_1.pfs.flags.appendAndCreate });
|
|
for (const key of sortedKeys) {
|
|
const values = map.get(key);
|
|
const isDateKey = values.dateKey === true;
|
|
const bytes = [
|
|
0, 0, 0, 0, // entry_length
|
|
];
|
|
// key:
|
|
let b = btree_1.BinaryWriter.getBytes(isDateKey ? new Date(key) : key);
|
|
bytes.push(...b);
|
|
// // values_byte_length:
|
|
// const valuesByteLengthIndex = bytes.length;
|
|
// bytes.push(0, 0, 0, 0);
|
|
// values_length:
|
|
b = btree_1.BinaryWriter.writeUint32(values.length, [0, 0, 0, 0], 0);
|
|
bytes.push(...b);
|
|
for (let j = 0; j < values.length; j++) {
|
|
const value = values[j];
|
|
// value_length:
|
|
b = btree_1.BinaryWriter.writeUint32(value.length, [0, 0, 0, 0], 0);
|
|
bytes.push(...b);
|
|
// value:
|
|
bytes.push(...value);
|
|
}
|
|
// // update values_byte_length:
|
|
// const valuesByteLength = bytes.length - valuesByteLengthIndex
|
|
// BinaryWriter.writeUint32(valuesByteLength, bytes, valuesByteLengthIndex);
|
|
// Update entry_length:
|
|
btree_1.BinaryWriter.writeUint32(bytes.length, bytes, 0);
|
|
const ok = batchStream.write(Uint8Array.from(bytes));
|
|
if (!ok) {
|
|
await new Promise(resolve => {
|
|
batchStream.once('drain', resolve);
|
|
});
|
|
}
|
|
}
|
|
await new Promise(resolve => {
|
|
batchStream.end(resolve);
|
|
});
|
|
}
|
|
await promise_fs_1.pfs.close(fd); // Close build file
|
|
await promise_fs_1.pfs.rm(buildFile); // Remove build file
|
|
// Now merge-sort all keys, by reading keys from each batch,
|
|
// taking the smallest value from each batch a time
|
|
const batches = batchNr;
|
|
if (batches === 0) {
|
|
// No batches -> no indexed entries
|
|
return;
|
|
}
|
|
// create write stream for merged data
|
|
const outputStream = promise_fs_1.pfs.fs.createWriteStream(mergeFile, { flags: promise_fs_1.pfs.flags.writeAndCreate });
|
|
// const outputStream = BinaryWriter.forFunction((data, position) => {
|
|
// return pfs.write(fd, data, 0, data.byteLength, position);
|
|
// });
|
|
// open readers for each batch file
|
|
const readers = [];
|
|
const bufferChunkSize = Math.max(10240, Math.round((10 * 1024 * 1024) / batches)); // 10MB dedicated memory to divide between readers, with a minimum of 10KB per reader
|
|
for (let i = 0; i < batches; i++) {
|
|
const reader = new btree_1.BinaryReader(`${buildFile}.${i + 1}`, bufferChunkSize);
|
|
readers.push(reader);
|
|
}
|
|
await Promise.all(readers.map(reader => reader.init()));
|
|
// load entries from each batch file
|
|
let sortedEntryIndexes = [];
|
|
const entriesPerBatch = new Array(batches);
|
|
const loadEntry = async (batchIndex) => {
|
|
const reader = readers[batchIndex];
|
|
try {
|
|
const entryLength = await reader.getUint32(); // entry_length:
|
|
const buffer = await reader.get(entryLength - 4);
|
|
// key:
|
|
const keyValue = btree_1.BinaryReader.readValue(buffer, 0);
|
|
const key = keyValue.value;
|
|
const values = buffer.slice(keyValue.byteLength); //Buffer.from(buffer.buffer, keyValue.byteLength, buffer.byteLength - keyValue.byteLength);
|
|
// Check if another batch has entry with the same key
|
|
const existing = entriesPerBatch.find(entry => entry && entry.key === key);
|
|
if (existing) {
|
|
// Append values to existing
|
|
// First 4 bytes of values contains values_length
|
|
const currentValues = btree_1.BinaryReader.readUint32(existing.values, 0);
|
|
const additionalValues = btree_1.BinaryReader.readUint32(values, 0);
|
|
const concatenated = new Uint8Array(existing.values.byteLength + values.byteLength - 4);
|
|
concatenated.set(existing.values, 0);
|
|
concatenated.set(values.slice(4), existing.values.byteLength);
|
|
// Update values_length to total
|
|
btree_1.BinaryWriter.writeUint32(currentValues + additionalValues, concatenated, 0);
|
|
existing.values = concatenated;
|
|
return loadEntry(batchIndex);
|
|
}
|
|
// Create new entry
|
|
const entry = { key, values };
|
|
entriesPerBatch[batchIndex] = entry;
|
|
// update sortedEntryIndexes (only if it has been populated already, not when loading start values)
|
|
if (sortedEntryIndexes.length > 0) {
|
|
// remove the old entry
|
|
const oldSortEntryIndex = sortedEntryIndexes.findIndex(sortEntry => sortEntry.index === batchIndex);
|
|
sortedEntryIndexes.splice(oldSortEntryIndex, 1);
|
|
// create new entry, insert at right sorted location
|
|
// const newSortEntryIndex = sortedEntryIndexes.findIndex(sortEntry => BPlusTree.typeSafeComparison.isMore(sortEntry.key, entry.key));
|
|
let newSortEntryIndex = oldSortEntryIndex; // The newly read value >= previous value, because they are stored sorted in the batch file
|
|
while (newSortEntryIndex < sortedEntryIndexes.length
|
|
&& btree_1.BPlusTree.typeSafeComparison.isMore(entry.key, sortedEntryIndexes[newSortEntryIndex].key)) {
|
|
newSortEntryIndex++;
|
|
}
|
|
const newSortEntry = { index: batchIndex, key: entry.key };
|
|
sortedEntryIndexes.splice(newSortEntryIndex, 0, newSortEntry);
|
|
}
|
|
// return entry;
|
|
}
|
|
catch (err) {
|
|
if (err.code === 'EOF') {
|
|
// No more entries in batch file, set this batch's entry to null
|
|
entriesPerBatch[batchIndex] = null;
|
|
// remove from sortedEntryIndexes
|
|
console.assert(sortedEntryIndexes.length > 0);
|
|
const sortEntryIndex = sortedEntryIndexes.findIndex(sortEntry => sortEntry.index === batchIndex);
|
|
sortedEntryIndexes.splice(sortEntryIndex, 1);
|
|
}
|
|
else {
|
|
throw err;
|
|
}
|
|
}
|
|
};
|
|
// load start entries from each batch file
|
|
const promises = readers.map((reader, index) => loadEntry(index));
|
|
await Promise.all(promises);
|
|
// Populate sortedEntryIndexes
|
|
sortedEntryIndexes = entriesPerBatch.map((entry, index) => ({ index, key: entry.key }))
|
|
.sort((a, b) => {
|
|
if (btree_1.BPlusTree.typeSafeComparison.isLess(a.key, b.key)) {
|
|
return -1;
|
|
}
|
|
if (btree_1.BPlusTree.typeSafeComparison.isMore(a.key, b.key)) {
|
|
return 1;
|
|
}
|
|
return 0; // happens when a key had too many values (and were split into multiple batches)
|
|
});
|
|
// write all entries
|
|
while (sortedEntryIndexes.length > 0) {
|
|
// take smallest (always at index 0 in sorted array)
|
|
const smallestDetails = sortedEntryIndexes[0];
|
|
const batchIndex = smallestDetails.index;
|
|
const smallestEntry = entriesPerBatch[batchIndex];
|
|
const bytes = [
|
|
0, 0, 0, 0, // entry_length
|
|
];
|
|
// key:
|
|
const keyBytes = btree_1.BinaryWriter.getBytes(smallestEntry.key);
|
|
bytes.push(...keyBytes);
|
|
// update entry_length
|
|
const byteLength = bytes.length + smallestEntry.values.byteLength;
|
|
btree_1.BinaryWriter.writeUint32(byteLength, bytes, 0);
|
|
// build buffer
|
|
const buffer = new Uint8Array(byteLength);
|
|
buffer.set(bytes, 0);
|
|
// values:
|
|
buffer.set(smallestEntry.values, bytes.length);
|
|
// write to stream
|
|
// console.log(`writing entry "${smallestEntry.key}"`);
|
|
// return outputStream.append(buffer)
|
|
// .then(() => {
|
|
// return loadEntry(batchIndex);
|
|
// })
|
|
// .then(writeSmallestEntry);
|
|
const ok = outputStream.write(buffer, err => {
|
|
console.assert(!err, 'Error while writing?');
|
|
});
|
|
if (!ok) {
|
|
await new Promise(resolve => {
|
|
outputStream.once('drain', resolve);
|
|
});
|
|
}
|
|
// load next entry from the batch we used
|
|
await loadEntry(batchIndex);
|
|
}
|
|
// Wait until output stream is done writing
|
|
await new Promise(resolve => {
|
|
outputStream.end(resolve);
|
|
});
|
|
// Close all batch files
|
|
const crPromises = readers.map(reader => reader.close());
|
|
await Promise.all(crPromises);
|
|
// Delete all batch files
|
|
const dbfPromises = [];
|
|
for (let i = 1; i <= batches; i++) {
|
|
dbfPromises.push(promise_fs_1.pfs.rm(`${buildFile}.${i}`));
|
|
}
|
|
await Promise.all(dbfPromises);
|
|
}
|
|
catch (err) {
|
|
// EEXIST error is ok because that means the .merge file was already built
|
|
if ((err === null || err === void 0 ? void 0 : err.code) !== 'EEXIST') {
|
|
throw err;
|
|
}
|
|
}
|
|
};
|
|
const startTime = Date.now();
|
|
const lock = await this._lock('exclusive', 24 * 60 * 60 * 1000); // Allow 24hrs to build the index max
|
|
try {
|
|
try {
|
|
await createBuildFile();
|
|
}
|
|
catch (err) {
|
|
// If the .build file already existed, use it!
|
|
if (err.code !== 'EEXIST') {
|
|
throw err;
|
|
}
|
|
}
|
|
// Done writing values to build file.
|
|
// Now we have to group all values per key, sort them.
|
|
// then create the binary B+tree.
|
|
this.storage.debug.log(`done writing build file ${buildFile}`);
|
|
await createMergeFile();
|
|
// Open merge file for reading, index file for writing
|
|
this.storage.debug.log(`done writing merge file ${mergeFile}`);
|
|
const [readFD, writeFD] = await Promise.all([
|
|
indexedValues === 0 ? -1 : promise_fs_1.pfs.open(mergeFile, promise_fs_1.pfs.flags.read),
|
|
promise_fs_1.pfs.open(this.fileName, promise_fs_1.pfs.flags.write),
|
|
]);
|
|
// create index from entry stream
|
|
const treeStatistics = {
|
|
totalEntries: 0,
|
|
totalValues: 0,
|
|
};
|
|
const headerStats = {
|
|
written: false,
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
updateTreeLength: (treeByteLength) => {
|
|
throw new Error('header hasn\'t been written yet');
|
|
},
|
|
length: DISK_BLOCK_SIZE,
|
|
promise: undefined,
|
|
};
|
|
const writer = btree_1.BinaryWriter.forFunction(async (data, index) => {
|
|
if (!headerStats.written) {
|
|
// Write header first, or wait until done
|
|
if (!headerStats.promise) {
|
|
headerStats.promise = this._writeIndexHeader(writeFD, treeStatistics).then(async (result) => {
|
|
headerStats.written = true;
|
|
headerStats.length = result.length;
|
|
headerStats.updateTreeLength = result.treeLengthCallback;
|
|
if (this.state === DataIndex.STATE.REBUILD) {
|
|
await promise_fs_1.pfs.truncate(this.fileName, headerStats.length);
|
|
}
|
|
});
|
|
}
|
|
await headerStats.promise;
|
|
}
|
|
await promise_fs_1.pfs.write(writeFD, data, 0, data.length, headerStats.length + index);
|
|
});
|
|
const reader = indexedValues > 0
|
|
? new btree_1.BinaryReader(readFD)
|
|
: new btree_1.BinaryReader(async (index, length) => Buffer.from([]));
|
|
await btree_1.BinaryBPlusTree.createFromEntryStream(reader, writer, {
|
|
treeStatistics,
|
|
fillFactor: FILL_FACTOR,
|
|
maxEntriesPerNode: 255,
|
|
isUnique: false,
|
|
keepFreeSpace: true,
|
|
metadataKeys: this.allMetadataKeys,
|
|
});
|
|
await Promise.all([
|
|
promise_fs_1.pfs.fsync(writeFD).then(() => promise_fs_1.pfs.close(writeFD)),
|
|
indexedValues > 0 && promise_fs_1.pfs.close(readFD),
|
|
]);
|
|
if (indexedValues > 0) {
|
|
await promise_fs_1.pfs.rm(mergeFile);
|
|
}
|
|
const doneTime = Date.now();
|
|
const duration = Math.round((doneTime - startTime) / 1000 / 60);
|
|
this.storage.debug.log(`Index ${this.description} was built successfully, took ${duration} minutes`.colorize(acebase_core_1.ColorStyle.green));
|
|
this.state = DataIndex.STATE.READY;
|
|
}
|
|
catch (err) {
|
|
this.storage.debug.error(`Error building index ${this.description}: ${(err === null || err === void 0 ? void 0 : err.message) || err}`);
|
|
this.state = DataIndex.STATE.ERROR;
|
|
this._buildError = err;
|
|
throw err;
|
|
}
|
|
finally {
|
|
lock.release(); // release index lock
|
|
}
|
|
this._processUpdateQueue(); // Process updates queued during build
|
|
return this;
|
|
}
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
test(obj, op, val) { throw new Error('test method must be overridden by subclass'); }
|
|
_getIndexHeaderBytes(treeStatistics) {
|
|
const indexEntries = treeStatistics.totalEntries;
|
|
const indexedValues = treeStatistics.totalValues;
|
|
const addNameBytes = (bytes, name) => {
|
|
// name_length:
|
|
bytes.push(name.length);
|
|
// name_data:
|
|
for (let i = 0; i < name.length; i++) {
|
|
bytes.push(name.charCodeAt(i));
|
|
}
|
|
};
|
|
const addValueBytes = (bytes, value) => {
|
|
let valBytes = [];
|
|
if (typeof value === 'undefined') {
|
|
// value_type:
|
|
bytes.push(INDEX_INFO_VALUE_TYPE.UNDEFINED);
|
|
// no value_length or value_data
|
|
return;
|
|
}
|
|
else if (typeof value === 'string') {
|
|
// value_type:
|
|
bytes.push(INDEX_INFO_VALUE_TYPE.STRING);
|
|
valBytes = Array.from(encodeString(value)); // textEncoder.encode(value)
|
|
}
|
|
else if (typeof value === 'number') {
|
|
// value_type:
|
|
bytes.push(INDEX_INFO_VALUE_TYPE.NUMBER);
|
|
valBytes = numberToBytes(value);
|
|
}
|
|
else if (typeof value === 'boolean') {
|
|
// value_type:
|
|
bytes.push(INDEX_INFO_VALUE_TYPE.BOOLEAN);
|
|
// no value_length
|
|
// value_data:
|
|
bytes.push(value ? 1 : 0);
|
|
// done
|
|
return;
|
|
}
|
|
else if (value instanceof Array) {
|
|
// value_type:
|
|
bytes.push(INDEX_INFO_VALUE_TYPE.ARRAY);
|
|
// value_length:
|
|
if (value.length > 0xffff) {
|
|
throw new Error('Array is too large to store. Max length is 0xffff');
|
|
}
|
|
bytes.push((value.length >> 8) & 0xff);
|
|
bytes.push(value.length & 0xff);
|
|
// value_data:
|
|
value.forEach(val => {
|
|
addValueBytes(bytes, val);
|
|
});
|
|
// done
|
|
return;
|
|
}
|
|
// Maybe in the future:
|
|
// else if (value !== null && typeof value === 'object') {
|
|
// // value_type:
|
|
// bytes.push(INDEX_INFO_VALUE_TYPE.OBJECT);
|
|
// // value_length:
|
|
// const keys = Object.keys(value);
|
|
// if (keys.length > 0xffff) {
|
|
// throw new Error('Object is too large to store. Max properties is 0xffff');
|
|
// }
|
|
// bytes.push((keys.length >> 8) & 0xff);
|
|
// bytes.push(keys.length & 0xff);
|
|
// // value_data:
|
|
// keys.forEach(key => {
|
|
// const val = value[key];
|
|
// addNameBytes(bytes, key);
|
|
// addValueBytes(bytes, val);
|
|
// });
|
|
// // done
|
|
// return;
|
|
// }
|
|
else {
|
|
throw new Error(`Invalid value type "${typeof value}"`);
|
|
}
|
|
// value_length:
|
|
bytes.push((valBytes.length >> 8) & 0xff);
|
|
bytes.push(valBytes.length & 0xff);
|
|
// value_data:
|
|
bytes.push(...valBytes);
|
|
};
|
|
const addInfoBytes = (bytes, obj) => {
|
|
const keys = Object.keys(obj);
|
|
// info_count:
|
|
bytes.push(keys.length);
|
|
// info, [info, [info...]]
|
|
keys.forEach(key => {
|
|
addNameBytes(bytes, key); // name
|
|
const value = obj[key];
|
|
addValueBytes(bytes, value);
|
|
});
|
|
};
|
|
const header = [
|
|
// signature:
|
|
65, 67, 69, 66, 65, 83, 69, 73, 68, 88,
|
|
// layout_version:
|
|
1,
|
|
// header_length:
|
|
0, 0, 0, 0,
|
|
];
|
|
// info:
|
|
const indexInfo = {
|
|
type: this.type,
|
|
version: 1,
|
|
path: this.path,
|
|
key: this.key,
|
|
include: this.includeKeys,
|
|
cs: this.caseSensitive,
|
|
locale: this.textLocale,
|
|
localeKey: this.textLocaleKey,
|
|
// Don't store:
|
|
// config: this.config,
|
|
};
|
|
addInfoBytes(header, indexInfo);
|
|
// const treeNames = Object.keys(this.trees);
|
|
// trees_info:
|
|
header.push(1); // trees_count
|
|
const treeName = 'default';
|
|
const treeDetails = this.trees[treeName];
|
|
// tree_info:
|
|
addNameBytes(header, treeName); // tree_name
|
|
const treeRefIndex = header.length;
|
|
header.push(0, 0, 0, 0); // file_index
|
|
header.push(0, 0, 0, 0); // byte_length
|
|
treeDetails.entries = indexEntries;
|
|
treeDetails.values = indexedValues;
|
|
const extraTreeInfo = {
|
|
class: treeDetails.class,
|
|
version: treeDetails.version,
|
|
entries: indexEntries,
|
|
values: indexedValues,
|
|
};
|
|
addInfoBytes(header, extraTreeInfo);
|
|
// align header bytes to block size
|
|
while (header.length % DISK_BLOCK_SIZE !== 0) {
|
|
header.push(0);
|
|
}
|
|
// end of header
|
|
const headerLength = header.length;
|
|
treeDetails.fileIndex = headerLength;
|
|
// treeDetails.byteLength = binary.length;
|
|
// Update header_length:
|
|
header[11] = (headerLength >> 24) & 0xff;
|
|
header[12] = (headerLength >> 16) & 0xff;
|
|
header[13] = (headerLength >> 8) & 0xff;
|
|
header[14] = headerLength & 0xff;
|
|
// Update default tree file_index:
|
|
header[treeRefIndex] = (headerLength >> 24) & 0xff;
|
|
header[treeRefIndex + 1] = (headerLength >> 16) & 0xff;
|
|
header[treeRefIndex + 2] = (headerLength >> 8) & 0xff;
|
|
header[treeRefIndex + 3] = headerLength & 0xff;
|
|
// // Update default tree byte_length:
|
|
// header[treeRefIndex+4] = (binary.byteLength >> 24) & 0xff;
|
|
// header[treeRefIndex+5] = (binary.byteLength >> 16) & 0xff;
|
|
// header[treeRefIndex+6] = (binary.byteLength >> 8) & 0xff;
|
|
// header[treeRefIndex+7] = binary.byteLength & 0xff;
|
|
// anything else?
|
|
return { header, headerLength, treeRefIndex, treeDetails };
|
|
}
|
|
async _writeIndexHeader(fd, treeStatistics) {
|
|
const { header, headerLength, treeRefIndex } = this._getIndexHeaderBytes(treeStatistics);
|
|
await promise_fs_1.pfs.write(fd, Buffer.from(header));
|
|
return {
|
|
length: headerLength,
|
|
treeLengthCallback: async (treeByteLength) => {
|
|
const bytes = [
|
|
(treeByteLength >> 24) & 0xff,
|
|
(treeByteLength >> 16) & 0xff,
|
|
(treeByteLength >> 8) & 0xff,
|
|
treeByteLength & 0xff,
|
|
];
|
|
// treeDetails.byteLength = treeByteLength;
|
|
await promise_fs_1.pfs.write(fd, Buffer.from(bytes), 0, bytes.length, treeRefIndex + 4);
|
|
},
|
|
};
|
|
}
|
|
async _writeIndex(builder) {
|
|
// Index v1 layout:
|
|
// data = header, trees_data
|
|
// header = signature, layout_version, header_length, index_info, trees_info
|
|
// signature = 10 bytes ('ACEBASEIDX')
|
|
// layout_version = 1 byte number (binary layout version)
|
|
// header_length = byte_length
|
|
// byte_length = 4 byte uint
|
|
// index_info = info_count, info, [info, [info...]]
|
|
// info_count = 1 byte number
|
|
// info = key, info_value
|
|
// key = key_length, key_name
|
|
// key_length = 1 byte number
|
|
// key_name = [key_length] bytes (ASCII encoded key name)
|
|
// info_value = value_type, [value_length], [value_data]
|
|
// value_type = 1 byte number
|
|
// 0: UNDEFINED
|
|
// 1: STRING
|
|
// 2: NUMBER
|
|
// 3: BOOLEAN
|
|
// 4: ARRAY
|
|
// value_length = value_type ?
|
|
// 0, 3: (not present)
|
|
// 1, 2, 4: 2 byte number
|
|
// value_data = value_type ?
|
|
// 0: (not present)
|
|
// 1-3: value_length bytes
|
|
// 4: info_value[value_length]
|
|
// trees_info = trees_count, tree_info, [tree_info, [tree_info...]]
|
|
// trees_count = 1 byte number
|
|
// tree_info = tree_name, file_index, byte_length, xtree_info
|
|
// tree_name = key
|
|
// file_index = 4 byte uint
|
|
// xtree_info = info_count, info, [info, [info...]]
|
|
// trees_data = tree_data, [tree_data, [tree_date...]]
|
|
// tree_data = [byte_length] bytes of data (from tree_info header)
|
|
const totalEntries = builder.list.size;
|
|
const totalValues = builder.indexedValues;
|
|
// const tree = builder.create();
|
|
// const binary = new Uint8Array(tree.toBinary(true));
|
|
const fd = await promise_fs_1.pfs.open(this.fileName, promise_fs_1.pfs.flags.write);
|
|
const { header, headerLength, treeRefIndex, treeDetails } = this._getIndexHeaderBytes({ totalEntries, totalValues });
|
|
try {
|
|
await promise_fs_1.pfs.write(fd, Buffer.from(header));
|
|
// append binary tree data
|
|
const tree = builder.create();
|
|
const stream = promise_fs_1.pfs.fs.createWriteStream(null, { fd, autoClose: false });
|
|
const references = [];
|
|
const writer = new btree_1.BinaryWriter(stream, async (data, position) => {
|
|
references.push({ data, position });
|
|
// return pfs.write(fd, data, 0, data.byteLength, headerLength + position);
|
|
});
|
|
await tree.toBinary(true, writer);
|
|
// Update all references
|
|
while (references.length > 0) {
|
|
const ref = references.shift();
|
|
await promise_fs_1.pfs.write(fd, ref.data, 0, ref.data.byteLength, headerLength + ref.position);
|
|
}
|
|
// Update default tree byte_length:
|
|
const treeByteLength = writer.length;
|
|
const bytes = [
|
|
(treeByteLength >> 24) & 0xff,
|
|
(treeByteLength >> 16) & 0xff,
|
|
(treeByteLength >> 8) & 0xff,
|
|
treeByteLength & 0xff,
|
|
];
|
|
treeDetails.byteLength = treeByteLength;
|
|
await promise_fs_1.pfs.write(fd, Buffer.from(bytes), 0, bytes.length, treeRefIndex + 4);
|
|
// return pfs.write(fd, binary);
|
|
await promise_fs_1.pfs.close(fd);
|
|
}
|
|
catch (err) {
|
|
this.storage.debug.error(err);
|
|
throw err;
|
|
}
|
|
}
|
|
async _getTree(lockMode = 'exclusive') {
|
|
// File is now opened the first time it is requested, only closed when it needs to be rebuilt or removed
|
|
// This enables the tree to keep its FST state in memory.
|
|
// Also enabled "autoGrow" again, this allows the tree to grow instead of being rebuilt every time it needs
|
|
// more storage space
|
|
if ([DataIndex.STATE.ERROR, DataIndex.STATE.CLOSED, DataIndex.STATE.REMOVED].includes(this.state)) {
|
|
throw new Error(`Can't open index ${this.description} with state "${this.state}"`);
|
|
}
|
|
const lock = await this._lock(lockMode);
|
|
if (!this._idx) {
|
|
// File being opened for the first time (or after a rebuild)
|
|
const fd = await promise_fs_1.pfs.open(this.fileName, promise_fs_1.pfs.flags.readAndWrite);
|
|
const reader = async (index, length) => {
|
|
const buffer = Buffer.alloc(length);
|
|
const { bytesRead } = await promise_fs_1.pfs.read(fd, buffer, 0, length, this.trees.default.fileIndex + index);
|
|
if (bytesRead < length) {
|
|
return buffer.slice(0, bytesRead);
|
|
}
|
|
return buffer;
|
|
};
|
|
const writer = async (data, index) => {
|
|
const buffer = data.constructor === Uint8Array
|
|
? Buffer.from(data.buffer, data.byteOffset, data.byteLength)
|
|
: Buffer.from(data);
|
|
const result = await promise_fs_1.pfs.write(fd, buffer, 0, data.length, this.trees.default.fileIndex + index);
|
|
return result;
|
|
};
|
|
const tree = new btree_1.BinaryBPlusTree(reader, DISK_BLOCK_SIZE, writer);
|
|
tree.id = acebase_core_1.ID.generate(); // this.fileName; // For tree locking
|
|
tree.autoGrow = true; // Allow the tree to grow. DISABLE THIS IF THERE ARE MULTIPLE TREES IN THE INDEX FILE LATER! (which is not implemented yet)
|
|
this._idx = { fd, tree };
|
|
}
|
|
return {
|
|
tree: this._idx.tree,
|
|
/** Closes the index file, does not release the lock! */
|
|
close: async () => {
|
|
const fd = this._idx.fd;
|
|
this._idx = null;
|
|
await promise_fs_1.pfs.close(fd)
|
|
.catch(err => {
|
|
this.storage.debug.warn(`Could not close index file "${this.fileName}":`, err);
|
|
});
|
|
},
|
|
/** Releases the acquired tree lock */
|
|
release() {
|
|
lock.release();
|
|
},
|
|
};
|
|
}
|
|
}
|
|
exports.DataIndex = DataIndex;
|
|
class IndexQueryResult {
|
|
constructor(key, path, value, metadata) {
|
|
this.key = key;
|
|
this.path = path;
|
|
this.value = value;
|
|
this.metadata = metadata;
|
|
}
|
|
}
|
|
exports.IndexQueryResult = IndexQueryResult;
|
|
class IndexQueryResults extends Array {
|
|
constructor(...args) {
|
|
super(...args);
|
|
this.hints = [];
|
|
this.stats = null;
|
|
}
|
|
static fromResults(results, filterKey) {
|
|
const arr = new IndexQueryResults(results.length);
|
|
results.forEach((result, i) => arr[i] = result);
|
|
arr.filterKey = filterKey;
|
|
return arr;
|
|
}
|
|
// /** @param {BinaryBPlusTreeLeafEntry[]} entries */
|
|
// set treeEntries(entries) {
|
|
// this._treeEntries = entries;
|
|
// }
|
|
// /** @type {BinaryBPlusTreeLeafEntry[]} */
|
|
// get treeEntries() {
|
|
// return this._treeEntries;
|
|
// }
|
|
// filter(callback: (result: IndexQueryResult, index: number, arr: IndexQueryResults) => boolean) {
|
|
// return super.filter(callback);
|
|
// }
|
|
filterMetadata(key, op, compare) {
|
|
if (typeof compare === 'undefined') {
|
|
compare = null; // compare with null so <, <=, > etc will get the right results
|
|
}
|
|
if (op === 'exists' || op === '!exists') {
|
|
op = op === 'exists' ? '!=' : '==';
|
|
compare = null;
|
|
}
|
|
const filtered = this.filter(result => {
|
|
let value = key === this.filterKey ? result.value : result.metadata ? result.metadata[key] : null;
|
|
if (typeof value === 'undefined') {
|
|
value = null; // compare with null
|
|
}
|
|
if (op === '<') {
|
|
return value < compare;
|
|
}
|
|
if (op === '<=') {
|
|
return value <= compare;
|
|
}
|
|
if (op === '>') {
|
|
return value > compare;
|
|
}
|
|
if (op === '>=') {
|
|
return value >= compare;
|
|
}
|
|
if (op === '==') {
|
|
return value == compare;
|
|
}
|
|
if (op === '!=') {
|
|
return value != compare;
|
|
}
|
|
if (op === 'like' || op === '!like') {
|
|
if (typeof compare !== 'string') {
|
|
return op === '!like';
|
|
}
|
|
const pattern = '^' + compare.replace(/\*/g, '.*').replace(/\?/g, '.') + '$';
|
|
const re = new RegExp(pattern, 'i');
|
|
const isLike = re.test(value);
|
|
return op === 'like' ? isLike : !isLike;
|
|
}
|
|
if (op === 'in' || op === '!in') {
|
|
const isIn = compare instanceof Array && compare.includes(value);
|
|
return op === 'in' ? isIn : !isIn;
|
|
}
|
|
if (op == 'between' || op === '!between') {
|
|
if (!(compare instanceof Array)) {
|
|
return op === '!between';
|
|
}
|
|
let bottom = compare[0], top = compare[1];
|
|
if (top < bottom) {
|
|
const swap = top;
|
|
top = bottom;
|
|
bottom = swap;
|
|
}
|
|
const isBetween = value >= bottom && value <= top;
|
|
return op === 'between' ? isBetween : !isBetween;
|
|
}
|
|
if (op === 'matches' || op === '!matches') {
|
|
if (!(compare instanceof RegExp)) {
|
|
return op === '!matches';
|
|
}
|
|
const re = compare;
|
|
const isMatch = re.test(value);
|
|
return op === 'matches' ? isMatch : !isMatch;
|
|
}
|
|
});
|
|
return IndexQueryResults.fromResults(filtered, this.filterKey);
|
|
}
|
|
}
|
|
exports.IndexQueryResults = IndexQueryResults;
|
|
class IndexQueryStats {
|
|
constructor(type, args, start = false) {
|
|
this.type = type;
|
|
this.args = args;
|
|
this.started = 0;
|
|
this.stopped = 0;
|
|
this.steps = [];
|
|
this.result = null;
|
|
/**
|
|
* Used by GeoIndex: amount of queries executed to get results
|
|
*/
|
|
this.queries = 1;
|
|
if (start) {
|
|
this.start();
|
|
}
|
|
}
|
|
start() {
|
|
this.started = Date.now();
|
|
}
|
|
stop(result = null) {
|
|
this.stopped = Date.now();
|
|
this.result = result;
|
|
}
|
|
get duration() { return this.stopped - this.started; }
|
|
}
|
|
/**
|
|
* An array index allows all values in an array node to be indexed and searched
|
|
*/
|
|
class ArrayIndex extends DataIndex {
|
|
constructor(storage, path, key, options) {
|
|
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, oldValue, newValue) {
|
|
const tmpOld = oldValue !== null && typeof oldValue === 'object' && this.key in oldValue ? oldValue[this.key] : null;
|
|
const tmpNew = newValue !== null && typeof newValue === 'object' && this.key in newValue ? newValue[this.key] : null;
|
|
let oldEntries;
|
|
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;
|
|
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: {}, new: {} };
|
|
Object.assign(mutated.old, oldValue);
|
|
Object.assign(mutated.new, newValue);
|
|
const promises = [];
|
|
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, 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: [node_1.Node.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;
|
|
}
|
|
/**
|
|
* @param op "contains" or "!contains"
|
|
* @param val value to search for
|
|
*/
|
|
async query(op, val, options) {
|
|
if (op instanceof btree_1.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.storage.debug.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 btree_1.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;
|
|
if (counts[0].count === 0) {
|
|
stats.stop(0);
|
|
this.storage.debug.log(`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, filter) => {
|
|
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 btree_1.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;
|
|
}
|
|
}
|
|
}
|
|
exports.ArrayIndex = ArrayIndex;
|
|
class WordInfo {
|
|
constructor(word, indexes, sourceIndexes) {
|
|
this.word = word;
|
|
this.indexes = indexes;
|
|
this.sourceIndexes = sourceIndexes;
|
|
}
|
|
get occurs() {
|
|
return this.indexes.length;
|
|
}
|
|
}
|
|
// const _wordsRegex = /[\w']+/gmi; // TODO: should use a better pattern that supports non-latin characters
|
|
class TextInfo {
|
|
constructor(text, options) {
|
|
var _a;
|
|
// 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) {
|
|
console.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, `"${(_a = options.includeChars) !== null && _a !== void 0 ? _a : ''}`);
|
|
}
|
|
// Unidecode text to get ASCII characters only
|
|
function safe_unidecode(str) {
|
|
// Fix for occasional multi-pass issue, copied from https://github.com/FGRibreau/node-unidecode/issues/16
|
|
let ret;
|
|
while (str !== (ret = (0, unidecode_1.default)(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++;
|
|
}
|
|
}
|
|
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) {
|
|
const settings = {};
|
|
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[key] = this[locale][key];
|
|
});
|
|
return settings;
|
|
},
|
|
};
|
|
}
|
|
getWordInfo(word) {
|
|
return this.words.get(word);
|
|
}
|
|
/**
|
|
* Reconstructs an array of words in the order they were encountered
|
|
*/
|
|
toSequence() {
|
|
const arr = [];
|
|
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 = [];
|
|
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);
|
|
}
|
|
}
|
|
/**
|
|
* 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
|
|
*/
|
|
class FullTextIndex extends DataIndex {
|
|
constructor(storage, path, key, options) {
|
|
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
|
|
storage.debug.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, locale) {
|
|
return new TextInfo(val, {
|
|
locale: locale !== null && locale !== void 0 ? 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, op, val) {
|
|
var _a;
|
|
if (obj === null) {
|
|
return op === 'fulltext:!contains';
|
|
}
|
|
const text = obj[this.key];
|
|
if (typeof text === 'undefined') {
|
|
return op === 'fulltext:!contains';
|
|
}
|
|
const locale = (_a = obj === null || obj === void 0 ? void 0 : obj[this.textLocaleKey]) !== null && _a !== void 0 ? _a : 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, occurrenceIndex) => {
|
|
var _a;
|
|
const startIndex = (_a = occurrencesPerWord[wordIndex]) === null || _a === void 0 ? void 0 : _a[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, oldValue, newValue) {
|
|
var _a, _b;
|
|
let oldText = oldValue !== null && typeof oldValue === 'object' && this.key in oldValue ? oldValue[this.key] : null;
|
|
let newText = newValue !== null && typeof newValue === 'object' && this.key in newValue ? newValue[this.key] : null;
|
|
const oldLocale = (_a = oldValue === null || oldValue === void 0 ? void 0 : oldValue[this.textLocaleKey]) !== null && _a !== void 0 ? _a : this.textLocale, newLocale = (_b = newValue === null || newValue === void 0 ? void 0 : newValue[this.textLocaleKey]) !== null && _b !== void 0 ? _b : 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 = [];
|
|
// 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 = {};
|
|
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, 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.storage.debug.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 = {
|
|
'_occurs_': occurs,
|
|
};
|
|
Object.assign(wordMetadata, metadata);
|
|
add(wordInfo.word, recordPointer, wordMetadata);
|
|
});
|
|
return textInfo.toArray(); //words.map(info => info.word);
|
|
},
|
|
valueTypes: [node_1.Node.VALUE_TYPES.STRING],
|
|
});
|
|
}
|
|
static get validOperators() {
|
|
return ['fulltext:contains', 'fulltext:!contains'];
|
|
}
|
|
get validOperators() {
|
|
return FullTextIndex.validOperators;
|
|
}
|
|
async query(op, val, options) {
|
|
if (op instanceof btree_1.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, val, options = {
|
|
phrase: false,
|
|
locale: undefined,
|
|
minimumWildcardWordLength: 2,
|
|
}) {
|
|
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) => {
|
|
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 = {};
|
|
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) {
|
|
// 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 btree_1.BlacklistingSearchOperator(entry => {
|
|
const blacklist = wordChecks.some(word => {
|
|
if (word instanceof RegExp) {
|
|
return word.test(entry.key);
|
|
}
|
|
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;
|
|
if (counts[0].count === 0) {
|
|
stats.stop(0);
|
|
this.storage.debug.log(`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, filter) => {
|
|
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 = 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.storage.debug.log(`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();
|
|
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._occurs_ = match.metadata._occurs_.split(',').map(parseInt);
|
|
});
|
|
const check = (wordMatchIndex, prevWordIndex) => {
|
|
const sourceIndexes = wordMatches[wordMatchIndex].metadata._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;
|
|
// });
|
|
}
|
|
}
|
|
exports.FullTextIndex = FullTextIndex;
|
|
function _getGeoRadiusPrecision(radiusM) {
|
|
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) {
|
|
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, lon, radiusM, precision) {
|
|
const isInCircle = (checkLat, checkLon, lat, lon, radiusM) => {
|
|
const deltaLon = checkLon - lon;
|
|
const deltaLat = checkLat - lat;
|
|
return Math.pow(deltaLon, 2) + Math.pow(deltaLat, 2) <= Math.pow(radiusM, 2);
|
|
};
|
|
const getCentroid = (latitude, longitude, height, width) => {
|
|
const y_cen = latitude + (height / 2);
|
|
const x_cen = longitude + (width / 2);
|
|
return { x: x_cen, y: y_cen };
|
|
};
|
|
const convertToLatLon = (y, x, lat, lon) => {
|
|
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 = [];
|
|
const geohashes = [];
|
|
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;
|
|
}
|
|
class GeoIndex extends DataIndex {
|
|
constructor(storage, path, key, options) {
|
|
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, oldValue, newValue) {
|
|
const mutated = { old: {}, new: {} };
|
|
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, recordPointer, metadata) => {
|
|
if (typeof obj !== 'object') {
|
|
this.storage.debug.warn(`GeoIndex cannot index location because value "${obj}" is not an object`);
|
|
return;
|
|
}
|
|
if (typeof obj.lat !== 'number' || typeof obj.long !== 'number') {
|
|
this.storage.debug.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: [node_1.Node.VALUE_TYPES.OBJECT],
|
|
});
|
|
}
|
|
static get validOperators() {
|
|
return ['geo:nearby'];
|
|
}
|
|
get validOperators() {
|
|
return GeoIndex.validOperators;
|
|
}
|
|
test(obj, op, val) {
|
|
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];
|
|
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, checkLon, lat, lon, radiusM) => {
|
|
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, val, options) {
|
|
if (op instanceof btree_1.BlacklistingSearchOperator) {
|
|
throw new Error(`Not implemented: Can't query geo index with blacklisting operator yet`);
|
|
}
|
|
if (options) {
|
|
this.storage.debug.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)) {
|
|
throw new Error(`geo nearby query expects an object with lat, long and radius properties`);
|
|
}
|
|
return this.nearby(val);
|
|
}
|
|
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) {
|
|
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;
|
|
}
|
|
}
|
|
exports.GeoIndex = GeoIndex;
|
|
class IndexQueryHint {
|
|
constructor(type, value) {
|
|
this.type = type;
|
|
this.value = value;
|
|
}
|
|
}
|
|
class FullTextIndexQueryHint extends IndexQueryHint {
|
|
static get types() {
|
|
return Object.freeze({
|
|
missingWord: 'missing',
|
|
genericWord: 'generic',
|
|
ignoredWord: 'ignored',
|
|
});
|
|
}
|
|
constructor(type, value) {
|
|
super(type, value);
|
|
}
|
|
get description() {
|
|
switch (this.type) {
|
|
case FullTextIndexQueryHint.types.missingWord: {
|
|
return `Word "${this.value}" does not occur in the index, you might want to remove it from your query`;
|
|
}
|
|
case FullTextIndexQueryHint.types.genericWord: {
|
|
return `Word "${this.value}" is very generic and occurs many times in the index. Removing the word from your query will speed up the results and minimally impact the size of the result set`;
|
|
}
|
|
case FullTextIndexQueryHint.types.ignoredWord: {
|
|
return `Word "${this.value}" was ignored because it is either blacklisted, occurs in a stoplist, or did not match other criteria such as minimum (wildcard) word length`;
|
|
}
|
|
default: {
|
|
return 'Uknown hint';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
class ArrayIndexQueryHint extends IndexQueryHint {
|
|
static get types() {
|
|
return Object.freeze({
|
|
missingValue: 'missing',
|
|
});
|
|
}
|
|
constructor(type, value) {
|
|
super(type, value);
|
|
}
|
|
get description() {
|
|
const val = typeof this.value === 'string' ? `"${this.value}"` : this.value;
|
|
switch (this.type) {
|
|
case ArrayIndexQueryHint.types.missingValue: {
|
|
return `Value ${val} does not occur in the index, you might want to remove it from your query`;
|
|
}
|
|
default: {
|
|
return 'Uknown hint';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}).call(this)}).call(this,require("buffer").Buffer)
|
|
},{"../btree":226,"../geohash":undefined,"../node":245,"../node-value-types":244,"../promise-fs":247,"../quicksort":249,"../thread-safe":254,"acebase-core":12,"buffer":27,"unidecode":208}],238:[function(require,module,exports){
|
|
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.DetailedError = void 0;
|
|
/* eslint-disable @typescript-eslint/no-this-alias */
|
|
class DetailedError extends Error {
|
|
/**
|
|
*
|
|
* @param code code identifying the error
|
|
* @param message user/developer friendly error message
|
|
* @param originalError optional original error thrown to enable stack debugging for caught-rethrown errors
|
|
*/
|
|
constructor(code, message, originalError = null) {
|
|
super(message);
|
|
this.code = code;
|
|
this.originalError = originalError;
|
|
}
|
|
get codes() {
|
|
const arr = [];
|
|
let err = this;
|
|
while (err) {
|
|
arr.push(err instanceof DetailedError ? err.code : 'thrown');
|
|
err = err instanceof DetailedError ? err.originalError : null;
|
|
}
|
|
return arr;
|
|
}
|
|
get stacks() {
|
|
const arr = [];
|
|
let err = this;
|
|
while (err) {
|
|
arr.push(err.stack);
|
|
err = err instanceof DetailedError ? err.originalError : null;
|
|
}
|
|
return arr.join('\r\n-----------------\r\n');
|
|
}
|
|
hasErrorCode(code) {
|
|
let err = this;
|
|
while (err.code !== code && err.originalError) {
|
|
err = err.originalError;
|
|
}
|
|
return err.code === code;
|
|
// TODO: Maybe just use this for simplicity:
|
|
// return this.codes.includes(code);
|
|
}
|
|
static hasErrorCode(err, code) {
|
|
if (!(err instanceof DetailedError)) {
|
|
return false;
|
|
}
|
|
return err.hasErrorCode(code);
|
|
}
|
|
}
|
|
exports.DetailedError = DetailedError;
|
|
|
|
},{}],239:[function(require,module,exports){
|
|
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.IPCPeer = void 0;
|
|
const acebase_core_1 = require("acebase-core");
|
|
const ipc_1 = require("./ipc");
|
|
/**
|
|
* 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
|
|
window.addEventListener('beforeunload', () => {
|
|
this.exit();
|
|
});
|
|
// Create BroadcastChannel to allow multi-tab communication
|
|
// This allows other tabs to make changes to the database, notifying us of those changes.
|
|
if (typeof window.BroadcastChannel !== 'undefined') {
|
|
this.channel = new BroadcastChannel(`acebase:${storage.name}`);
|
|
}
|
|
else {
|
|
// 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
|
|
window.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 });
|
|
});
|
|
}
|
|
// 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;
|
|
}
|
|
storage.debug.verbose(`[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;
|
|
storage.debug.log(`[BroadcastChannel] Tab ${this.masterPeerId} is the master.`);
|
|
}
|
|
else if (message.type === 'bye' && message.from === this.masterPeerId) {
|
|
// The master tab is leaving
|
|
storage.debug.log(`[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];
|
|
storage.debug.log(`[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);
|
|
});
|
|
// // Schedule periodic "pulse" to let others know we're still around
|
|
// setInterval(() => {
|
|
// sendMessage(<IPulseMessage>{ 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.storage.debug.verbose(`[BroadcastChannel] sending: `, message);
|
|
this.channel.postMessage(message);
|
|
}
|
|
}
|
|
exports.IPCPeer = IPCPeer;
|
|
|
|
},{"./ipc":240,"acebase-core":12}],240:[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 {
|
|
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.debug, storage.settings.lockTimeout);
|
|
// Setup db event listeners
|
|
storage.on('subscribe', (subscription) => {
|
|
// Subscription was added to db
|
|
storage.debug.verbose(`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;
|
|
}
|
|
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);
|
|
});
|
|
});
|
|
}
|
|
get isMaster() { return this.masterPeerId === this.id; }
|
|
/**
|
|
* 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.storage.debug.warn(`Received ${this.isMaster ? 'master' : 'worker ' + this.id} process exit request`);
|
|
if (this._locks.length > 0) {
|
|
this.storage.debug.warn(`Waiting for ${this.isMaster ? 'master' : 'worker'} ${this.id} locks to clear`);
|
|
await this.once('locks-cleared');
|
|
}
|
|
// Send "bye"
|
|
this.sayGoodbye(this.id);
|
|
this.storage.debug.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);
|
|
});
|
|
}
|
|
}
|
|
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.storage.debug.log(`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;
|
|
}
|
|
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!`);
|
|
}
|
|
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': {
|
|
// 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;
|
|
}
|
|
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);
|
|
}
|
|
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);
|
|
}
|
|
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!`);
|
|
}
|
|
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();
|
|
}
|
|
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,
|
|
};
|
|
}
|
|
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));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* 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');
|
|
}
|
|
}
|
|
const removeLock = (lockDetails) => {
|
|
this._locks.splice(this._locks.indexOf(lockDetails), 1);
|
|
if (this._locks.length === 0) {
|
|
// this.storage.debug.log(`No more locks in worker ${this.id}`);
|
|
this.emit('locks-cleared');
|
|
}
|
|
};
|
|
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.storage.debug.verbose(`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.storage.debug.log(`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;
|
|
}
|
|
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.storage.debug.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.storage.debug.log(`ipc events ${enabled ? 'enabled' : 'disabled'}`);
|
|
this._eventsEnabled = enabled;
|
|
}
|
|
}
|
|
exports.AceBaseIPCPeer = AceBaseIPCPeer;
|
|
|
|
},{"../node-lock":243,"acebase-core":12}],241:[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;
|
|
|
|
},{}],242:[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.pageNr},${this.address.recordNr}`;
|
|
}
|
|
else {
|
|
return `"${this.path}" is ${this.valueTypeName} with value ${this.value}`;
|
|
}
|
|
}
|
|
}
|
|
exports.NodeInfo = NodeInfo;
|
|
|
|
},{"./node-value-types":244,"acebase-core":12}],243:[function(require,module,exports){
|
|
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.NodeLock = exports.NodeLocker = exports.LOCK_STATE = void 0;
|
|
const acebase_core_1 = require("acebase-core");
|
|
const DEBUG_MODE = false;
|
|
const DEFAULT_LOCK_TIMEOUT = 120; // in seconds
|
|
exports.LOCK_STATE = {
|
|
PENDING: 'pending',
|
|
LOCKED: 'locked',
|
|
EXPIRED: 'expired',
|
|
DONE: 'done',
|
|
};
|
|
class NodeLocker {
|
|
/**
|
|
* Provides locking mechanism for nodes, ensures no simultanious read and writes happen to overlapping paths
|
|
*/
|
|
constructor(debug, lockTimeout = DEFAULT_LOCK_TIMEOUT) {
|
|
this._locks = [];
|
|
this._lastTid = 0;
|
|
this.debug = debug;
|
|
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();
|
|
}
|
|
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();
|
|
}
|
|
}
|
|
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) {
|
|
throw new Error(`lock on tid ${tid} has expired, not allowed to continue`);
|
|
}
|
|
else if (this._quit && !options.withPriority) {
|
|
throw new Error(`Quitting`);
|
|
}
|
|
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.debug.warn(`${lock.forWriting ? 'write' : 'read'} lock on path "/${lock.path}" by tid ${lock.tid} (${lock.comment}) is taking a long time to complete [${timeoutCount}]`);
|
|
lock.timeout = setTimeout(timeoutHandler, this.timeout / 4);
|
|
return;
|
|
}
|
|
this.debug.error(`lock :: ${lock.forWriting ? 'write' : 'read'} lock on path "/${lock.path}" by tid ${lock.tid} (${lock.comment}) took too long`);
|
|
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);
|
|
}
|
|
}
|
|
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}`);
|
|
console.assert(lock.state === exports.LOCK_STATE.PENDING);
|
|
return new Promise((resolve, reject) => {
|
|
lock.resolve = resolve;
|
|
lock.reject = reject;
|
|
});
|
|
}
|
|
}
|
|
unlock(lockOrId, comment, processQueue = true) {
|
|
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 "/${lock.path}" for tid ${lock.tid} wasn't found; ${comment}`;
|
|
// debug.error(`unlock :: ${msg}`);
|
|
throw new Error(msg);
|
|
}
|
|
lock.state = exports.LOCK_STATE.DONE;
|
|
clearTimeout(lock.timeout);
|
|
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 {
|
|
/**
|
|
* 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 = [];
|
|
}
|
|
static get LOCK_STATE() { return exports.LOCK_STATE; }
|
|
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 {
|
|
// 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;
|
|
}
|
|
}
|
|
}
|
|
exports.NodeLock = NodeLock;
|
|
|
|
},{"acebase-core":12}],244:[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");
|
|
exports.VALUE_TYPES = {
|
|
// 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
|
|
};
|
|
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;
|
|
}
|
|
else if (value instanceof Date) {
|
|
return exports.VALUE_TYPES.DATETIME;
|
|
}
|
|
// 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 === '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}],245:[function(require,module,exports){
|
|
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.Node = void 0;
|
|
const node_value_types_1 = require("./node-value-types");
|
|
class Node {
|
|
static get VALUE_TYPES() {
|
|
return node_value_types_1.VALUE_TYPES;
|
|
}
|
|
}
|
|
exports.Node = Node;
|
|
|
|
},{"./node-value-types":244}],246:[function(require,module,exports){
|
|
// Not supported in current environment
|
|
},{}],247:[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;
|
|
|
|
},{}],248:[function(require,module,exports){
|
|
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.query = 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");
|
|
// 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`
|
|
*/
|
|
function query(api, path, query, options = { snapshots: false, include: undefined, exclude: undefined, child_objects: undefined, eventHandler: noop }) {
|
|
// TODO: Refactor to async
|
|
var _a;
|
|
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(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 batches = [];
|
|
const items = preResults.map((result, index) => ({ path: result.path, index }));
|
|
while (items.length > 0) {
|
|
const batchItems = items.splice(0, maxBatchSize);
|
|
batches.push(batchItems);
|
|
}
|
|
const results = [];
|
|
const nextBatch = async () => {
|
|
const batch = batches.shift();
|
|
await Promise.all(batch.map(async (item) => {
|
|
const { path, index } = item;
|
|
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.storage.debug.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);
|
|
// const ascending = querySort.length === 0 || (query.take >= 0 ? querySort[0].ascending : !querySort[0].ascending);
|
|
// if (ascending) {
|
|
// results.pop(); // Ascending sort order, toss last value
|
|
// }
|
|
// else {
|
|
// results.shift(); // Descending, toss first value
|
|
// }
|
|
results.pop(); // Always toss last value, results have been sorted already
|
|
}
|
|
}
|
|
}));
|
|
if (batches.length > 0) {
|
|
await nextBatch();
|
|
}
|
|
};
|
|
await nextBatch();
|
|
// Got all values
|
|
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 = [];
|
|
if (isWildcardPath) {
|
|
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);
|
|
}
|
|
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 });
|
|
}
|
|
}
|
|
// 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;
|
|
}
|
|
// // 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;
|
|
}
|
|
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.storage.debug.log(`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);
|
|
}
|
|
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 => {
|
|
options.eventHandler && options.eventHandler({ name: 'stats', type: 'index_query', source: filter.index.description, stats: results.stats });
|
|
if (results.hints.length > 0) {
|
|
options.eventHandler && options.eventHandler({ name: 'hints', type: 'index_query', source: filter.index.description, hints: results.hints });
|
|
}
|
|
return results;
|
|
});
|
|
// 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);
|
|
}
|
|
});
|
|
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.storage.debug.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) {
|
|
api.storage.debug.log(`Using index for sorting: ${sortIndex.description}`);
|
|
const promise = sortIndex.take(query.skip, Math.abs(query.take), ascending)
|
|
.then(results => {
|
|
options.eventHandler && options.eventHandler({ name: 'stats', type: 'sort_index_take', source: sortIndex.description, stats: results.stats });
|
|
if (results.hints.length > 0) {
|
|
options.eventHandler && options.eventHandler({ 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
|
|
// }
|
|
}
|
|
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;
|
|
}
|
|
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;
|
|
});
|
|
}
|
|
else {
|
|
// No need to take further actions, return what we have now
|
|
return indexedResults;
|
|
}
|
|
}
|
|
// 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: [],
|
|
add(promise) {
|
|
this.promises.push(promise);
|
|
if (this.promises.length >= 1000) {
|
|
return Promise.all(this.promises.splice(0)).then(_ => undefined);
|
|
}
|
|
},
|
|
};
|
|
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;
|
|
}
|
|
});
|
|
}
|
|
catch (reason) {
|
|
// No record?
|
|
if (!(reason instanceof node_errors_1.NodeNotFoundError)) {
|
|
api.storage.debug.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);
|
|
}
|
|
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);
|
|
}
|
|
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 => {
|
|
// Order the results
|
|
if (!stepsExecuted.sorted && querySort.length > 0) {
|
|
sortMatches(matches);
|
|
}
|
|
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);
|
|
}
|
|
// Limit result set
|
|
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);
|
|
}
|
|
// 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 };
|
|
}
|
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
let stop = async () => { };
|
|
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 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 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 && options.monitor.change) {
|
|
keepMonitoring = options.eventHandler({ name: 'change', path, value: newValue }) !== false;
|
|
}
|
|
else if (!wasMatch && options.monitor.add) {
|
|
keepMonitoring = options.eventHandler({ name: 'add', path, value: newValue }) !== false;
|
|
}
|
|
}
|
|
else if (wasMatch) {
|
|
removeMatch(path);
|
|
if (options.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 (options.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 (options.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.query = query;
|
|
|
|
},{"./data-index":237,"./node-errors":241,"./node-value-types":244,"acebase-core":12}],249:[function(require,module,exports){
|
|
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
/**
|
|
* Faster quicksort using a stack to eliminate recursion, sorting inplace to reduce memory usage, and using insertion sort for small partition sizes.
|
|
*
|
|
* Original author unknown.
|
|
* I (Ewout Stortenbeker) isolated the fast quicksort code from benchmark (see below), ported to TypeScript and added custom sort function argument.
|
|
*
|
|
* Benchmark results at https://www.measurethat.net/Benchmarks/Show/3549/0/javascript-sorting-algorithms indicate this algorithm is at least 10x faster
|
|
* than the native sort function (tested on Chrome v101, June 2022). My own tests (using sort function callbacks) indicate it's typically 1.5x faster than
|
|
* the native sort algorithm. This difference is probably caused by the built-in sort being called with a callback function in the benchmark, all others
|
|
* with basic < and > operators, which are obviously faster than callbacks.
|
|
*
|
|
* @param arr array to sort
|
|
* @param compareFn optional compare function to use. Must return a negative value if a < b, 0 if a == b, positive number if a > b
|
|
* @returns
|
|
*/
|
|
function fastQuickSort(arr, compareFn = (a, b) => a - b) {
|
|
if (arr.length <= 1) {
|
|
// No sorting needed, fixes #118
|
|
return arr;
|
|
}
|
|
const stack = [];
|
|
let entry = [
|
|
0,
|
|
arr.length,
|
|
2 * Math.floor(Math.log(arr.length) / Math.log(2))
|
|
];
|
|
stack.push(entry);
|
|
while (stack.length > 0) {
|
|
entry = stack.pop();
|
|
var start = entry[0];
|
|
var end = entry[1];
|
|
var depth = entry[2];
|
|
if (depth == 0) {
|
|
arr = shellSortBound(arr, start, end, compareFn);
|
|
continue;
|
|
}
|
|
depth--;
|
|
var pivot = Math.round((start + end) / 2);
|
|
var pivotNewIndex = inplaceQuickSortPartition(arr, start, end, pivot, compareFn);
|
|
if (end - pivotNewIndex > 16) {
|
|
entry = [pivotNewIndex, end, depth];
|
|
stack.push(entry);
|
|
}
|
|
if (pivotNewIndex - start > 16) {
|
|
entry = [start, pivotNewIndex, depth];
|
|
stack.push(entry);
|
|
}
|
|
}
|
|
arr = insertionSort(arr, compareFn);
|
|
return arr;
|
|
}
|
|
exports.default = fastQuickSort;
|
|
function shellSortBound(arr, start, end, compareFn) {
|
|
let inc = Math.round((start + end) / 2), i, j, t;
|
|
while (inc >= start) {
|
|
for (i = inc; i < end; i++) {
|
|
t = arr[i];
|
|
j = i;
|
|
while (j >= inc && compareFn(arr[j - inc], t) > 0) { // arr[j - inc] > t
|
|
arr[j] = arr[j - inc];
|
|
j -= inc;
|
|
}
|
|
arr[j] = t;
|
|
}
|
|
inc = Math.round(inc / 2.2);
|
|
}
|
|
return arr;
|
|
}
|
|
function swap(arr, a, b) {
|
|
var t = arr[a];
|
|
arr[a] = arr[b];
|
|
arr[b] = t;
|
|
}
|
|
// Insertion sort
|
|
function insertionSort(arr, compareFn) {
|
|
for (let i = 1, l = arr.length; i < l; i++) {
|
|
let value = arr[i];
|
|
for (var j = i - 1; j >= 0; j--) {
|
|
if (compareFn(arr[j], value) <= 0) // arr[j] <= value
|
|
break;
|
|
arr[j + 1] = arr[j];
|
|
}
|
|
arr[j + 1] = value;
|
|
}
|
|
return arr;
|
|
}
|
|
// In place quicksort
|
|
function inplaceQuickSortPartition(arr, start, end, pivotIndex, compareFn) {
|
|
var i = start, j = end;
|
|
var pivot = arr[pivotIndex];
|
|
while (true) {
|
|
while (compareFn(arr[i], pivot) < 0) { // arr[i] < pivot
|
|
i++;
|
|
}
|
|
j--;
|
|
while (compareFn(pivot, arr[j]) < 0) { // pivot < arr[j]
|
|
j--;
|
|
}
|
|
if (!(i < j)) {
|
|
return i;
|
|
}
|
|
swap(arr, i, j);
|
|
i++;
|
|
}
|
|
}
|
|
|
|
},{}],250:[function(require,module,exports){
|
|
const { ID, PathReference, PathInfo, ascii85, ColorStyle, Utils } = require('acebase-core');
|
|
const { compareValues } = Utils;
|
|
const { NodeInfo } = require('./node-info');
|
|
const { NodeLocker } = require('./node-lock');
|
|
const { VALUE_TYPES } = require('./node-value-types');
|
|
const { Storage, StorageSettings, NodeNotFoundError, NodeRevisionError } = require('./storage');
|
|
|
|
/** 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;
|
|
}
|
|
}
|
|
|
|
/** Interface for metadata combined with a stored value */
|
|
class ICustomStorageNode extends ICustomStorageNodeMetaData {
|
|
constructor() {
|
|
super();
|
|
/** @type {any} only Object, Array, large string and binary values. */
|
|
this.value = null;
|
|
}
|
|
}
|
|
|
|
/** Enables get/set/remove operations to be wrapped in transactions to improve performance and reliability. */
|
|
class CustomStorageTransaction {
|
|
|
|
/**
|
|
* @param {{ path: string, write: boolean }} target Which path the transaction is taking place on, and whether it is a read or read/write lock. If your storage backend does not support transactions, is synchronous, or if you are able to lock resources based on path: use storage.nodeLocker to ensure threadsafe transactions
|
|
*/
|
|
constructor(target) {
|
|
this.production = false; // dev mode by default
|
|
this.target = {
|
|
get originalPath() { return target.path; },
|
|
path: target.path,
|
|
get write() { return target.write; }
|
|
};
|
|
/** @type {string} Transaction ID */
|
|
this.id = ID.generate();
|
|
}
|
|
|
|
/**
|
|
* @param {string} path
|
|
* @returns {Promise<ICustomStorageNode>}
|
|
*/
|
|
// eslint-disable-next-line no-unused-vars
|
|
async get(path) { throw new Error(`CustomStorageTransaction.get must be overridden by subclass`); }
|
|
|
|
/**
|
|
* @param {string} path
|
|
* @param {ICustomStorageNode} node
|
|
* @returns {Promise<any>}
|
|
*/
|
|
// eslint-disable-next-line no-unused-vars
|
|
async set(path, node) { throw new Error(`CustomStorageTransaction.set must be overridden by subclass`); }
|
|
|
|
/**
|
|
* @param {string} path
|
|
* @returns {Promise<any>}
|
|
*/
|
|
// eslint-disable-next-line no-unused-vars
|
|
async remove(path) { throw new Error(`CustomStorageTransaction.remove must be overridden by subclass`); }
|
|
|
|
/**
|
|
*
|
|
* @param {string} path Parent path to load children of
|
|
* @param {object} include
|
|
* @param {boolean} include.metadata Whether metadata needs to be loaded
|
|
* @param {boolean} include.value Whether value needs to be loaded
|
|
* @param {(childPath: string) => boolean} checkCallback callback method to precheck if child needs to be added, perform before loading metadata/value if possible
|
|
* @param {(childPath: string, node?: ICustomStorageNodeMetaData|ICustomStorageNode) => boolean} addCallback callback method that adds the child node. Returns whether or not to keep calling with more children
|
|
* @returns {Promise<any>} Returns a promise that resolves when there are no more children to be streamed
|
|
*/
|
|
// eslint-disable-next-line no-unused-vars
|
|
async childrenOf(path, include, checkCallback, addCallback) { throw new Error(`CustomStorageTransaction.childrenOf must be overridden by subclass`); }
|
|
|
|
/**
|
|
*
|
|
* @param {string} path Parent path to load descendants of
|
|
* @param {object} include
|
|
* @param {boolean} include.metadata Whether metadata needs to be loaded
|
|
* @param {boolean} include.value Whether value needs to be loaded
|
|
* @param {(descPath: string, metadata?: ICustomStorageNodeMetaData) => boolean} checkCallback callback method to precheck if descendant needs to be added, perform before loading metadata/value if possible. NOTE: if include.metadata === true, you should load and pass the metadata to the checkCallback if doing so has no or small performance impact
|
|
* @param {(descPath: string, node?: ICustomStorageNodeMetaData|ICustomStorageNode) => boolean} addCallback callback method that adds the descendant node. Returns whether or not to keep calling with more children
|
|
* @returns {Promise<any>} Returns a promise that resolves when there are no more descendants to be streamed
|
|
*/
|
|
// eslint-disable-next-line no-unused-vars
|
|
async descendantsOf(path, include, checkCallback, addCallback) { throw new Error(`CustomStorageTransaction.descendantsOf must be overridden by subclass`); }
|
|
|
|
/**
|
|
* 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 {string} path
|
|
* @returns {Promise<number>} Returns a promise that resolves with the number of children
|
|
*/
|
|
async getChildCount(path) {
|
|
let childCount = 0;
|
|
await this.childrenOf(path, { metadata: false, value: false }, () => { childCount++; return false; });
|
|
return childCount;
|
|
}
|
|
|
|
/**
|
|
* NOT USED YET
|
|
* Default implementation of getMultiple that executes .get for each given path. Override for custom logic
|
|
* @param {string[]} paths
|
|
* @returns {Promise<Map<string, ICustomStorageNode>>} Returns promise with a Map of paths to nodes
|
|
*/
|
|
async getMultiple(paths) {
|
|
const map = new Map();
|
|
await Promise.all(paths.map(path => this.get(path).then(val => map.set(path, val))));
|
|
return map;
|
|
}
|
|
|
|
/**
|
|
* NOT USED YET
|
|
* Default implementation of setMultiple that executes .set for each given path. Override for custom logic
|
|
* @param {Array<{ path: string, node: ICustomStorageNode }>} nodes
|
|
*/
|
|
async setMultiple(nodes) {
|
|
await Promise.all(nodes.map(({ path, node }) => this.set(path, node)));
|
|
}
|
|
|
|
/**
|
|
* Default implementation of removeMultiple that executes .remove for each given path. Override for custom logic
|
|
* @param {string[]} paths
|
|
*/
|
|
async removeMultiple(paths) {
|
|
await Promise.all(paths.map(path => this.remove(path)));
|
|
}
|
|
|
|
/**
|
|
* @param {Error} reason
|
|
* @returns {Promise<any>}
|
|
*/
|
|
// eslint-disable-next-line no-unused-vars
|
|
async rollback(reason) { throw new Error(`CustomStorageTransaction.rollback must be overridden by subclass`); }
|
|
|
|
/**
|
|
* @returns {Promise<any>}
|
|
*/
|
|
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 {string} targetPath;
|
|
*/
|
|
async moveToParentPath(targetPath) {
|
|
const currentPath = (this._lock && this._lock.path) || this.target.path;
|
|
if (currentPath === targetPath) {
|
|
return targetPath; // Already on the right path
|
|
}
|
|
const pathInfo = CustomStorageHelpers.PathInfo.get(targetPath);
|
|
if (pathInfo.isParentOf(currentPath)) {
|
|
if (this._lock) {
|
|
this._lock = await this._lock.moveToParent();
|
|
}
|
|
}
|
|
else {
|
|
throw new Error(`Locking issue. Locked path "${this._lock.path}" is not a child/descendant of "${targetPath}"`);
|
|
}
|
|
this.target.path = targetPath;
|
|
return targetPath;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 StorageSettings {
|
|
|
|
/**
|
|
*
|
|
* @param {object} settings
|
|
* @param {string} [settings.name] Name of the custom storage adapter
|
|
* @param {boolean} [settings.locking=true] Whether default node locking should be used. Set to false if your storage backend disallows multiple simultanious write transactions (eg IndexedDB). Set to true if your storage backend does not support transactions (eg LocalStorage) or allows multiple simultanious write transactions (eg AceBase binary).
|
|
* @param {number} [settings.lockTimeout=120] If default node locking is used, timeout setting for read and write locks in seconds. Operations taking longer than this will be aborted. Default is 120 seconds.
|
|
* @param {() => Promise<any>} settings.ready Function that returns a Promise that resolves once your data store backend is ready for use
|
|
* @param {(target: { path: string, write: boolean }, nodeLocker: NodeLocker) => Promise<CustomStorageTransaction>} settings.getTransaction Function that starts a transaction for read/write operations on a specific path and/or child paths
|
|
*/
|
|
constructor(settings) {
|
|
super(settings);
|
|
settings = settings || {};
|
|
if (typeof settings.ready !== 'function') {
|
|
throw new Error(`ready must be a function`);
|
|
}
|
|
if (typeof settings.getTransaction !== 'function') {
|
|
throw new Error(`getTransaction must be a function`);
|
|
}
|
|
this.name = settings.name;
|
|
// this.info = `${this.name || 'CustomStorage'} realtime database`;
|
|
this.locking = settings.locking !== false;
|
|
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 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 });
|
|
console.assert(typeof transaction.id === 'string', `transaction id not set`);
|
|
// console.log(`Got transaction ${transaction.id} for ${write ? 'WRITE' : 'READ'} on path "${path}"`);
|
|
|
|
// Hijack rollback and commit
|
|
const rollback = transaction.rollback;
|
|
const commit = transaction.commit;
|
|
transaction.commit = async () => {
|
|
// console.log(`COMMIT ${transaction.id} for ${write ? 'WRITE' : 'READ'} on path "${path}"`);
|
|
const ret = await commit.call(transaction);
|
|
// console.log(`COMMIT DONE ${transaction.id} for ${write ? 'WRITE' : 'READ'} on path "${path}"`);
|
|
if (useLocking) {
|
|
await transaction._lock.release('commit');
|
|
}
|
|
return ret;
|
|
}
|
|
transaction.rollback = async (reason) => {
|
|
// const reasonText = reason instanceof Error ? reason.message : reason.toString();
|
|
// console.error(`ROLLBACK ${transaction.id} for ${write ? 'WRITE' : 'READ'} on path "${path}":`, reason);
|
|
const ret = await rollback.call(transaction, reason);
|
|
// console.log(`ROLLBACK DONE ${transaction.id} for ${write ? 'WRITE' : 'READ'} on path "${path}"`);
|
|
if (useLocking) {
|
|
await transaction._lock.release('rollback');
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
if (useLocking) {
|
|
// Lock the path before continuing
|
|
transaction._lock = await nodeLocker.lock(path, transaction.id, write, `${this.name}::getTransaction`);
|
|
}
|
|
return transaction;
|
|
}
|
|
}
|
|
}
|
|
|
|
class CustomStorageNodeAddress {
|
|
constructor(containerPath) {
|
|
this.path = containerPath;
|
|
}
|
|
}
|
|
|
|
class CustomStorageNodeInfo extends NodeInfo {
|
|
constructor(info) {
|
|
super(info);
|
|
|
|
/** @type {CustomStorageNodeAddress} */
|
|
this.address; // no assignment, only typedef
|
|
|
|
/** @type {string} */
|
|
this.revision = info.revision;
|
|
/** @type {number} */
|
|
this.revision_nr = info.revision_nr;
|
|
/** @type {Date} */
|
|
this.created = info.created;
|
|
/** @type {Date} */
|
|
this.modified = info.modified;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper functions to build custom storage classes with
|
|
*/
|
|
class CustomStorageHelpers {
|
|
/**
|
|
* Helper function that returns a SQL where clause for all children of given path
|
|
* @param {string} path Path to get children of
|
|
* @param {string} [columnName] Name of the Path column in your SQL db, default is 'path'
|
|
* @returns {string} Returns the SQL where clause
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Helper function that returns a regular expression to test if paths are children of the given path
|
|
* @param {string} path Path to test children of
|
|
* @returns {RegExp} 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 {string} path Path to get descendants of
|
|
* @param {string} [columnName] Name of the Path column in your SQL db, default is 'path'
|
|
* @returns {string} Returns the SQL where clause
|
|
*/
|
|
static DescendantPathsSql(path, columnName = 'path') {
|
|
const where = path === ''
|
|
? `${columnName} <> ''`
|
|
: `${columnName} LIKE '${path}/%' OR ${columnName} LIKE '${path}[%'`
|
|
return where;
|
|
}
|
|
/**
|
|
* Helper function that returns a regular expression to test if paths are descendants of the given path
|
|
* @param {string} path Path to test descendants of
|
|
* @returns {RegExp} Returns regular expression to test paths with
|
|
*/
|
|
static DescendantPathsRegex(path) {
|
|
return new RegExp(`^${path}(?:/[^/[]+|\\[[0-9]+\\])`);
|
|
}
|
|
|
|
/**
|
|
* 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]';
|
|
*/
|
|
static get PathInfo() {
|
|
return PathInfo;
|
|
}
|
|
}
|
|
|
|
class CustomStorage extends Storage {
|
|
|
|
/**
|
|
*
|
|
* @param {string} dbname
|
|
* @param {CustomStorageSettings} settings
|
|
*/
|
|
constructor(dbname, settings) {
|
|
super(dbname, settings);
|
|
|
|
this._init();
|
|
}
|
|
|
|
async _init() {
|
|
/** @type {CustomStorageSettings} */
|
|
this._customImplementation = this.settings;
|
|
this.debug.log(`Database "${this.name}" details:`.colorize(ColorStyle.dim));
|
|
this.debug.log(`- Type: CustomStorage`.colorize(ColorStyle.dim));
|
|
this.debug.log(`- Path: ${this.settings.path}`.colorize(ColorStyle.dim));
|
|
this.debug.log(`- Max inline value size: ${this.settings.maxInlineValueSize}`.colorize(ColorStyle.dim));
|
|
this.debug.log(`- Autoremove undefined props: ${this.settings.removeVoidProperties}`.colorize(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');
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {string} path
|
|
* @param {ICustomStorageNode} node
|
|
* @param {object} options
|
|
* @param {CustomStorageTransaction} options.transaction
|
|
* @returns {Promise<void>}
|
|
*/
|
|
_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: VALUE_TYPES.DATETIME, value: val.getTime() };
|
|
}
|
|
else if (val instanceof PathReference) {
|
|
return { type: VALUE_TYPES.REFERENCE, value: val.path };
|
|
}
|
|
else if (val instanceof ArrayBuffer) {
|
|
return { type: VALUE_TYPES.BINARY, value: ascii85.encode(val) };
|
|
}
|
|
else if (typeof val === 'object') {
|
|
console.assert(Object.keys(val).length === 0, 'child object stored in parent can only be empty');
|
|
return val;
|
|
}
|
|
}
|
|
|
|
const unprocessed = `Caller should have pre-processed the value by converting it to a string`;
|
|
if (node.type === VALUE_TYPES.ARRAY && node.value instanceof Array) {
|
|
// 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 === VALUE_TYPES.BINARY && typeof node.value !== 'string') {
|
|
console.warn(`Unprocessed binary value. ${unprocessed}`);
|
|
node.value = ascii85.encode(node.value);
|
|
}
|
|
if (node.type === VALUE_TYPES.REFERENCE && node.value instanceof PathReference) {
|
|
console.warn(`Unprocessed path reference. ${unprocessed}`);
|
|
node.value = node.value.path;
|
|
}
|
|
if ([VALUE_TYPES.OBJECT, VALUE_TYPES.ARRAY].includes(node.type)) {
|
|
const original = node.value;
|
|
node.value = {};
|
|
// If original is an array, it'll automatically be converted to an object now
|
|
Object.keys(original).forEach(key => {
|
|
node.value[key] = getTypedChildValue(original[key]);
|
|
});
|
|
}
|
|
|
|
return options.transaction.set(path, node);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {ICustomStorageNode} node
|
|
*/
|
|
_processReadNodeValue(node) {
|
|
|
|
const getTypedChildValue = val => {
|
|
// Typed value stored in parent record
|
|
if (val.type === VALUE_TYPES.BINARY) {
|
|
// binary stored in a parent record as a string
|
|
return ascii85.decode(val.value);
|
|
}
|
|
else if (val.type === VALUE_TYPES.DATETIME) {
|
|
// Date value stored as number
|
|
return new Date(val.value);
|
|
}
|
|
else if (val.type === VALUE_TYPES.REFERENCE) {
|
|
// Path reference stored as string
|
|
return new PathReference(val.value);
|
|
}
|
|
else {
|
|
throw new Error(`Unhandled child value type ${val.type}`);
|
|
}
|
|
}
|
|
|
|
switch (node.type) {
|
|
|
|
case VALUE_TYPES.ARRAY:
|
|
case VALUE_TYPES.OBJECT: {
|
|
// check if any value needs to be converted
|
|
// NOTE: Arrays are stored with numeric properties
|
|
const obj = node.value;
|
|
Object.keys(obj).forEach(key => {
|
|
let item = obj[key];
|
|
if (typeof item === 'object' && 'type' in item) {
|
|
obj[key] = getTypedChildValue(item);
|
|
}
|
|
});
|
|
node.value = obj;
|
|
break;
|
|
}
|
|
|
|
case VALUE_TYPES.BINARY: {
|
|
node.value = ascii85.decode(node.value);
|
|
break;
|
|
}
|
|
|
|
case VALUE_TYPES.REFERENCE: {
|
|
node.value = new PathReference(node.value);
|
|
break;
|
|
}
|
|
|
|
case VALUE_TYPES.STRING: {
|
|
// No action needed
|
|
// node.value = node.value;
|
|
break;
|
|
}
|
|
|
|
default:
|
|
throw new Error(`Invalid standalone record value type`); // should never happen
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} path
|
|
* @param {object} options
|
|
* @param {CustomStorageTransaction} options.transaction
|
|
* @returns {Promise<ICustomStorageNode>}
|
|
*/
|
|
async _readNode(path, options) {
|
|
// deserialize a stored value (always an object with "type", "value", "revision", "revision_nr", "created", "modified")
|
|
let node = await options.transaction.get(path);
|
|
if (node === null) { return null; }
|
|
if (typeof node !== 'object') {
|
|
throw new Error(`CustomStorageTransaction.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 = VALUE_TYPES.STRING;
|
|
}
|
|
else if (typeof val === 'number') {
|
|
type = VALUE_TYPES.NUMBER;
|
|
}
|
|
else if (typeof val === 'boolean') {
|
|
type = VALUE_TYPES.BOOLEAN;
|
|
}
|
|
else if (val instanceof Array) {
|
|
type = VALUE_TYPES.ARRAY;
|
|
}
|
|
else if (typeof val === 'object') {
|
|
if ('type' in val) {
|
|
type = val.type;
|
|
val = val.value;
|
|
if (type === VALUE_TYPES.DATETIME) {
|
|
val = new Date(val);
|
|
}
|
|
else if (type === VALUE_TYPES.REFERENCE) {
|
|
val = new PathReference(val);
|
|
}
|
|
}
|
|
else {
|
|
type = VALUE_TYPES.OBJECT;
|
|
}
|
|
}
|
|
else {
|
|
throw new Error(`Unknown value type`);
|
|
}
|
|
return { type, 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
|
|
* @param {string} path
|
|
* @param {any} value
|
|
* @param {object} options
|
|
* @param {CustomStorageTransaction} options.transaction
|
|
* @param {boolean} [options.merge=false]
|
|
* @param {string} [options.revision]
|
|
* @param {any} [options.currentValue]
|
|
* @returns {Promise<void>}
|
|
*/
|
|
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 === VALUE_TYPES.ARRAY && !(value instanceof Array) && typeof value === 'object' && Object.keys(value).some(key => isNaN(key))) {
|
|
throw new Error(`Cannot merge existing array of path "${path}" with an object`);
|
|
}
|
|
if (value instanceof Array && currentRow.type !== VALUE_TYPES.ARRAY) {
|
|
throw new Error(`Cannot merge existing object of path "${path}" with an array`);
|
|
}
|
|
}
|
|
|
|
const revision = options.revision || ID.generate();
|
|
let mainNode = {
|
|
type: currentRow && currentRow.type === VALUE_TYPES.ARRAY ? VALUE_TYPES.ARRAY : VALUE_TYPES.OBJECT,
|
|
value: {}
|
|
};
|
|
const childNodeValues = {};
|
|
if (value instanceof Array) {
|
|
mainNode.type = 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 PathReference) {
|
|
mainNode.type = VALUE_TYPES.REFERENCE;
|
|
mainNode.value = value.path;
|
|
}
|
|
else if (value instanceof ArrayBuffer) {
|
|
mainNode.type = VALUE_TYPES.BINARY;
|
|
mainNode.value = ascii85.encode(value);
|
|
}
|
|
else if (typeof value === 'string') {
|
|
mainNode.type = VALUE_TYPES.STRING;
|
|
mainNode.value = value;
|
|
}
|
|
|
|
const currentIsObjectOrArray = currentRow ? [VALUE_TYPES.OBJECT, VALUE_TYPES.ARRAY].includes(currentRow.type) : false;
|
|
const newIsObjectOrArray = [VALUE_TYPES.OBJECT, 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 === VALUE_TYPES.ARRAY;
|
|
if (currentRow) {
|
|
// update
|
|
this.debug.log(`Node "/${path}" is being ${options.merge ? 'updated' : 'overwritten'}`.colorize(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 = PathInfo.get(path);
|
|
const keys = [];
|
|
let checkExecuted = false;
|
|
const includeChildCheck = childPath => {
|
|
checkExecuted = true;
|
|
if (!transaction.production && !pathInfo.isParentOf(childPath)) {
|
|
// Double check failed
|
|
throw new Error(`"${childPath}" is not a child of "${path}" - childrenOf must only check and return paths that are children`);
|
|
}
|
|
return true;
|
|
}
|
|
const addChildPath = childPath => {
|
|
if (!checkExecuted) {
|
|
throw new Error(`${this._customImplementation.info} childrenOf did not call checkCallback before addCallback`);
|
|
}
|
|
const key = PathInfo.get(childPath).key;
|
|
keys.push(key.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 => {
|
|
if (isArray) { key = parseInt(key); }
|
|
const childDiff = typeof options.diff === 'object' ? options.diff.forChild(key) : undefined;
|
|
if (childDiff === 'identical') {
|
|
// console.warn(`Skipping _writeNode recursion for child "${key}"`);
|
|
return; // Skip
|
|
}
|
|
const childPath = pathInfo.childPath(key); // PathInfo.getChildPath(path, key);
|
|
const childValue = childNodeValues[key];
|
|
|
|
// Pass current child value to _writeNode
|
|
const currentChildValue = typeof options.currentValue === 'undefined' // Fixing issue #20
|
|
? undefined
|
|
: options.currentValue !== null && typeof options.currentValue === 'object' && key in options.currentValue
|
|
? options.currentValue[key]
|
|
: 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 => {
|
|
if (isArray) { key = parseInt(key); }
|
|
const childPath = pathInfo.childPath(key); //PathInfo.getChildPath(path, key);
|
|
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.debug.log(`Node "/${path}" is being created`.colorize(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 => {
|
|
if (isArray) { key = parseInt(key); }
|
|
const childPath = PathInfo.getChildPath(path, key);
|
|
const childValue = childNodeValues[key];
|
|
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
|
|
});
|
|
promises.push(p);
|
|
return Promise.all(promises);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Deletes (dedicated) node and all subnodes without checking for existence. Use with care - all removed nodes will lose their revision stats! DOES NOT REMOVE INLINE CHILD NODES!
|
|
* @param {string} path
|
|
* @param {object} options
|
|
* @param {CustomStorageTransaction} options.transaction
|
|
*/
|
|
async _deleteNode(path, options) {
|
|
const pathInfo = PathInfo.get(path);
|
|
this.debug.log(`Node "/${path}" is being deleted`.colorize(ColorStyle.cyan));
|
|
|
|
const deletePaths = [path];
|
|
let checkExecuted = false;
|
|
const includeDescendantCheck = (descPath) => {
|
|
checkExecuted = true;
|
|
if (!transaction.production && !pathInfo.isAncestorOf(descPath)) {
|
|
// Double check failed
|
|
throw new Error(`"${descPath}" is not a descendant of "${path}" - descendantsOf must only check and return paths that are descendants`);
|
|
}
|
|
return true;
|
|
};
|
|
const addDescendant = (descPath) => {
|
|
if (!checkExecuted) {
|
|
throw new Error(`${this._customImplementation.info} descendantsOf did not call checkCallback before addCallback`);
|
|
}
|
|
deletePaths.push(descPath);
|
|
return true;
|
|
};
|
|
const transaction = options.transaction;
|
|
await transaction.descendantsOf(path, { metadata: false, value: false }, includeDescendantCheck, addDescendant);
|
|
|
|
this.debug.log(`Nodes ${deletePaths.map(p => `"/${p}"`).join(',')} are being deleted`.colorize(ColorStyle.cyan));
|
|
return transaction.removeMultiple(deletePaths);
|
|
}
|
|
|
|
/**
|
|
* Enumerates all children of a given Node for reflection purposes
|
|
* @param {string} path
|
|
* @param {object} [options]
|
|
* @param {CustomStorageTransaction} [options.transaction]
|
|
* @param {string[]|number[]} [options.keyFilter]
|
|
*/
|
|
getChildren(path, options) {
|
|
// return generator
|
|
options = options || {};
|
|
var callback; //, resolve, reject;
|
|
const generator = {
|
|
/**
|
|
*
|
|
* @param {(child: NodeInfo) => boolean} valueCallback callback function to run for each child. Return false to stop iterating
|
|
* @returns {Promise<bool>} 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 () => {
|
|
let node = await this._readNode(path, { transaction });
|
|
if (!node) { throw new NodeNotFoundError(`Node "/${path}" does not exist`); }
|
|
|
|
if (![VALUE_TYPES.OBJECT, VALUE_TYPES.ARRAY].includes(node.type)) {
|
|
// No children
|
|
return;
|
|
}
|
|
const isArray = node.type === VALUE_TYPES.ARRAY;
|
|
const value = node.value;
|
|
let keys = Object.keys(value);
|
|
if (options.keyFilter) {
|
|
keys = keys.filter(key => options.keyFilter.includes(key));
|
|
}
|
|
const pathInfo = PathInfo.get(path);
|
|
keys.length > 0 && keys.every(key => {
|
|
let child = this._getTypeFromStoredValue(value[key]);
|
|
|
|
const info = new CustomStorageNodeInfo({
|
|
path: pathInfo.childPath(key),
|
|
key: isArray ? null : key,
|
|
index: isArray ? key : null,
|
|
type: child.type,
|
|
address: null,
|
|
exists: true,
|
|
value: child.value,
|
|
revision: node.revision,
|
|
revision_nr: node.revision_nr,
|
|
created: node.created,
|
|
modified: node.modified
|
|
});
|
|
|
|
canceled = callback(info) === false;
|
|
return !canceled; // stop .every loop if canceled
|
|
});
|
|
if (canceled) {
|
|
return;
|
|
}
|
|
|
|
// Go on... get other children
|
|
let checkExecuted = false;
|
|
const includeChildCheck = (childPath) => {
|
|
checkExecuted = true;
|
|
if (!transaction.production && !pathInfo.isParentOf(childPath)) {
|
|
// Double check failed
|
|
throw new Error(`"${childPath}" is not a child of "${path}" - childrenOf must only check and return paths that are children`);
|
|
}
|
|
if (options.keyFilter) {
|
|
const key = PathInfo.get(childPath).key;
|
|
return options.keyFilter.includes(key);
|
|
}
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
*
|
|
* @param {string} childPath
|
|
* @param {ICustomStorageNodeMetaData} node
|
|
*/
|
|
const addChildNode = (childPath, node) => {
|
|
if (!checkExecuted) {
|
|
throw new Error(`${this._customImplementation.info} childrenOf did not call checkCallback before addCallback`);
|
|
}
|
|
const key = PathInfo.get(childPath).key;
|
|
const info = new CustomStorageNodeInfo({
|
|
path: childPath,
|
|
type: node.type,
|
|
key: isArray ? null : key,
|
|
index: isArray ? key : null,
|
|
address: new CustomStorageNodeAddress(childPath),
|
|
exists: true,
|
|
value: null, // not loaded
|
|
revision: node.revision,
|
|
revision_nr: node.revision_nr,
|
|
created: new Date(node.created),
|
|
modified: new Date(node.modified)
|
|
});
|
|
|
|
canceled = callback(info) === false;
|
|
return !canceled;
|
|
};
|
|
await transaction.childrenOf(path, { metadata: true, value: false }, includeChildCheck, addChildNode);
|
|
})();
|
|
if (!options.transaction) {
|
|
// transaction was created by us, commit
|
|
await transaction.commit();
|
|
}
|
|
return canceled;
|
|
}
|
|
catch (err) {
|
|
if (!options.transaction) {
|
|
// transaction was created by us, rollback
|
|
await transaction.rollback(err);
|
|
}
|
|
throw err;
|
|
}
|
|
|
|
// })
|
|
// .then(() => {
|
|
// lock.release();
|
|
// return canceled;
|
|
// })
|
|
// .catch(err => {
|
|
// lock.release();
|
|
// throw err;
|
|
// });
|
|
}; // start()
|
|
return generator;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {string} path
|
|
* @param {object} [options]
|
|
* @param {string[]} [options.include]
|
|
* @param {string[]} [options.exclude]
|
|
* @param {boolean} [options.child_objects=true]
|
|
* @param {CustomStorageTransaction} [options.transaction]
|
|
* @returns {Promise<ICustomStorageNode>}
|
|
*/
|
|
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 });
|
|
// let lock;
|
|
// return this.nodeLocker.lock(path, tid, false, 'getNode')
|
|
// .then(async l => {
|
|
// lock = l;
|
|
try {
|
|
const node = await (async () => {
|
|
// Get path, path/* and path[*
|
|
const filtered = (options.include && options.include.length > 0) || (options.exclude && options.exclude.length > 0) || options.child_objects === false;
|
|
const pathInfo = PathInfo.get(path);
|
|
const targetNode = await this._readNode(path, { transaction });
|
|
if (!targetNode) {
|
|
// Lookup parent node
|
|
if (path === '') { return { value: null }; } // path is root. There is no parent.
|
|
const lockPath = await transaction.moveToParentPath(pathInfo.parentPath);
|
|
console.assert(lockPath === pathInfo.parentPath, `transaction.moveToParentPath() did not move to the right parent path of "${path}"`)
|
|
// return lock.moveToParent()
|
|
// .then(async parentLock => {
|
|
// lock = parentLock;
|
|
let parentNode = await this._readNode(pathInfo.parentPath, { transaction });
|
|
if (parentNode && [VALUE_TYPES.OBJECT, VALUE_TYPES.ARRAY].includes(parentNode.type) && pathInfo.key in parentNode.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 === VALUE_TYPES.ARRAY;
|
|
/**
|
|
* Convert include & exclude filters to PathInfo instances for easier handling
|
|
* @param {string[]} arr
|
|
* @returns {PathInfo[]}
|
|
*/
|
|
const convertFilterArray = (arr) => {
|
|
const isNumber = key => /^[0-9]+$/.test(key);
|
|
return arr.map(path => 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
|
|
*
|
|
* @param {string} descPath
|
|
* @param {ICustomStorageNode} node
|
|
*/
|
|
const applyFiltersOnInlineData = (descPath, node) => {
|
|
if ([VALUE_TYPES.OBJECT, VALUE_TYPES.ARRAY].includes(node.type) && includeFilter.length > 0) {
|
|
const trailKeys = PathInfo.getPathKeys(descPath).slice(pathInfo.keys.length);
|
|
const checkPathInfo = new PathInfo(trailKeys);
|
|
const remove = [];
|
|
const includes = includeFilter.filter(info => info.isDescendantOf(checkPathInfo));
|
|
if (includes.length > 0) {
|
|
const isArray = node.type === VALUE_TYPES.ARRAY;
|
|
remove.push(...Object.keys(node.value).map(key => isArray ? +key : key)); // Mark all at first
|
|
for (let 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 (let 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
|
|
throw new Error(`"${descPath}" is not a descendant of "${path}" - descendantsOf must only check and return paths that are descendants`);
|
|
}
|
|
if (!filtered) { return true; }
|
|
|
|
// Apply include & exclude filters
|
|
const descPathKeys = PathInfo.getPathKeys(descPath);
|
|
const trailKeys = descPathKeys.slice(pathInfo.keys.length);
|
|
const checkPathInfo = new 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) && [VALUE_TYPES.OBJECT, VALUE_TYPES.ARRAY].includes(metadata ? metadata.type : -1)
|
|
|| PathInfo.getPathKeys(descPath).length > pathInfo.pathKeys.length + 1)
|
|
) {
|
|
include = false;
|
|
}
|
|
return include;
|
|
}
|
|
|
|
const descRows = [];
|
|
/**
|
|
*
|
|
* @param {string} descPath
|
|
* @param {ICustomStorageNode} node
|
|
*/
|
|
const addDescendant = (descPath, node) => {
|
|
// console.warn(`Adding descendant "${descPath}"`);
|
|
if (!checkExecuted) {
|
|
throw new Error(`${this._customImplementation.info} descendantsOf did not call checkCallback before addCallback`);
|
|
}
|
|
if (options.child_objects === false && [VALUE_TYPES.OBJECT, 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
|
|
node.path = descPath;
|
|
descRows.push(node);
|
|
|
|
return true; // Keep streaming
|
|
};
|
|
|
|
await transaction.descendantsOf(path, { metadata: true, value: true }, includeDescendantCheck, addDescendant);
|
|
|
|
this.debug.log(`Read node "/${path}" and ${filtered ? '(filtered) ' : ''}descendants from ${descRows.length + 1} records`.colorize(ColorStyle.magenta));
|
|
|
|
const result = targetNode;
|
|
|
|
const objectToArray = obj => {
|
|
// Convert object value to array
|
|
const arr = [];
|
|
Object.keys(obj).forEach(key => {
|
|
let index = parseInt(key);
|
|
arr[index] = obj[index];
|
|
});
|
|
return arr;
|
|
};
|
|
|
|
if (targetNode.type === VALUE_TYPES.ARRAY) {
|
|
result.value = objectToArray(result.value);
|
|
}
|
|
|
|
if (targetNode.type === VALUE_TYPES.OBJECT || targetNode.type === VALUE_TYPES.ARRAY) {
|
|
// target node is an object or array
|
|
// merge with other found (child) nodes
|
|
const targetPathKeys = PathInfo.getPathKeys(path);
|
|
let value = targetNode.value;
|
|
for (let i = 0; i < descRows.length; i++) {
|
|
const otherNode = descRows[i];
|
|
const pathKeys = PathInfo.getPathKeys(otherNode.path);
|
|
const trailKeys = pathKeys.slice(targetPathKeys.length);
|
|
let parent = value;
|
|
for (let j = 0 ; j < trailKeys.length; j++) {
|
|
console.assert(typeof parent === 'object', 'parent must be an object/array to have children!!');
|
|
const key = trailKeys[j];
|
|
const isLast = j === trailKeys.length-1;
|
|
const nodeType = isLast
|
|
? otherNode.type
|
|
: typeof trailKeys[j+1] === 'number'
|
|
? VALUE_TYPES.ARRAY
|
|
: VALUE_TYPES.OBJECT;
|
|
let nodeValue;
|
|
if (!isLast) {
|
|
nodeValue = nodeType === VALUE_TYPES.OBJECT ? {} : [];
|
|
}
|
|
else {
|
|
nodeValue = otherNode.value;
|
|
if (nodeType === VALUE_TYPES.ARRAY) {
|
|
nodeValue = objectToArray(nodeValue);
|
|
}
|
|
}
|
|
if (key in parent) {
|
|
// Merge with parent
|
|
const mergePossible = typeof parent[key] === typeof nodeValue && [VALUE_TYPES.OBJECT, 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.debug.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]) {
|
|
throw new Error( `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) {
|
|
throw new Error(`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
|
|
console.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 = PathInfo.getPathKeys(path);
|
|
process(result.value, checkKeys);
|
|
});
|
|
}
|
|
|
|
return result;
|
|
})();
|
|
if (!options.transaction) {
|
|
// transaction was created by us, commit
|
|
await transaction.commit();
|
|
}
|
|
return node;
|
|
}
|
|
catch (err) {
|
|
if (!options.transaction) {
|
|
// transaction was created by us, rollback
|
|
await transaction.rollback(err);
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {string} path
|
|
* @param {object} [options]
|
|
* @param {CustomStorageTransaction} [options.transaction]
|
|
* @param {boolean} [options.include_child_count=false] whether to include child count if node is an object or array
|
|
* @returns {Promise<CustomStorageNodeInfo>}
|
|
*/
|
|
async getNodeInfo(path, options) {
|
|
options = options || {};
|
|
const pathInfo = 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 CustomStorageNodeAddress(path) : null,
|
|
created: node ? new Date(node.created) : null,
|
|
modified: node ? new Date(node.modified) : null,
|
|
revision: node ? node.revision : null,
|
|
revision_nr: node ? node.revision_nr : null
|
|
});
|
|
|
|
if (!node && path !== '') {
|
|
// Try parent node
|
|
|
|
const lockPath = await transaction.moveToParentPath(pathInfo.parentPath);
|
|
console.assert(lockPath === pathInfo.parentPath, `transaction.moveToParentPath() did not move to the right parent path of "${path}"`)
|
|
const parent = await this._readNode(pathInfo.parentPath, { transaction });
|
|
if (parent && [VALUE_TYPES.OBJECT, VALUE_TYPES.ARRAY].includes(parent.type) && pathInfo.key in parent.value) {
|
|
// Stored in parent node
|
|
info.exists = true;
|
|
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 ([VALUE_TYPES.ARRAY, VALUE_TYPES.OBJECT].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;
|
|
}
|
|
catch (err) {
|
|
if (!options.transaction) {
|
|
// transaction was created by us, rollback
|
|
await transaction.rollback(err);
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
// TODO: Move to Storage base class?
|
|
/**
|
|
*
|
|
* @param {string} path
|
|
* @param {any} value
|
|
* @param {object} [options]
|
|
* @param {string} [options.assert_revision]
|
|
* @param {CustomStorageTransaction} [options.transaction]
|
|
* @param {boolean} [options.suppress_events=false]
|
|
* @param {any} [options.context]
|
|
* @returns {Promise<CustomStorageNodeInfo>}
|
|
*/
|
|
async setNode(path, value, options = { suppress_events: false, context: null }) {
|
|
const pathInfo = 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 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);
|
|
console.assert(lockPath === pathInfo.parentPath, `transaction.moveToParentPath() did not move to the right parent path of "${path}"`)
|
|
await this._writeNodeWithTracking(pathInfo.parentPath, { [pathInfo.key]: value }, { merge: true, transaction, suppress_events: options.suppress_events, context: options.context });
|
|
}
|
|
}
|
|
else {
|
|
// Delegate operation to update on parent node
|
|
const lockPath = await transaction.moveToParentPath(pathInfo.parentPath);
|
|
console.assert(lockPath === pathInfo.parentPath, `transaction.moveToParentPath() did not move to the right parent path of "${path}"`)
|
|
await this.updateNode(pathInfo.parentPath, { [pathInfo.key]: value }, { transaction, suppress_events: options.suppress_events, context: options.context });
|
|
}
|
|
if (!options.transaction) {
|
|
// transaction was created by us, commit
|
|
await transaction.commit();
|
|
}
|
|
}
|
|
catch (err) {
|
|
if (!options.transaction) {
|
|
// transaction was created by us, rollback
|
|
await transaction.rollback(err);
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
// TODO: Move to Storage base class?
|
|
/**
|
|
*
|
|
* @param {string} path
|
|
* @param {*} updates
|
|
* @param {object} [options]
|
|
* @param {CustomStorageTransaction} [options.transaction]
|
|
* @param {boolean} [options.suppress_events=false]
|
|
* @param {any} [options.context]
|
|
*/
|
|
async updateNode(path, updates, options = { suppress_events: false, context: null }) {
|
|
|
|
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!
|
|
}
|
|
|
|
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 = 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 = PathInfo.get(path);
|
|
const lockPath = await transaction.moveToParentPath(pathInfo.parentPath);
|
|
console.assert(lockPath === pathInfo.parentPath, `transaction.moveToParentPath() did not move to the right parent path of "${path}"`)
|
|
await this._writeNodeWithTracking(pathInfo.parentPath, { [pathInfo.key]: 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);
|
|
console.assert(lockPath === pathInfo.parentPath, `transaction.moveToParentPath() did not move to the right parent path of "${path}"`)
|
|
await this.updateNode(pathInfo.parentPath, { [pathInfo.key]: updates }, { transaction, suppress_events: options.suppress_events, context: options.context });
|
|
}
|
|
if (!options.transaction) {
|
|
// transaction was created by us, commit
|
|
await transaction.commit();
|
|
}
|
|
}
|
|
catch (err) {
|
|
if (!options.transaction) {
|
|
// transaction was created by us, rollback
|
|
await transaction.rollback(err);
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
module.exports = {
|
|
CustomStorageNodeAddress,
|
|
CustomStorageNodeInfo,
|
|
CustomStorage,
|
|
CustomStorageSettings,
|
|
CustomStorageHelpers,
|
|
CustomStorageTransaction,
|
|
ICustomStorageNodeMetaData,
|
|
ICustomStorageNode
|
|
}
|
|
},{"./node-info":242,"./node-lock":243,"./node-value-types":244,"./storage":252,"acebase-core":12}],251:[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, debug, 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) {
|
|
debug.log(`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 this.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, 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 (!existingIndex) {
|
|
indexes.push(index);
|
|
}
|
|
try {
|
|
await index.build();
|
|
}
|
|
catch (err) {
|
|
context.debug.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;
|
|
}
|
|
ipc.sendNotification({ type: 'index.created', fileName: index.fileName, path, key, options });
|
|
return index;
|
|
}
|
|
exports.createIndex = createIndex;
|
|
|
|
},{"../data-index":237,"../promise-fs":247,"acebase-core":12}],252:[function(require,module,exports){
|
|
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.Storage = exports.StorageSettings = exports.SchemaValidationError = 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 node_info_1 = require("../node-info");
|
|
const ipc_1 = require("../ipc");
|
|
const promise_fs_1 = require("../promise-fs");
|
|
// const { IPCTransactionManager } = require('./node-transaction');
|
|
const data_index_1 = require("../data-index"); // Indexing might not be available: the browser dist bundle doesn't include it because fs is not available: browserify --i ./src/data-index.js
|
|
const indexes_1 = require("./indexes");
|
|
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 SchemaValidationError extends Error {
|
|
constructor(reason) {
|
|
super(`Schema validation failed: ${reason}`);
|
|
this.reason = reason;
|
|
}
|
|
}
|
|
exports.SchemaValidationError = SchemaValidationError;
|
|
/**
|
|
* Storage Settings
|
|
*/
|
|
class StorageSettings {
|
|
constructor(settings = {}) {
|
|
this.maxInlineValueSize = typeof settings.maxInlineValueSize === 'number' ? settings.maxInlineValueSize : 50;
|
|
this.removeVoidProperties = settings.removeVoidProperties === true;
|
|
this.path = settings.path || '.';
|
|
if (this.path.endsWith('/')) {
|
|
this.path = this.path.slice(0, -1);
|
|
}
|
|
this.logLevel = settings.logLevel || 'log';
|
|
this.info = settings.info || 'realtime database';
|
|
this.type = settings.type || 'data';
|
|
this.ipc = settings.ipc;
|
|
this.lockTimeout = typeof settings.lockTimeout === 'number' ? settings.lockTimeout : 120;
|
|
this.transactions = typeof settings.transactions === 'object' ? settings.transactions : { log: false };
|
|
}
|
|
}
|
|
exports.StorageSettings = StorageSettings;
|
|
class Storage extends acebase_core_1.SimpleEventEmitter {
|
|
/**
|
|
* 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) {
|
|
super();
|
|
this.name = name;
|
|
this.settings = settings;
|
|
// private _validation = new Map<string, { validate?: (previous: any, value: any) => boolean, schema?: SchemaDefinition }>;
|
|
this._schemas = [];
|
|
this._indexes = [];
|
|
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 promise_fs_1.pfs === null || promise_fs_1.pfs === void 0 ? void 0 : promise_fs_1.pfs.hasFileSystem;
|
|
},
|
|
create: (path, key, options = {
|
|
rebuild: false,
|
|
}) => {
|
|
const context = { storage: this, debug: this.debug, indexes: this._indexes, ipc: this.ipc };
|
|
return (0, indexes_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 (!promise_fs_1.pfs.hasFileSystem) {
|
|
// If pfs (fs) is not available, don't try using it
|
|
return;
|
|
}
|
|
let files = [];
|
|
try {
|
|
files = (await promise_fs_1.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.debug.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) => {
|
|
try {
|
|
const index = await data_index_1.DataIndex.readFromFile(this, fileName);
|
|
this._indexes.push(index);
|
|
return index;
|
|
}
|
|
catch (err) {
|
|
this.debug.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.debug.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)) {
|
|
// storage.debug.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.debug = new acebase_core_1.DebugLogger(settings.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}` : '');
|
|
if (settings.ipc) {
|
|
if (typeof settings.ipc.port !== 'number') {
|
|
throw new Error('IPC port number must be a number');
|
|
}
|
|
if (!['master', 'worker'].includes(settings.ipc.role)) {
|
|
throw new Error(`IPC client role must be either "master" or "worker", not "${settings.ipc.role}"`);
|
|
}
|
|
const ipcSettings = Object.assign({ dbname: ipcName }, settings.ipc);
|
|
this.ipc = new ipc_1.RemoteIPCPeer(this, ipcSettings);
|
|
}
|
|
else {
|
|
this.ipc = new ipc_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: (path, tid, write, comment) => {
|
|
return this.ipc.lock({ path, tid, write, comment });
|
|
},
|
|
};
|
|
// this.transactionManager = new IPCTransactionManager(this.ipc);
|
|
this._lastTid = 0;
|
|
} // end of constructor
|
|
createTid() {
|
|
return DEBUG_MODE ? ++this._lastTid : acebase_core_1.ID.generate();
|
|
}
|
|
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();
|
|
}
|
|
get path() {
|
|
return `${this.settings.path}/${this.name}.acebase`;
|
|
}
|
|
/**
|
|
* 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 (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 (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?');
|
|
}
|
|
}
|
|
/**
|
|
* 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');
|
|
}
|
|
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
|
|
}
|
|
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 };
|
|
}
|
|
/**
|
|
* Wrapper for _writeNode, handles triggering change events, index updating.
|
|
* @param {string} path
|
|
* @param {any} value
|
|
* @param {object} [options]
|
|
* @returns {Promise<IWriteNodeResult>} 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 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();
|
|
}
|
|
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) {
|
|
/** @type {string|number} trailKeys.shift() as string|number */
|
|
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();
|
|
}
|
|
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}`);
|
|
}
|
|
}
|
|
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.debug.warn('WARNING: One or more value event listeners on the root node are causing the entire database value to be read to facilitate change tracking. Using "value", "notify_value", "child_changed" and "notify_child_changed" events on the root node are a bad practice because of the significant performance impact. 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;
|
|
}
|
|
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' ? [] : {};
|
|
}
|
|
else {
|
|
newTopEventData = topEventData instanceof Array ? [] : {};
|
|
Object.keys(topEventData).forEach(key => {
|
|
newTopEventData[key] = topEventData[key];
|
|
});
|
|
}
|
|
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];
|
|
}
|
|
}
|
|
if (options.merge) {
|
|
// Update target value with updates
|
|
Object.keys(value).forEach(key => {
|
|
modifiedData[key] = value[key];
|
|
});
|
|
}
|
|
// console.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 (a.keys.length > b.keys.length) {
|
|
return -1;
|
|
}
|
|
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) {
|
|
console.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', 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) {
|
|
console.assert(pathKeys.length === indexPathKeys.length, 'check logic');
|
|
return [{ path, oldValue, newValue }];
|
|
}
|
|
let results = [];
|
|
let trailPath = '';
|
|
while (trailKeys.length > 0) {
|
|
/** @type {string|number} trailKeys.shift() as string|number */
|
|
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', 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;
|
|
}
|
|
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;
|
|
}
|
|
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);
|
|
console.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), '');
|
|
trigger && 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 });
|
|
// }
|
|
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;
|
|
};
|
|
// 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.debug.verbose(`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
|
|
let currentPath = topEventPath;
|
|
const trailPath = sub.eventPath.slice(currentPath.length).replace(/^\//, '');
|
|
const trailKeys = acebase_core_1.PathInfo.getPathKeys(trailPath);
|
|
let oldValue = topEventData, newValue = newTopEventData;
|
|
while (trailKeys.length > 0) {
|
|
const subKey = trailKeys.shift();
|
|
currentPath = acebase_core_1.PathInfo.getChildPath(currentPath, subKey);
|
|
const childValues = getChildValues(subKey, oldValue, newValue);
|
|
oldValue = childValues.oldValue;
|
|
newValue = childValues.newValue;
|
|
}
|
|
const batch = prepareMutationEvents(currentPath, oldValue, newValue);
|
|
if (batch.length === 0) {
|
|
return;
|
|
}
|
|
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 values = isNotifyEvent ? null : batch.map(m => ({ target: acebase_core_1.PathInfo.getPathKeys(m.path.slice(sub.subscriptionPath.length)), prev: m.oldValue, val: m.newValue }));
|
|
this.subscriptions.trigger(sub.type, sub.subscriptionPath, sub.subscriptionPath, null, values, options.context);
|
|
}
|
|
});
|
|
};
|
|
// 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
|
|
*/
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
getChildren(path, options) {
|
|
throw new Error('This method must be implemented by subclass');
|
|
}
|
|
/**
|
|
* @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;
|
|
}
|
|
/**
|
|
* 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');
|
|
}
|
|
/**
|
|
* 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');
|
|
}
|
|
/**
|
|
* 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');
|
|
}
|
|
/**
|
|
* 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);
|
|
}
|
|
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.debug.error(`Error in transaction callback: ${err.message}`);
|
|
});
|
|
}
|
|
}
|
|
catch (err) {
|
|
this.debug.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_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_1.NodeRevisionError) {
|
|
// try again
|
|
console.warn(`node value changed, running again. Error: ${err.message}`);
|
|
return this.transactNode(path, callback, options);
|
|
}
|
|
else {
|
|
throw err;
|
|
}
|
|
}
|
|
finally {
|
|
lock.release();
|
|
}
|
|
}
|
|
/**
|
|
* 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 (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 => {
|
|
unseenKeys.includes(childInfo.key) && unseenKeys.splice(unseenKeys.indexOf(childInfo.key), 1);
|
|
const keyCriteria = criteria
|
|
.filter(cr => cr.key === childInfo.key)
|
|
.map(cr => ({ op: cr.op, compare: cr.compare }));
|
|
const keyResult = keyCriteria.length > 0 ? checkChild(childInfo, keyCriteria) : { isMatch: true, promises: [] };
|
|
isMatch = keyResult.isMatch;
|
|
if (isMatch) {
|
|
delayedMatchPromises.push(...keyResult.promises);
|
|
const childCriteria = criteria
|
|
.filter(cr => cr.key.startsWith(`${childInfo.key}/`))
|
|
.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(key => {
|
|
const childInfo = new node_info_1.NodeInfo({ key, exists: false });
|
|
const childCriteria = criteria
|
|
.filter(cr => cr.key.startsWith(`${key}/`))
|
|
.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 === key)
|
|
.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;
|
|
}
|
|
catch (err) {
|
|
this.debug.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;
|
|
}
|
|
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_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_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_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_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_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;
|
|
}); // fs.every
|
|
return { isMatch, promises };
|
|
}; // checkChild
|
|
return checkNode(path, criteria);
|
|
}
|
|
test(val, op, compare) {
|
|
if (op === '<') {
|
|
return val < compare;
|
|
}
|
|
if (op === '<=') {
|
|
return val <= compare;
|
|
}
|
|
if (op === '==') {
|
|
return val === compare;
|
|
}
|
|
if (op === '!=') {
|
|
return val !== compare;
|
|
}
|
|
if (op === '>') {
|
|
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 val < compare[0] || val > compare[1];
|
|
}
|
|
if (op === 'has' || op === '!has') {
|
|
const has = typeof val === 'object' && compare in val;
|
|
return op === 'has' ? has : !has;
|
|
}
|
|
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;
|
|
}
|
|
/**
|
|
* 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, '\\"').replace(/\n/g, '\\n');
|
|
if (type === node_value_types_1.VALUE_TYPES.DATETIME) {
|
|
val = `"${val.toISOString()}"`;
|
|
if (options.type_safe) {
|
|
val = `{".type":"date",".val":${val}}`; // Previously: "Date"
|
|
}
|
|
}
|
|
else if (type === node_value_types_1.VALUE_TYPES.STRING) {
|
|
val = `"${escape(val)}"`;
|
|
}
|
|
else if (type === node_value_types_1.VALUE_TYPES.ARRAY) {
|
|
val = '[]';
|
|
}
|
|
else if (type === node_value_types_1.VALUE_TYPES.OBJECT) {
|
|
val = '{}';
|
|
}
|
|
else if (type === node_value_types_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_1.VALUE_TYPES.REFERENCE) {
|
|
val = `"${val.path}"`;
|
|
if (options.type_safe) {
|
|
val = `{".type":"reference",".val":${val}}`; // Previously: "PathReference"
|
|
}
|
|
}
|
|
else if (type === node_value_types_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');
|
|
}
|
|
else if (nodeInfo.type === node_value_types_1.VALUE_TYPES.OBJECT) {
|
|
objStart = '{';
|
|
objEnd = '}';
|
|
}
|
|
else if (nodeInfo.type === node_value_types_1.VALUE_TYPES.ARRAY) {
|
|
objStart = '[';
|
|
objEnd = ']';
|
|
}
|
|
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 (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 {
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* 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
|
|
*/
|
|
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);
|
|
}
|
|
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();
|
|
}
|
|
if (spaces.includes(state.data[state.index])) {
|
|
state.index++;
|
|
}
|
|
else {
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
/**
|
|
* 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}`);
|
|
}
|
|
}
|
|
};
|
|
/**
|
|
* 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);
|
|
};
|
|
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 {
|
|
// 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}"`);
|
|
}
|
|
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) {
|
|
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);
|
|
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;
|
|
});
|
|
}
|
|
}
|
|
/**
|
|
* 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;
|
|
}
|
|
/**
|
|
* Gets all currently active schema definitions
|
|
*/
|
|
getSchemas() {
|
|
return this._schemas.map(item => ({ path: item.path, schema: item.schema.source, text: item.schema.text }));
|
|
}
|
|
/**
|
|
* 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);
|
|
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;
|
|
|
|
},{"../data-index":237,"../ipc":239,"../node-errors":241,"../node-info":242,"../node-value-types":244,"../promise-fs":247,"./indexes":253,"acebase-core":12}],253:[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":251}],254:[function(require,module,exports){
|
|
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.ThreadSafe2 = exports.ThreadSafeLock2 = exports.ThreadSafe = void 0;
|
|
const acebase_core_1 = require("acebase-core");
|
|
/** Set to true to add stack traces to achieved locks (performance impact!) */
|
|
const DEBUG_MODE = false;
|
|
const _lockTimeoutMsg = 'Lock "${name}" timed out! lock.release() was not called in a timely fashion';
|
|
const _lockWaitTimeoutMsg = 'Lock "${name}" wait time expired, failed to lock target';
|
|
const _threadSafeLocks = new Map();
|
|
class ThreadSafe {
|
|
/**
|
|
*
|
|
* @param target Target object to lock. Do not use object references!
|
|
* @param options Locking options
|
|
* @returns returns a lock
|
|
*/
|
|
static lock(target, options = { timeout: 60000 * 15, critical: true, name: 'unnamed lock', shared: false }) {
|
|
if (typeof options !== 'object') {
|
|
options = {};
|
|
}
|
|
if (typeof options.timeout !== 'number') {
|
|
options.timeout = 60 * 1000;
|
|
}
|
|
if (typeof options.critical !== 'boolean') {
|
|
options.critical = true;
|
|
}
|
|
if (typeof options.name !== 'string') {
|
|
options.name = typeof target === 'string' ? target : 'unnamed lock';
|
|
}
|
|
if (typeof options.shared !== 'boolean') {
|
|
options.shared = false;
|
|
}
|
|
if (options.shared) {
|
|
// TODO: Implement
|
|
// console.warn('shared locking not implemented yet, using exclusive lock');
|
|
}
|
|
let lock = _threadSafeLocks.get(target);
|
|
const timeoutHandler = (critical) => {
|
|
console.error(_lockTimeoutMsg.replace('${name}', lock.name));
|
|
// Copy lock object so we can alter the original's release method to throw an exception
|
|
const copy = Object.assign({}, lock);
|
|
const originalName = lock.name;
|
|
lock.release = () => {
|
|
throw new Error(`Cannot release lock "${originalName}" because it timed out earlier`);
|
|
};
|
|
lock = copy;
|
|
if (critical) {
|
|
// cancel any queued requests
|
|
_threadSafeLocks.delete(target);
|
|
lock._queue.forEach(item => {
|
|
clearTimeout(item.waitTimeout);
|
|
item.reject(new Error(`Could not achieve lock because the current lock ("${lock.name}") was not released in time (and lock is flagged critical)`));
|
|
});
|
|
}
|
|
else {
|
|
next();
|
|
}
|
|
};
|
|
const next = () => {
|
|
clearTimeout(lock._timeout);
|
|
if (lock._queue.length === 0) {
|
|
return _threadSafeLocks.delete(target);
|
|
}
|
|
const item = lock._queue.shift();
|
|
clearTimeout(item.waitTimeout);
|
|
lock._timeout = setTimeout(timeoutHandler, item.options.timeout, item.options.critical);
|
|
lock.target = item.options.target || target;
|
|
lock.achieved = new Date();
|
|
lock.name = item.options.name;
|
|
lock.stack = DEBUG_MODE ? (new Error()).stack : 'not available';
|
|
item.resolve(lock);
|
|
};
|
|
if (!lock) {
|
|
// Create lock
|
|
lock = {
|
|
target: options.target || target,
|
|
achieved: new Date(),
|
|
release() {
|
|
next();
|
|
},
|
|
name: options.name,
|
|
stack: DEBUG_MODE ? (new Error()).stack : 'not available',
|
|
_timeout: setTimeout(timeoutHandler, options.timeout, options.critical),
|
|
_queue: [],
|
|
};
|
|
_threadSafeLocks.set(target, lock);
|
|
return Promise.resolve(lock);
|
|
}
|
|
else {
|
|
// Add to queue
|
|
return new Promise((resolve, reject) => {
|
|
const waitTimeout = setTimeout(() => {
|
|
lock._queue.splice(lock._queue.indexOf(item), 1);
|
|
if (lock._queue.length === 0) {
|
|
_threadSafeLocks.delete(target);
|
|
}
|
|
reject(_lockWaitTimeoutMsg.replace('${name}', options.name));
|
|
}, options.timeout);
|
|
const item = { resolve, reject, waitTimeout, options };
|
|
lock._queue.push(item);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
exports.ThreadSafe = ThreadSafe;
|
|
/**
|
|
* New locking mechasnism that supports exclusive or shared locking
|
|
*/
|
|
class ThreadSafeLock2 extends acebase_core_1.SimpleEventEmitter {
|
|
constructor(target, shared) {
|
|
super();
|
|
this.target = target;
|
|
this.shares = 0;
|
|
this.queue = [];
|
|
this._shared = shared;
|
|
this.achieved = new Date();
|
|
}
|
|
get shared() { return this._shared; }
|
|
release() {
|
|
if (this.shared && this.shares > 0) {
|
|
this.shares--;
|
|
}
|
|
else if (this.queue.length > 0) {
|
|
const next = this.queue.shift();
|
|
this._shared = next.shared;
|
|
next.grant();
|
|
if (next.shared) {
|
|
// Also grant other pending shared requests
|
|
while (this.queue.length > 0 && this.queue[0].shared) {
|
|
this.queue.shift().grant();
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
// No more shares, no queue: this lock can be now be released entirely
|
|
this.emitOnce('released');
|
|
}
|
|
}
|
|
async request(shared) {
|
|
if (this.shared && shared) {
|
|
// Grant!
|
|
this.shares++;
|
|
}
|
|
else {
|
|
// Add to queue, wait until granted
|
|
let grant;
|
|
const promise = new Promise(resolve => { grant = resolve; });
|
|
this.queue.push({ shared, grant });
|
|
await promise;
|
|
}
|
|
}
|
|
}
|
|
exports.ThreadSafeLock2 = ThreadSafeLock2;
|
|
const locks2 = new Map();
|
|
class ThreadSafe2 {
|
|
/**
|
|
*
|
|
* @param target Target object to lock. Do not use object references!
|
|
* @param options Locking options
|
|
* @returns returns a lock
|
|
*/
|
|
static async lock(target, shared = false) {
|
|
const timeout = 60 * 1000;
|
|
if (!locks2.has(target)) {
|
|
// New lock
|
|
const lock = new ThreadSafeLock2(target, shared);
|
|
locks2.set(target, lock);
|
|
lock.once('released', () => {
|
|
locks2.delete(target);
|
|
});
|
|
return lock;
|
|
}
|
|
else {
|
|
// Existing lock
|
|
const lock = locks2.get(target);
|
|
await lock.request(shared);
|
|
return lock;
|
|
}
|
|
}
|
|
}
|
|
exports.ThreadSafe2 = ThreadSafe2;
|
|
|
|
},{"acebase-core":12}]},{},[213])(213)
|
|
});
|