@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
JavaScript
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();
}
}