types/observer.js

'use strict';

/**
 * @author schukai GmbH
 */

import {Monster} from '../namespace.js';
import {TokenList} from './tokenlist.js';
import {isObject} from './is.js';
import {Base} from './base.js';
import {UniqueQueue} from './uniquequeue.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.5.0/dist/modules/types/observer.js';
 * console.log(new Monster.Types.Observer())
 * </script>
 * ```
 *
 * Alternatively, you can also integrate this function individually.
 *
 * ```
 * <script type="module">
 * import {Observer} from 'https://cdn.jsdelivr.net/npm/@schukai/monster@1.5.0/dist/modules/types/observer.js';
 * console.log(Observer())
 * </script>
 * ```
 *
 * the update method is called with the subject object as this pointer. for this reason the callback should not
 * be an arrow function, because it gets the this pointer of its own context.
 *
 * ```
 * <script>
 * Observer(()=>{
 *     // this is not subject
 * })
 *
 * Observer(function() {
 *     // this is subject
 * })
 * </script>
 * ```
 *
 * additional arguments can be passed to the callback. to do this, simply specify them.
 *
 * ```
 * <script>
 * Observer(function(a, b, c) {
 *     console.log(a, b, c); // ↦ "a", 2, true 
 * }, "a", 2, true)
 * </script>
 * ```
 *
 * the callback function must have as many parameters as arguments are given.
 *
 *
 * @since 1.0.0
 * @copyright schukai GmbH
 * @memberOf Monster/Types
 */
class Observer extends Base {

    /**
     *
     * @param {function} callback
     * @param {*} args
     */
    constructor(callback, ...args) {
        super();

        if (typeof callback !== 'function') {
            throw new Error("observer callback must be a function")
        }

        this.callback = callback;
        this.arguments = args;
        this.tags = new TokenList;
        this.queue = new UniqueQueue();
    }

    /**
     *
     * @param {string} tag
     * @returns {Observer}
     */
    addTag(tag) {
        this.tags.add(tag);
        return this;
    }

    /**
     *
     * @param {string} tag
     * @returns {Observer}
     */
    removeTag(tag) {
        this.tags.remove(tag);
        return this;
    }

    /**
     *
     * @returns {Array}
     */
    getTags() {
        return this.tags.entries()
    }

    /**
     *
     * @param {string} tag
     * @returns {boolean}
     */
    hasTag(tag) {
        return this.tags.contains(tag)
    }

    /**
     *
     * @param {object} subject
     * @returns {Promise}
     */
    update(subject) {
        let self = this;

        return new Promise(function (resolve, reject) {
            if (!isObject(subject)) {
                reject("subject must be an object");
            }

            self.queue.add(subject);

            setTimeout(() => {

                // the queue and the settimeout ensure that an object is not 
                // informed of the same change more than once.
                if (self.queue.isEmpty()) {
                    resolve();
                    return;
                }

                let s = self.queue.poll();
                let result = self.callback.apply(s, self.arguments);

                if (isObject(result) && result instanceof Promise) {
                    result.then(resolve).catch(reject);
                    return;
                }

                resolve(result);
            }, 0)

        });

    };

}

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