types/proxyobserver.js

'use strict';

/**
 * @author schukai GmbH
 */

import {Monster} from '../namespace.js';
import {Base} from  './base.js';
import {validateObject} from "./validate.js";
import {ObserverList} from "./observerlist.js";
import {Observer} from "./observer.js";
import {isObject, isArray, isPrimitive} from "./is.js";

/**
 * an observer manages a callback function
 *
 * you can call the method via the monster namespace `new Monster.Types.Observer()`.
 *
 * ```
 * <script type="module">
 * import {Monster} from 'https://cdn.jsdelivr.net/npm/@schukai/monster@1.6.0/dist/modules/types/proxyobserver.js';
 * console.log(new Monster.Types.ProxyObserver())
 * </script>
 * ```
 *
 * Alternatively, you can also integrate this function individually.
 *
 * ```
 * <script type="module">
 * import {Observer} from 'https://cdn.jsdelivr.net/npm/@schukai/monster@1.6.0/dist/modules/types/proxyobserver.js';
 * console.log(new ProxyObserver())
 * </script>
 * ```
 *
 * 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.
 *
 * ```javascript
 * const o = new Observer(function () {
 *    if (isObject(this) && this instanceof ProxyObserver) {
 *        // do something (this ist ProxyObserver)
 *        const subject = this.getSubject();
 *    }
 * )
 * 
 * let realSubject = {
 *    a: {
 *        b: {
 *            c: true
 *        },
 *        d: 5
 *    }
 * 
 * 
 * const p = new ProxyObserver(realSubject);
 * p.attachObserver(o);
 * const s = p.getSubject();
 * s.a.b.c = false;
 * ```
 *
 * @since 1.0.0
 * @copyright schukai GmbH
 * @memberOf Monster/Types
 */
class ProxyObserver extends Base {

    /**
     *
     * @param {object} object
     * @throws {TypeError} value is not a object
     */
    constructor(object) {
        super();
        validateObject(object);
        this.realSubject = object
        this.subject = new Proxy(object, getHandler.call(this));

        this.objectMap = new WeakMap();
        this.objectMap.set(this.realSubject, this.subject);

        this.observers = new ObserverList;
    }

    /**
     * get the real object
     *
     * changes to this object are not noticed by the observers, so you can make a large number of changes and inform the observers later.
     *
     * @returns {object}
     */
    getSubject() {
        return this.subject
    }

    /**
     * get the proxied object
     *
     * @returns {object}
     */
    getRealSubject() {
        return this.realSubject
    }

    /**
     * attach a new observer
     *
     * @param {Observer} observer
     * @returns {ProxyObserver}
     */
    attachObserver(observer) {
        this.observers.attach(observer)
        return this;
    }

    /**
     * detach a observer
     *
     * @param {Observer} observer
     * @returns {ProxyObserver}
     */
    detachObserver(observer) {
        this.observers.detach(observer)
        return this;
    }

    /**
     * notify all observer
     *
     * @returns {ProxyObserver}
     */
    notifyObservers() {
        this.observers.notify(this);
        return this;
    }

    /**
     * @param {Observer} observer
     * @returns {ProxyObserver}
     */
    containsObserver(observer) {
        return this.observers.contains(observer)
    }

}

Monster.assignToNamespace('Monster.Types', ProxyObserver);
export {Monster, ProxyObserver}

/**
 *
 * @returns {{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) {

            const value = Reflect.get(target, key, receiver);

            if (typeof key === "symbol") {
                return value;
            }

            if (isPrimitive(value)) {
                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 {
                    let p = new Proxy(value, handler);
                    proxy.objectMap.set(value, p);
                    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) {
            const result = Reflect.set(target, key, value, receiver);
            if (typeof property !== "symbol") {
                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) {
            let 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) {
            let result = Reflect.setPrototypeOf(object1, key);

            if (typeof key !== "symbol") {
                proxy.observers.notify(proxy);
            }

            return result;
        }

    };


    return handler;
}