UNPKG

deep-state-observer

Version:

Deep state observer is an state management library that will fire listeners only when specified object node (which also can be a wildcard) was changed.

1,179 lines (1,153 loc) 75.5 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.DeepStateObserver = factory()); })(this, (function () { 'use strict'; /*! ***************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } // forked from https://github.com/joonhocho/superwild const segments = []; function Match(pattern, match, wchar = "*") { if (pattern === wchar) { return true; } segments.length = 0; let starCount = 0; let minLength = 0; let maxLength = 0; let segStartIndex = 0; for (let i = 0, len = pattern.length; i < len; i += 1) { const char = pattern[i]; if (char === wchar) { starCount += 1; if (i > segStartIndex) { segments.push(pattern.substring(segStartIndex, i)); } segments.push(char); segStartIndex = i + 1; } } if (segStartIndex < pattern.length) { segments.push(pattern.substring(segStartIndex)); } if (starCount) { minLength = pattern.length - starCount; maxLength = Infinity; } else { maxLength = minLength = pattern.length; } if (segments.length === 0) { return pattern === match; } const length = match.length; if (length < minLength || length > maxLength) { return false; } let segLeftIndex = 0; let segRightIndex = segments.length - 1; let rightPos = match.length - 1; let rightIsStar = false; while (true) { const segment = segments[segRightIndex]; segRightIndex -= 1; if (segment === wchar) { rightIsStar = true; } else { const lastIndex = rightPos + 1 - segment.length; const index = match.lastIndexOf(segment, lastIndex); if (index === -1 || index > lastIndex) { return false; } if (rightIsStar) { rightPos = index - 1; rightIsStar = false; } else { if (index !== lastIndex) { return false; } rightPos -= segment.length; } } if (segLeftIndex > segRightIndex) { break; } } return true; } class WildcardObject { constructor(obj, delimiter, wildcard, is_match = undefined) { this.obj = obj; this.delimiter = delimiter; this.wildcard = wildcard; this.is_match = is_match; } shortMatch(first, second) { if (first === second) return true; if (first === this.wildcard) return true; if (this.is_match) return this.is_match(first, second); const index = first.indexOf(this.wildcard); if (index > -1) { const end = first.substr(index + 1); if (index === 0 || second.substring(0, index) === first.substring(0, index)) { const len = end.length; if (len > 0) { return second.substr(-len) === end; } return true; } } return false; } match(first, second) { if (this.is_match) return this.is_match(first, second); return (first === second || first === this.wildcard || second === this.wildcard || this.shortMatch(first, second) || Match(first, second, this.wildcard)); } handleArray(wildcard, currentArr, partIndex, path, result = {}) { let nextPartIndex = wildcard.indexOf(this.delimiter, partIndex); let end = false; if (nextPartIndex === -1) { end = true; nextPartIndex = wildcard.length; } const currentWildcardPath = wildcard.substring(partIndex, nextPartIndex); let index = 0; for (const item of currentArr) { const key = index.toString(); const currentPath = path === "" ? key : path + this.delimiter + index; if (currentWildcardPath === this.wildcard || currentWildcardPath === key || this.shortMatch(currentWildcardPath, key)) { end ? (result[currentPath] = item) : this.goFurther(wildcard, item, nextPartIndex + 1, currentPath, result); } index++; } return result; } handleObject(wildcardPath, currentObj, partIndex, path, result = {}) { let nextPartIndex = wildcardPath.indexOf(this.delimiter, partIndex); let end = false; if (nextPartIndex === -1) { end = true; nextPartIndex = wildcardPath.length; } const currentWildcardPath = wildcardPath.substring(partIndex, nextPartIndex); for (let key in currentObj) { key = key.toString(); const currentPath = path === "" ? key : path + this.delimiter + key; if (currentWildcardPath === this.wildcard || currentWildcardPath === key || this.shortMatch(currentWildcardPath, key)) { if (end) { result[currentPath] = currentObj[key]; } else { this.goFurther(wildcardPath, currentObj[key], nextPartIndex + 1, currentPath, result); } } } return result; } goFurther(path, currentObj, partIndex, currentPath, result = {}) { if (Array.isArray(currentObj)) { return this.handleArray(path, currentObj, partIndex, currentPath, result); } return this.handleObject(path, currentObj, partIndex, currentPath, result); } get(path) { return this.goFurther(path, this.obj, 0, ""); } } class ObjectPath { static get(path, obj, create = false) { if (!obj) return; let currObj = obj; for (const currentPath of path) { if (currentPath in currObj) { currObj = currObj[currentPath]; } else if (create) { currObj[currentPath] = Object.create({}); currObj = currObj[currentPath]; } else { return; } } return currObj; } static set(path, value, obj) { if (!obj) return; if (path.length === 0) { for (const key in obj) { delete obj[key]; } for (const key in value) { obj[key] = value[key]; } return; } const prePath = path.slice(); const lastPath = prePath.pop(); const get = ObjectPath.get(prePath, obj, true); if (typeof get === "object") { get[lastPath] = value; } return value; } } let wasm; let WASM_VECTOR_LEN = 0; let cachegetUint8Memory0 = null; function getUint8Memory0() { if (cachegetUint8Memory0 === null || cachegetUint8Memory0.buffer !== wasm.memory.buffer) { cachegetUint8Memory0 = new Uint8Array(wasm.memory.buffer); } return cachegetUint8Memory0; } let cachedTextEncoder = new TextEncoder("utf-8"); const encodeString = typeof cachedTextEncoder.encodeInto === "function" ? function (arg, view) { return cachedTextEncoder.encodeInto(arg, view); } : function (arg, view) { const buf = cachedTextEncoder.encode(arg); view.set(buf); return { read: arg.length, written: buf.length, }; }; function passStringToWasm0(arg, malloc, realloc) { if (realloc === undefined) { const buf = cachedTextEncoder.encode(arg); const ptr = malloc(buf.length); getUint8Memory0() .subarray(ptr, ptr + buf.length) .set(buf); WASM_VECTOR_LEN = buf.length; return ptr; } let len = arg.length; let ptr = malloc(len); const mem = getUint8Memory0(); let offset = 0; for (; offset < len; offset++) { const code = arg.charCodeAt(offset); if (code > 0x7f) break; mem[ptr + offset] = code; } if (offset !== len) { if (offset !== 0) { arg = arg.slice(offset); } ptr = realloc(ptr, len, (len = offset + arg.length * 3)); const view = getUint8Memory0().subarray(ptr + offset, ptr + len); const ret = encodeString(arg, view); offset += ret.written; } WASM_VECTOR_LEN = offset; return ptr; } /** * @param {string} pattern * @param {string} input * @returns {boolean} */ function is_match(pattern, input) { var ptr0 = passStringToWasm0(pattern, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); var len0 = WASM_VECTOR_LEN; var ptr1 = passStringToWasm0(input, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); var len1 = WASM_VECTOR_LEN; var ret = wasm.is_match(ptr0, len0, ptr1, len1); return ret !== 0; } async function load(module, imports) { if (typeof Response === "function" && module instanceof Response) { if (typeof WebAssembly.instantiateStreaming === "function") { try { return await WebAssembly.instantiateStreaming(module, imports); } catch (e) { if (module.headers.get("Content-Type") != "application/wasm") { console.warn( "`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e ); } else { throw e; } } } const bytes = await module.arrayBuffer(); return await WebAssembly.instantiate(bytes, imports); } else { const instance = await WebAssembly.instantiate(module, imports); if (instance instanceof WebAssembly.Instance) { return { instance, module }; } else { return instance; } } } async function init(input) { const imports = {}; if ( typeof input === "string" || (typeof Request === "function" && input instanceof Request) || (typeof URL === "function" && input instanceof URL) ) { input = fetch(input); } const { instance, module } = await load(await input, imports); wasm = instance.exports; init.__wbindgen_wasm_module = module; return wasm; } const defaultUpdateOptions = { only: [], source: "", debug: false, data: undefined, queue: false, force: false, }; function log(message, info) { console.debug(message, info); } function getDefaultOptions() { return { delimiter: `.`, debug: false, extraDebug: false, useMute: true, notRecursive: `;`, param: `:`, wildcard: `*`, experimentalMatch: false, queue: false, defaultBulkValue: true, useCache: false, useSplitCache: false, useIndicesCache: false, maxSimultaneousJobs: 1000, maxQueueRuns: 1000, log, Promise, }; } /** * Is object - helper function to determine if specified variable is an object * * @param {any} item * @returns {boolean} */ function isObject(item) { if (item && item.constructor) { return item.constructor.name === "Object"; } return typeof item === "object" && item !== null; } class DeepState { constructor(data = {}, options = {}) { this.jobsRunning = 0; this.updateQueue = []; this.subscribeQueue = []; this.listenersIgnoreCache = new WeakMap(); this.is_match = null; this.destroyed = false; this.queueRuns = 0; this.groupId = 0; this.namedGroups = []; this.numberGroups = []; this.traceId = 0; this.traceMap = new Map(); this.tracing = []; this.savedTrace = []; this.collection = null; this.collections = 0; this.cache = new Map(); this.splitCache = new Map(); this.indices = new Map(); this.indicesCount = new Map(); this.lastExecs = new WeakMap(); this.listeners = new Map(); this.waitingListeners = new Map(); this.options = Object.assign(Object.assign({}, getDefaultOptions()), options); this.data = data; this.id = 0; if (!this.options.useCache) { this.pathGet = ObjectPath.get; this.pathSet = ObjectPath.set; } else { this.pathGet = this.cacheGet; this.pathSet = this.cacheSet; } if (options.Promise) { this.resolved = options.Promise.resolve(); } else { this.resolved = Promise.resolve(); } this.muted = new Set(); this.mutedListeners = new Set(); this.scan = new WildcardObject(this.data, this.options.delimiter, this.options.wildcard); this.destroyed = false; } getDefaultListenerOptions() { return { bulk: false, bulkValue: this.options.defaultBulkValue, debug: false, source: "", data: undefined, queue: false, group: false, }; } cacheGet(pathChunks, data = this.data, create = false) { const path = pathChunks.join(this.options.delimiter); const weakRefValue = this.cache.get(path); if (weakRefValue) { const value = weakRefValue.deref(); if (value) { return value; } } const value = ObjectPath.get(pathChunks, data, create); if (isObject(value) || Array.isArray(value)) { // @ts-ignore-next-line this.cache.set(path, new WeakRef(value)); } return value; } cacheSet(pathChunks, value, data = this.data) { const path = pathChunks.join(this.options.delimiter); if (isObject(value) || Array.isArray(value)) { this.cache.set(path, //@ts-ignore-next-line new WeakRef(value)); } else { this.cache.delete(path); } return ObjectPath.set(pathChunks, value, data); } /** * Silently update data * @param path string * @param value any * @returns */ silentSet(path, value) { return this.pathSet(this.split(path), value, this.data); } loadWasmMatcher(pathToWasmFile) { return __awaiter(this, void 0, void 0, function* () { yield init(pathToWasmFile); this.is_match = is_match; this.scan = new WildcardObject(this.data, this.options.delimiter, this.options.wildcard, this.is_match); }); } same(newValue, oldValue) { return ((["number", "string", "undefined", "boolean"].includes(typeof newValue) || newValue === null) && oldValue === newValue); } getListeners() { return this.listeners; } destroy() { this.destroyed = true; this.data = undefined; this.listeners = new Map(); this.waitingListeners = new Map(); this.updateQueue = []; this.jobsRunning = 0; } match(first, second, nested = true) { if (this.is_match) return this.is_match(first, second); if (first === second) return true; if (first === this.options.wildcard || second === this.options.wildcard) return true; if (!nested && this.getIndicesCount(this.options.delimiter, first) < this.getIndicesCount(this.options.delimiter, second)) { // first < second because first is a listener path and may be longer but not shorter return false; } return this.scan.match(first, second); } getIndicesOf(searchStr, str) { if (this.options.useIndicesCache && this.indices.has(str)) return this.indices.get(str); const searchStrLen = searchStr.length; if (searchStrLen == 0) { return []; } let startIndex = 0, index, indices = []; while ((index = str.indexOf(searchStr, startIndex)) > -1) { indices.push(index); startIndex = index + searchStrLen; } if (this.options.useIndicesCache) this.indices.set(str, indices); return indices; } getIndicesCount(searchStr, str) { if (this.options.useIndicesCache && this.indicesCount.has(str)) return this.indicesCount.get(str); const searchStrLen = searchStr.length; if (searchStrLen == 0) { return 0; } let startIndex = 0, index, indices = 0; while ((index = str.indexOf(searchStr, startIndex)) > -1) { indices++; startIndex = index + searchStrLen; } if (this.options.useIndicesCache) this.indicesCount.set(str, indices); return indices; } cutPath(longer, shorter) { if (shorter === "") return ""; longer = this.cleanNotRecursivePath(longer); shorter = this.cleanNotRecursivePath(shorter); if (longer === shorter) return longer; const shorterPartsLen = this.getIndicesCount(this.options.delimiter, shorter); const longerParts = this.getIndicesOf(this.options.delimiter, longer); return longer.substring(0, longerParts[shorterPartsLen]); } trimPath(path) { path = this.cleanNotRecursivePath(path); if (path.charAt(0) === this.options.delimiter) { return path.substr(1); } return path; } split(path) { if (path === "") return []; if (!this.options.useSplitCache) { return path.split(this.options.delimiter); } const fromCache = this.splitCache.get(path); if (fromCache) { return fromCache.slice(); } const value = path.split(this.options.delimiter); this.splitCache.set(path, value.slice()); return value; } isWildcard(path) { return path.includes(this.options.wildcard) || this.hasParams(path); } isNotRecursive(path) { return path.endsWith(this.options.notRecursive); } cleanNotRecursivePath(path) { return this.isNotRecursive(path) ? path.substring(0, path.length - 1) : path; } hasParams(path) { return path.includes(this.options.param); } getParamsInfo(path) { let paramsInfo = { replaced: "", original: path, params: {} }; let partIndex = 0; let fullReplaced = []; for (const part of this.split(path)) { paramsInfo.params[partIndex] = { original: part, replaced: "", name: "", }; const reg = new RegExp(`\\${this.options.param}([^\\${this.options.delimiter}\\${this.options.param}]+)`, "g"); let param = reg.exec(part); if (param) { paramsInfo.params[partIndex].name = param[1]; } else { delete paramsInfo.params[partIndex]; fullReplaced.push(part); partIndex++; continue; } reg.lastIndex = 0; paramsInfo.params[partIndex].replaced = part.replace(reg, this.options.wildcard); fullReplaced.push(paramsInfo.params[partIndex].replaced); partIndex++; } paramsInfo.replaced = fullReplaced.join(this.options.delimiter); return paramsInfo; } getParams(paramsInfo, path) { if (!paramsInfo) { return undefined; } const split = this.split(path); const result = {}; for (const partIndex in paramsInfo.params) { const param = paramsInfo.params[partIndex]; result[param.name] = split[partIndex]; } return result; } waitForAll(userPaths, fn) { const paths = {}; for (let path of userPaths) { paths[path] = { dirty: false }; if (this.hasParams(path)) { paths[path].paramsInfo = this.getParamsInfo(path); } paths[path].isWildcard = this.isWildcard(path); paths[path].isRecursive = !this.isNotRecursive(path); } this.waitingListeners.set(userPaths, { fn, paths }); fn(paths); return function unsubscribe() { this.waitingListeners.delete(userPaths); }; } executeWaitingListeners(updatePath) { if (this.destroyed) return; for (const waitingListener of this.waitingListeners.values()) { const { fn, paths } = waitingListener; let dirty = 0; let all = 0; for (let path in paths) { const pathInfo = paths[path]; let match = false; if (pathInfo.isRecursive) updatePath = this.cutPath(updatePath, path); if (pathInfo.isWildcard && this.match(path, updatePath)) match = true; if (updatePath === path) match = true; if (match) { pathInfo.dirty = true; } if (pathInfo.dirty) { dirty++; } all++; } if (dirty === all) { fn(paths); } } } subscribeAll(userPaths, fn, options = this.getDefaultListenerOptions()) { if (this.destroyed) return () => { }; let unsubscribers = []; let index = 0; let groupId = null; if (typeof options.group === "boolean" && options.group) { this.groupId++; groupId = this.groupId; options.bulk = true; } else if (typeof options.group === "string") { options.bulk = true; groupId = options.group; } for (const userPath of userPaths) { unsubscribers.push(this.subscribe(userPath, fn, options, { all: userPaths, index, groupId, })); index++; } return function unsubscribe() { for (const unsubscribe of unsubscribers) { unsubscribe(); } }; } getCleanListenersCollection(values = {}) { return Object.assign({ listeners: new Map(), isRecursive: false, isWildcard: false, hasParams: false, match: undefined, paramsInfo: undefined, path: undefined, originalPath: undefined, count: 0 }, values); } getCleanListener(fn, options = this.getDefaultListenerOptions()) { return { fn, options: Object.assign(Object.assign({}, this.getDefaultListenerOptions()), options), groupId: null, }; } getListenerCollectionMatch(listenerPath, isRecursive, isWildcard) { listenerPath = this.cleanNotRecursivePath(listenerPath); const self = this; return function listenerCollectionMatch(path, debug = false) { let scopedListenerPath = listenerPath; if (isRecursive) { path = self.cutPath(path, listenerPath); } else { scopedListenerPath = self.cutPath(self.cleanNotRecursivePath(listenerPath), path); } if (debug) { console.log("[getListenerCollectionMatch]", { listenerPath, scopedListenerPath, path, isRecursive, isWildcard, }); } if (isWildcard && self.match(scopedListenerPath, path, isRecursive)) return true; return scopedListenerPath === path; }; } getListenersCollection(listenerPath, listener) { if (this.listeners.has(listenerPath)) { let listenersCollection = this.listeners.get(listenerPath); listenersCollection.listeners.set(++this.id, listener); listener.id = this.id; return listenersCollection; } const hasParams = this.hasParams(listenerPath); let paramsInfo; if (hasParams) { paramsInfo = this.getParamsInfo(listenerPath); } let collCfg = { isRecursive: !this.isNotRecursive(listenerPath), isWildcard: this.isWildcard(listenerPath), hasParams, paramsInfo, originalPath: listenerPath, path: hasParams ? paramsInfo.replaced : listenerPath, }; if (!collCfg.isRecursive) { collCfg.path = this.cleanNotRecursivePath(collCfg.path); } let listenersCollection = this.getCleanListenersCollection(Object.assign(Object.assign({}, collCfg), { match: this.getListenerCollectionMatch(collCfg.path, collCfg.isRecursive, collCfg.isWildcard) })); this.id++; listenersCollection.listeners.set(this.id, listener); listener.id = this.id; this.listeners.set(collCfg.originalPath, listenersCollection); return listenersCollection; } subscribe(listenerPath, fn, options = this.getDefaultListenerOptions(), subscribeAllOptions = { all: [listenerPath], index: 0, groupId: null, }) { if (this.destroyed) return () => { }; this.jobsRunning++; const type = "subscribe"; let listener = this.getCleanListener(fn, options); if (options.group) { options.bulk = true; if (typeof options.group === "string") { listener.groupId = options.group; } else if (subscribeAllOptions.groupId) { listener.groupId = subscribeAllOptions.groupId; } } this.listenersIgnoreCache.set(listener, { truthy: [], falsy: [] }); const listenersCollection = this.getListenersCollection(listenerPath, listener); if (options.debug) { console.log("[subscribe]", { listenerPath, options }); } listenersCollection.count++; let shouldFire = true; if (listener.groupId) { if (typeof listener.groupId === "string") { if (this.namedGroups.includes(listener.groupId)) { shouldFire = false; } else { this.namedGroups.push(listener.groupId); } } else if (typeof listener.groupId === "number") { if (this.numberGroups.includes(listener.groupId)) { shouldFire = false; } else { this.numberGroups.push(listener.groupId); } } } if (shouldFire) { const cleanPath = this.cleanNotRecursivePath(listenersCollection.path); const cleanPathChunks = this.split(cleanPath); if (!listenersCollection.isWildcard) { if (!this.isMuted(cleanPath) && !this.isMuted(fn)) { fn(this.pathGet(cleanPathChunks, this.data), { type, listener, listenersCollection, path: { listener: listenerPath, update: undefined, resolved: this.cleanNotRecursivePath(listenerPath), }, params: this.getParams(listenersCollection.paramsInfo, cleanPath), options, }); } } else { const paths = this.scan.get(cleanPath); if (options.bulk) { const bulkValue = []; for (const path in paths) { if (this.isMuted(path)) continue; bulkValue.push({ path, params: this.getParams(listenersCollection.paramsInfo, path), value: paths[path], }); } if (!this.isMuted(fn)) { fn(bulkValue, { type, listener, listenersCollection, path: { listener: listenerPath, update: undefined, resolved: undefined, }, options, params: undefined, }); } } else { for (const path in paths) { if (!this.isMuted(path) && !this.isMuted(fn)) { fn(paths[path], { type, listener, listenersCollection, path: { listener: listenerPath, update: undefined, resolved: this.cleanNotRecursivePath(path), }, params: this.getParams(listenersCollection.paramsInfo, path), options, }); } } } } } this.debugSubscribe(listener, listenersCollection, listenerPath); this.jobsRunning--; return this.unsubscribe(listenerPath, this.id); } unsubscribe(path, id) { const listeners = this.listeners; const listenersCollection = listeners.get(path); return function unsub() { listenersCollection.listeners.delete(id); listenersCollection.count--; if (listenersCollection.count === 0) { listeners.delete(path); } }; } runQueuedListeners() { if (this.destroyed) return; if (this.subscribeQueue.length === 0) return; if (this.jobsRunning === 0) { this.queueRuns = 0; const queue = [...this.subscribeQueue]; for (let i = 0, len = queue.length; i < len; i++) { queue[i](); } this.subscribeQueue.length = 0; } else { this.queueRuns++; if (this.queueRuns >= this.options.maxQueueRuns) { this.queueRuns = 0; throw new Error("Maximal number of queue runs exhausted."); } else { Promise.resolve() .then(() => this.runQueuedListeners()) .catch((e) => { throw e; }); } } } getQueueNotifyListeners(groupedListeners, queue = []) { for (const path in groupedListeners) { if (this.isMuted(path)) continue; let { single, bulk } = groupedListeners[path]; for (const singleListener of single) { let alreadyInQueue = false; let resolvedIdPath = singleListener.listener.id + ":" + singleListener.eventInfo.path.resolved; if (!singleListener.eventInfo.path.resolved) { resolvedIdPath = singleListener.listener.id + ":" + singleListener.eventInfo.path.listener; } for (const excludedListener of queue) { if (resolvedIdPath === excludedListener.resolvedIdPath) { alreadyInQueue = true; break; } } if (alreadyInQueue) { continue; } const time = this.debugTime(singleListener); if (!this.isMuted(singleListener.listener.fn)) { if (singleListener.listener.options.queue && this.jobsRunning) { this.subscribeQueue.push(() => { singleListener.listener.fn(singleListener.value ? singleListener.value() : undefined, singleListener.eventInfo); }); } else { let resolvedIdPath = singleListener.listener.id + ":" + singleListener.eventInfo.path.resolved; if (!singleListener.eventInfo.path.resolved) { resolvedIdPath = singleListener.listener.id + ":" + singleListener.eventInfo.path.listener; } queue.push({ id: singleListener.listener.id, resolvedPath: singleListener.eventInfo.path.resolved, resolvedIdPath, originalFn: singleListener.listener.fn, fn: () => { singleListener.listener.fn(singleListener.value ? singleListener.value() : undefined, singleListener.eventInfo); }, options: singleListener.listener.options, groupId: singleListener.listener.groupId, }); } } this.debugListener(time, singleListener); } for (const bulkListener of bulk) { let alreadyInQueue = false; for (const excludedListener of queue) { if (excludedListener.id === bulkListener.listener.id) { alreadyInQueue = true; break; } } if (alreadyInQueue) continue; const time = this.debugTime(bulkListener); const bulkValue = []; for (const bulk of bulkListener.value) { bulkValue.push(Object.assign(Object.assign({}, bulk), { value: bulk.value ? bulk.value() : undefined })); } if (!this.isMuted(bulkListener.listener.fn)) { if (bulkListener.listener.options.queue && this.jobsRunning) { this.subscribeQueue.push(() => { if (!this.jobsRunning) { bulkListener.listener.fn(bulkValue, bulkListener.eventInfo); return true; } return false; }); } else { let resolvedIdPath = bulkListener.listener.id + ":" + bulkListener.eventInfo.path.resolved; if (!bulkListener.eventInfo.path.resolved) { resolvedIdPath = bulkListener.listener.id + ":" + bulkListener.eventInfo.path.listener; } queue.push({ id: bulkListener.listener.id, resolvedPath: bulkListener.eventInfo.path.resolved, resolvedIdPath, originalFn: bulkListener.listener.fn, fn: () => { bulkListener.listener.fn(bulkValue, bulkListener.eventInfo); }, options: bulkListener.listener.options, groupId: bulkListener.listener.groupId, }); } } this.debugListener(time, bulkListener); } } Promise.resolve().then(() => this.runQueuedListeners()); return queue; } shouldIgnore(listener, updatePath) { if (!listener.options.ignore) return false; for (const ignorePath of listener.options.ignore) { if (updatePath.startsWith(ignorePath)) { return true; } if (this.is_match && this.is_match(ignorePath, updatePath)) { return true; } else { const cuttedUpdatePath = this.cutPath(updatePath, ignorePath); if (this.match(ignorePath, cuttedUpdatePath)) { return true; } } } return false; } getSubscribedListeners(updatePath, newValue, options, type = "update", originalPath = null) { options = Object.assign(Object.assign({}, defaultUpdateOptions), options); const listeners = {}; for (let [listenerPath, listenersCollection] of this.listeners) { if (listenersCollection.match(updatePath)) { listeners[listenerPath] = { single: [], bulk: [], bulkData: [] }; const params = listenersCollection.paramsInfo ? this.getParams(listenersCollection.paramsInfo, updatePath) : undefined; const cutPath = this.cutPath(updatePath, listenerPath); const traverse = listenersCollection.isRecursive || listenersCollection.isWildcard; const value = traverse ? () => this.get(cutPath) : () => newValue; const bulkValue = [{ value, path: updatePath, params }]; for (const listener of listenersCollection.listeners.values()) { if (this.shouldIgnore(listener, updatePath)) { if (listener.options.debug) { console.log(`[getSubscribedListeners] Listener was not fired because it was ignored.`, { listener, listenersCollection, }); } continue; } if (listener.options.bulk) { listeners[listenerPath].bulk.push({ listener, listenersCollection, eventInfo: { type, listener, path: { listener: listenerPath, update: originalPath ? originalPath : updatePath, resolved: undefined, }, params, options, }, value: bulkValue, }); } else { listeners[listenerPath].single.push({ listener, listenersCollection, eventInfo: { type, listener, path: { listener: listenerPath, update: originalPath ? originalPath : updatePath, resolved: this.cleanNotRecursivePath(updatePath), }, params, options, }, value, }); } } } else if (this.options.extraDebug) { // debug let showMatch = false; for (const listener of listenersCollection.listeners.values()) { if (listener.options.debug) { showMatch = true; console.log(`[getSubscribedListeners] Listener was not fired because there was no match.`, { listener, listenersCollection, updatePath, }); } } if (showMatch) { listenersCollection.match(updatePath, true); } } } return listeners; } notifySubscribedListeners(updatePath, newValue, options, type = "update", originalPath = null) { return this.getQueueNotifyListeners(this.getSubscribedListeners(updatePath, newValue, options, type, originalPath)); } useBulkValue(listenersCollection) { for (const [listenerId, listener] of listenersCollection.listeners) { if (listener.options.bulk && listener.options.bulkValue) return true; if (!listener.options.bulk) return true; } return false; } getNestedListeners(updatePath, newValue, options, type = "update", originalPath = null) { const listeners = {}; const restBelowValues = {}; for (let [listenerPath, listenersCollection] of this.listeners) { if (!listenersCollection.isRecursive) continue; // listenerPath may be longer and is shortened - because we want to get listeners underneath change const currentAbovePathCut = this.cutPath(listenerPath, updatePath); if (this.match(currentAbovePathCut, updatePath)) { listeners[listenerPath] = { single: [], bulk: [] }; // listener is listening below updated node const restBelowPathCut = this.trimPath(listenerPath.substr(currentAbovePathCut.length)); const useBulkValue = this.useBulkValue(listenersCollection); let wildcardNewValues; if (useBulkValue) { wildcardNewValues = restBelowValues[restBelowPathCut] ? restBelowValues[restBelowPathCut] // if those values are already calculated use it : new WildcardObject(newValue, this.options.delimiter, this.options.wildcard).get(restBelowPathCut); restBelowValues[restBelowPathCut] = wildcardNewValues; } const params = listenersCollection.paramsInfo ? this.getParams(listenersCollection.paramsInfo, updatePath) : undefined;