UNPKG

@gullerya/object-observer

Version:

object-observer utility provides simple means to (deeply) observe specified object/array changes; implemented via native Proxy; changes delivered in a synchronous way

578 lines (577 loc) 20.3 kB
var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var object_observer_exports = {}; __export(object_observer_exports, { ObjectObserver: () => ObjectObserver, Observable: () => Observable }); module.exports = __toCommonJS(object_observer_exports); const INSERT = "insert", UPDATE = "update", DELETE = "delete", REVERSE = "reverse", SHUFFLE = "shuffle", oMetaKey = Symbol.for("object-observer-meta-key-0"), validObservableOptionKeys = { async: 1 }, processObserveOptions = (options) => { if (!options || typeof options !== "object") { return null; } const result = {}; const invalidOptions = []; for (const [optName, optVal] of Object.entries(options)) { if (optName === "path") { if (typeof optVal !== "string" || optVal === "") { throw new Error('"path" option, if/when provided, MUST be a non-empty string'); } result[optName] = optVal; } else if (optName === "pathsOf") { if (options.path) { throw new Error('"pathsOf" option MAY NOT be specified together with "path" option'); } if (typeof optVal !== "string") { throw new Error('"pathsOf" option, if/when provided, MUST be a string (MAY be empty)'); } result[optName] = options.pathsOf.split(".").filter(Boolean); } else if (optName === "pathsFrom") { if (options.path || options.pathsOf) { throw new Error('"pathsFrom" option MAY NOT be specified together with "path"/"pathsOf" option/s'); } if (typeof optVal !== "string" || optVal === "") { throw new Error('"pathsFrom" option, if/when provided, MUST be a non-empty string'); } result[optName] = optVal; } else { invalidOptions.push(optName); } } if (invalidOptions.length) { throw new Error(`'${invalidOptions.join(", ")}' is/are not a valid observer option/s`); } return result; }, prepareObject = (source, oMeta, visited) => { const target = {}; target[oMetaKey] = oMeta; for (const key in source) { target[key] = getObservedOf(source[key], key, oMeta, visited); } return target; }, prepareArray = (source, oMeta, visited) => { let l = source.length; const target = new Array(l); target[oMetaKey] = oMeta; for (let i = 0; i < l; i++) { target[i] = getObservedOf(source[i], i, oMeta, visited); } return target; }, prepareTypedArray = (source, oMeta) => { source[oMetaKey] = oMeta; return source; }, filterChanges = (options, changes) => { if (options === null) { return changes; } let result = changes; if (options.path) { const oPath = options.path; result = changes.filter( (change) => change.path.join(".") === oPath ); } else if (options.pathsOf) { const oPathsOf = options.pathsOf; const oPathsOfStr = oPathsOf.join("."); result = changes.filter( (change) => (change.path.length === oPathsOf.length + 1 || change.path.length === oPathsOf.length && (change.type === REVERSE || change.type === SHUFFLE)) && change.path.join(".").startsWith(oPathsOfStr) ); } else if (options.pathsFrom) { const oPathsFrom = options.pathsFrom; result = changes.filter( (change) => change.path.join(".").startsWith(oPathsFrom) ); } return result; }, callObserverSafe = (listener, changes) => { try { listener(changes); } catch (e) { console.error(`failed to notify listener ${listener} with ${changes}`, e); } }, callObserversFromMT = function callObserversFromMT2() { const batches = this.batches; this.batches = []; for (const [listener, changes] of batches) { callObserverSafe(listener, changes); } }, callObservers = (oMeta, changes) => { let currentObservable = oMeta; let isAsync, observers, target, options, relevantChanges, i; const l = changes.length; do { isAsync = currentObservable.options.async; observers = currentObservable.observers; i = observers.length; while (i--) { [target, options] = observers[i]; relevantChanges = filterChanges(options, changes); if (relevantChanges.length) { if (isAsync) { if (currentObservable.batches.length === 0) { queueMicrotask(callObserversFromMT.bind(currentObservable)); } let rb; for (const b of currentObservable.batches) { if (b[0] === target) { rb = b; break; } } if (!rb) { rb = [target, []]; currentObservable.batches.push(rb); } Array.prototype.push.apply(rb[1], relevantChanges); } else { callObserverSafe(target, relevantChanges); } } } const parent = currentObservable.parent; if (parent) { for (let j = 0; j < l; j++) { const change = changes[j]; changes[j] = new Change( change.type, [currentObservable.ownKey, ...change.path], change.value, change.oldValue, change.object ); } currentObservable = parent; } else { currentObservable = null; } } while (currentObservable); }, getObservedOf = (item, key, parent, visited) => { if (visited !== void 0 && visited.has(item)) { return null; } else if (typeof item !== "object" || item === null) { return item; } else if (Array.isArray(item)) { return new ArrayOMeta({ target: item, ownKey: key, parent, visited }).proxy; } else if (ArrayBuffer.isView(item)) { return new TypedArrayOMeta({ target: item, ownKey: key, parent }).proxy; } else if (item instanceof Date) { return item; } else { return new ObjectOMeta({ target: item, ownKey: key, parent, visited }).proxy; } }, proxiedPop = function proxiedPop2() { const oMeta = this[oMetaKey], target = oMeta.target, poppedIndex = target.length - 1; let popResult = target.pop(); if (popResult && typeof popResult === "object") { const tmpObserved = popResult[oMetaKey]; if (tmpObserved) { popResult = tmpObserved.detach(); } } const changes = [new Change(DELETE, [poppedIndex], void 0, popResult, this)]; callObservers(oMeta, changes); return popResult; }, proxiedPush = function proxiedPush2() { const oMeta = this[oMetaKey], target = oMeta.target, l = arguments.length, pushContent = new Array(l), initialLength = target.length; for (let i = 0; i < l; i++) { pushContent[i] = getObservedOf(arguments[i], initialLength + i, oMeta); } const pushResult = Reflect.apply(target.push, target, pushContent); const changes = []; for (let i = initialLength, j = target.length; i < j; i++) { changes[i - initialLength] = new Change(INSERT, [i], target[i], void 0, this); } callObservers(oMeta, changes); return pushResult; }, proxiedShift = function proxiedShift2() { const oMeta = this[oMetaKey], target = oMeta.target; let shiftResult, i, l, item, tmpObserved; shiftResult = target.shift(); if (shiftResult && typeof shiftResult === "object") { tmpObserved = shiftResult[oMetaKey]; if (tmpObserved) { shiftResult = tmpObserved.detach(); } } for (i = 0, l = target.length; i < l; i++) { item = target[i]; if (item && typeof item === "object") { tmpObserved = item[oMetaKey]; if (tmpObserved) { tmpObserved.ownKey = i; } } } const changes = [new Change(DELETE, [0], void 0, shiftResult, this)]; callObservers(oMeta, changes); return shiftResult; }, proxiedUnshift = function proxiedUnshift2() { const oMeta = this[oMetaKey], target = oMeta.target, al = arguments.length, unshiftContent = new Array(al); for (let i = 0; i < al; i++) { unshiftContent[i] = getObservedOf(arguments[i], i, oMeta); } const unshiftResult = Reflect.apply(target.unshift, target, unshiftContent); for (let i = 0, l2 = target.length, item; i < l2; i++) { item = target[i]; if (item && typeof item === "object") { const tmpObserved = item[oMetaKey]; if (tmpObserved) { tmpObserved.ownKey = i; } } } const l = unshiftContent.length; const changes = new Array(l); for (let i = 0; i < l; i++) { changes[i] = new Change(INSERT, [i], target[i], void 0, this); } callObservers(oMeta, changes); return unshiftResult; }, proxiedReverse = function proxiedReverse2() { const oMeta = this[oMetaKey], target = oMeta.target; let i, l, item; target.reverse(); for (i = 0, l = target.length; i < l; i++) { item = target[i]; if (item && typeof item === "object") { const tmpObserved = item[oMetaKey]; if (tmpObserved) { tmpObserved.ownKey = i; } } } const changes = [new Change(REVERSE, [], void 0, void 0, this)]; callObservers(oMeta, changes); return this; }, proxiedSort = function proxiedSort2(comparator) { const oMeta = this[oMetaKey], target = oMeta.target; let i, l, item; target.sort(comparator); for (i = 0, l = target.length; i < l; i++) { item = target[i]; if (item && typeof item === "object") { const tmpObserved = item[oMetaKey]; if (tmpObserved) { tmpObserved.ownKey = i; } } } const changes = [new Change(SHUFFLE, [], void 0, void 0, this)]; callObservers(oMeta, changes); return this; }, proxiedFill = function proxiedFill2(filVal, start, end) { const oMeta = this[oMetaKey], target = oMeta.target, changes = [], tarLen = target.length, prev = target.slice(0); start = start === void 0 ? 0 : start < 0 ? Math.max(tarLen + start, 0) : Math.min(start, tarLen); end = end === void 0 ? tarLen : end < 0 ? Math.max(tarLen + end, 0) : Math.min(end, tarLen); if (start < tarLen && end > start) { target.fill(filVal, start, end); let tmpObserved; for (let i = start, item, tmpTarget; i < end; i++) { item = target[i]; target[i] = getObservedOf(item, i, oMeta); if (i in prev) { tmpTarget = prev[i]; if (tmpTarget && typeof tmpTarget === "object") { tmpObserved = tmpTarget[oMetaKey]; if (tmpObserved) { tmpTarget = tmpObserved.detach(); } } changes.push(new Change(UPDATE, [i], target[i], tmpTarget, this)); } else { changes.push(new Change(INSERT, [i], target[i], void 0, this)); } } callObservers(oMeta, changes); } return this; }, proxiedCopyWithin = function proxiedCopyWithin2(dest, start, end) { const oMeta = this[oMetaKey], target = oMeta.target, tarLen = target.length; dest = dest < 0 ? Math.max(tarLen + dest, 0) : dest; start = start === void 0 ? 0 : start < 0 ? Math.max(tarLen + start, 0) : Math.min(start, tarLen); end = end === void 0 ? tarLen : end < 0 ? Math.max(tarLen + end, 0) : Math.min(end, tarLen); const len = Math.min(end - start, tarLen - dest); if (dest < tarLen && dest !== start && len > 0) { const prev = target.slice(0), changes = []; target.copyWithin(dest, start, end); for (let i = dest, nItem, oItem, tmpObserved; i < dest + len; i++) { nItem = target[i]; if (nItem && typeof nItem === "object") { nItem = getObservedOf(nItem, i, oMeta); target[i] = nItem; } oItem = prev[i]; if (oItem && typeof oItem === "object") { tmpObserved = oItem[oMetaKey]; if (tmpObserved) { oItem = tmpObserved.detach(); } } if (typeof nItem !== "object" && nItem === oItem) { continue; } changes.push(new Change(UPDATE, [i], nItem, oItem, this)); } callObservers(oMeta, changes); } return this; }, proxiedSplice = function proxiedSplice2() { const oMeta = this[oMetaKey], target = oMeta.target, splLen = arguments.length, spliceContent = new Array(splLen), tarLen = target.length; for (let i2 = 0; i2 < splLen; i2++) { spliceContent[i2] = getObservedOf(arguments[i2], i2, oMeta); } const startIndex = splLen === 0 ? 0 : spliceContent[0] < 0 ? tarLen + spliceContent[0] : spliceContent[0], removed = splLen < 2 ? tarLen - startIndex : spliceContent[1], inserted = Math.max(splLen - 2, 0), spliceResult = Reflect.apply(target.splice, target, spliceContent), newTarLen = target.length; let tmpObserved; for (let i2 = 0, item2; i2 < newTarLen; i2++) { item2 = target[i2]; if (item2 && typeof item2 === "object") { tmpObserved = item2[oMetaKey]; if (tmpObserved) { tmpObserved.ownKey = i2; } } } let i, l, item; for (i = 0, l = spliceResult.length; i < l; i++) { item = spliceResult[i]; if (item && typeof item === "object") { tmpObserved = item[oMetaKey]; if (tmpObserved) { spliceResult[i] = tmpObserved.detach(); } } } const changes = []; let index; for (index = 0; index < removed; index++) { if (index < inserted) { changes.push(new Change(UPDATE, [startIndex + index], target[startIndex + index], spliceResult[index], this)); } else { changes.push(new Change(DELETE, [startIndex + index], void 0, spliceResult[index], this)); } } for (; index < inserted; index++) { changes.push(new Change(INSERT, [startIndex + index], target[startIndex + index], void 0, this)); } callObservers(oMeta, changes); return spliceResult; }, proxiedTypedArraySet = function proxiedTypedArraySet2(source, offset) { const oMeta = this[oMetaKey], target = oMeta.target, souLen = source.length, prev = target.slice(0); offset = offset || 0; target.set(source, offset); const changes = new Array(souLen); for (let i = offset; i < souLen + offset; i++) { changes[i - offset] = new Change(UPDATE, [i], target[i], prev[i], this); } callObservers(oMeta, changes); }, proxiedArrayMethods = { pop: proxiedPop, push: proxiedPush, shift: proxiedShift, unshift: proxiedUnshift, reverse: proxiedReverse, sort: proxiedSort, fill: proxiedFill, copyWithin: proxiedCopyWithin, splice: proxiedSplice }, proxiedTypedArrayMethods = { reverse: proxiedReverse, sort: proxiedSort, fill: proxiedFill, copyWithin: proxiedCopyWithin, set: proxiedTypedArraySet }; class Change { constructor(type, path, value, oldValue, object) { this.type = type; this.path = path; this.value = value; this.oldValue = oldValue; this.object = object; } } class OMetaBase { constructor(properties, cloningFunction) { const { target, parent, ownKey, visited = /* @__PURE__ */ new Set() } = properties; if (parent && ownKey !== void 0) { this.parent = parent; this.ownKey = ownKey; } else { this.parent = null; this.ownKey = null; } visited.add(target); const targetClone = cloningFunction(target, this, visited); visited.delete(target); this.observers = []; this.revocable = Proxy.revocable(targetClone, this); this.proxy = this.revocable.proxy; this.target = targetClone; this.options = this.processOptions(properties.options); if (this.options.async) { this.batches = []; } } processOptions(options) { if (options) { if (typeof options !== "object") { throw new Error(`Observable options if/when provided, MAY only be an object, got '${options}'`); } const invalidOptions = Object.keys(options).filter((option) => !(option in validObservableOptionKeys)); if (invalidOptions.length) { throw new Error(`'${invalidOptions.join(", ")}' is/are not a valid Observable option/s`); } return Object.assign({}, options); } else { return {}; } } detach() { this.parent = null; return this.target; } set(target, key, value) { let oldValue = target[key]; if (value !== oldValue) { const newValue = getObservedOf(value, key, this); target[key] = newValue; if (oldValue && typeof oldValue === "object") { const tmpObserved = oldValue[oMetaKey]; if (tmpObserved) { oldValue = tmpObserved.detach(); } } const changes = oldValue === void 0 ? [new Change(INSERT, [key], newValue, void 0, this.proxy)] : [new Change(UPDATE, [key], newValue, oldValue, this.proxy)]; callObservers(this, changes); } return true; } deleteProperty(target, key) { let oldValue = target[key]; delete target[key]; if (oldValue && typeof oldValue === "object") { const tmpObserved = oldValue[oMetaKey]; if (tmpObserved) { oldValue = tmpObserved.detach(); } } const changes = [new Change(DELETE, [key], void 0, oldValue, this.proxy)]; callObservers(this, changes); return true; } } class ObjectOMeta extends OMetaBase { constructor(properties) { super(properties, prepareObject); } } class ArrayOMeta extends OMetaBase { constructor(properties) { super(properties, prepareArray); } get(target, key) { return proxiedArrayMethods[key] || target[key]; } } class TypedArrayOMeta extends OMetaBase { constructor(properties) { super(properties, prepareTypedArray); } get(target, key) { return proxiedTypedArrayMethods[key] || target[key]; } } const Observable = Object.freeze({ from: (target, options) => { if (!target || typeof target !== "object") { throw new Error("observable MAY ONLY be created from a non-null object"); } else if (target[oMetaKey]) { return target; } else if (Array.isArray(target)) { return new ArrayOMeta({ target, ownKey: null, parent: null, options }).proxy; } else if (ArrayBuffer.isView(target)) { return new TypedArrayOMeta({ target, ownKey: null, parent: null, options }).proxy; } else if (target instanceof Date) { throw new Error(`${target} found to be one of a non-observable types`); } else { return new ObjectOMeta({ target, ownKey: null, parent: null, options }).proxy; } }, isObservable: (input) => { return !!(input && input[oMetaKey]); }, observe: (observable, observer, options) => { if (!Observable.isObservable(observable)) { throw new Error(`invalid observable parameter`); } if (typeof observer !== "function") { throw new Error(`observer MUST be a function, got '${observer}'`); } const observers = observable[oMetaKey].observers; if (!observers.some((o) => o[0] === observer)) { observers.push([observer, processObserveOptions(options)]); } else { console.warn("observer may be bound to an observable only once; will NOT rebind"); } }, unobserve: (observable, ...observers) => { if (!Observable.isObservable(observable)) { throw new Error(`invalid observable parameter`); } const existingObs = observable[oMetaKey].observers; let el = existingObs.length; if (!el) { return; } if (!observers.length) { existingObs.splice(0); return; } while (el) { let i = observers.indexOf(existingObs[--el][0]); if (i >= 0) { existingObs.splice(el, 1); } } } }); class ObjectObserver { #observer; #targets; constructor(observer) { this.#observer = observer; this.#targets = /* @__PURE__ */ new Set(); Object.freeze(this); } observe(target, options) { const r = Observable.from(target); Observable.observe(r, this.#observer, options); this.#targets.add(r); return r; } unobserve(target) { Observable.unobserve(target, this.#observer); this.#targets.delete(target); } disconnect() { for (const t of this.#targets) { Observable.unobserve(t, this.#observer); } this.#targets.clear(); } }