@schukai/monster
Version:
Monster is a simple library for creating fast, robust and lightweight websites.
267 lines (227 loc) • 6.66 kB
JavaScript
/**
* Copyright © Volker Schukai and all contributing authors, {{copyRightYear}}. All rights reserved.
* Node module: @schukai/monster
*
* This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
* The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
*
* For those who do not wish to adhere to the AGPLv3, a commercial license is available.
* Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
* For more information about purchasing a commercial license, please contact Volker Schukai.
*
* SPDX-License-Identifier: AGPL-3.0
*/
import { Base } from "./base.mjs";
import { isArray, isElement, isObject, isPrimitive } from "./is.mjs";
import { Observer } from "./observer.mjs";
import { ObserverList } from "./observerlist.mjs";
import { validateObject } from "./validate.mjs";
import { extend } from "../data/extend.mjs";
import { instanceSymbol, proxyInstanceMarker } from "../constants.mjs";
import { clone } from "../util/clone.mjs";
export { ProxyObserver };
/**
* An observer manages a callback function
*
* With the ProxyObserver you can attach observer for observation.
* With each change at the object to be observed, an update takes place.
*
* This also applies to nested objects.
*
* @license AGPLv3
* @since 1.0.0
* @copyright Volker Schukai
*/
class ProxyObserver extends Base {
/**
*
* @param {object} object
* @throws {TypeError} value is not a object
*/
constructor(object) {
super();
this.realSubject = validateObject(object);
this.subject = new Proxy(object, getHandler.call(this));
this.objectMap = new WeakMap();
this.objectMap.set(this.realSubject, this.subject);
this.proxyMap = new WeakMap();
this.proxyMap.set(this.subject, this.realSubject);
this.observers = new ObserverList();
}
/**
* This method is called by the `instanceof` operator.
* @return {symbol}
* @since 2.1.0
*/
static get [instanceSymbol]() {
return Symbol.for("@schukai/monster/types/proxy-observer");
}
/**
* @return {object}
*/
getSubject() {
return this.subject;
}
/**
* @since 1.24.0
* @param {Object} obj
* @return {ProxyObserver}
*/
setSubject(obj) {
let i;
const clonedObject = clone(obj);
const k = Object.keys(this.subject);
for (i = 0; i < k.length; i++) {
delete this.subject[k[i]];
}
this.subject = extend(this.subject, clonedObject);
return this;
}
/**
* Retrieves the real subject associated with the current instance.
*
* @return {Object} The real subject object.
*/
getRealSubject() {
return this.realSubject;
}
/**
* attach a new observer
*
* @param {Observer} observer
* @return {ProxyObserver}
*/
attachObserver(observer) {
this.observers.attach(observer);
return this;
}
/**
* detach a observer
*
* @param {Observer} observer
* @return {ProxyObserver}
*/
detachObserver(observer) {
this.observers.detach(observer);
return this;
}
/**
* notify all observer
*
* @return {Promise}
*/
notifyObservers() {
return this.observers.notify(this);
}
/**
* @param {Observer} observer
* @return {boolean}
*/
containsObserver(observer) {
return this.observers.contains(observer);
}
}
/**
*
* @return {{defineProperty: (function(*=, *=, *=): *), setPrototypeOf: (function(*, *=): boolean), set: (function(*, *, *, *): boolean), get: ((function(*=, *=, *=): (undefined))|*), deleteProperty: ((function(*, *): (boolean))|*)}}
* @private
* @see {@link https://gitlab.schukai.com/-/snippets/49}
*/
function getHandler() {
const proxy = this;
// https://262.ecma-international.org/9.0/#sec-proxy-object-internal-methods-and-internal-slots
const handler = {
// https://262.ecma-international.org/9.0/#sec-proxy-object-internal-methods-and-internal-slots-get-p-receiver
get: function (target, key, receiver) {
// this is an internal hack to identify proxy
if (key === proxyInstanceMarker) {
return proxyInstanceMarker;
}
const value = Reflect.get(target, key, receiver);
if (typeof key === "symbol") {
return value;
}
if (isPrimitive(value)) {
return value;
}
if (isElement(value)) {
return value;
}
if (typeof Node !== "undefined" && value instanceof Node) {
return value;
}
// set value as proxy if object or array
if (isArray(value) || isObject(value)) {
if (proxy.objectMap.has(value)) {
return proxy.objectMap.get(value);
} else if (proxy.proxyMap.has(value)) {
return value;
} else {
const p = new Proxy(value, handler);
proxy.objectMap.set(value, p);
proxy.proxyMap.set(p, value);
return p;
}
}
return value;
},
// https://262.ecma-international.org/9.0/#sec-proxy-object-internal-methods-and-internal-slots-set-p-v-receiver
set: function (target, key, value, receiver) {
if (proxy.proxyMap.has(value)) {
value = proxy.proxyMap.get(value);
}
if (proxy.proxyMap.has(target)) {
target = proxy.proxyMap.get(target);
}
let current = Reflect.get(target, key, receiver);
if (proxy.proxyMap.has(current)) {
current = proxy.proxyMap.get(current);
}
if (current === value) {
return true;
}
const result = Reflect.set(target, key, value, receiver);
if (result !== true) {
return result;
}
if (typeof key !== "symbol") {
let next = Reflect.get(target, key, receiver);
if (proxy.proxyMap.has(next)) {
next = proxy.proxyMap.get(next);
}
if (next !== current) {
proxy.observers.notify(proxy);
}
}
return result;
},
// https://262.ecma-international.org/9.0/#sec-proxy-object-internal-methods-and-internal-slots-delete-p
deleteProperty: function (target, key) {
if (key in target) {
delete target[key];
if (typeof key !== "symbol") {
proxy.observers.notify(proxy);
}
return true;
}
return false;
},
// https://262.ecma-international.org/9.0/#sec-proxy-object-internal-methods-and-internal-slots-defineownproperty-p-desc
defineProperty: function (target, key, descriptor) {
const result = Reflect.defineProperty(target, key, descriptor);
if (typeof key !== "symbol") {
proxy.observers.notify(proxy);
}
return result;
},
// https://262.ecma-international.org/9.0/#sec-proxy-object-internal-methods-and-internal-slots-setprototypeof-v
setPrototypeOf: function (target, key) {
const result = Reflect.setPrototypeOf(target, key);
if (typeof key !== "symbol") {
proxy.observers.notify(proxy);
}
return result;
},
};
return handler;
}