@schukai/monster
Version:
Monster is a simple library for creating fast, robust and lightweight websites.
159 lines (141 loc) • 4.17 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 { isObject } from "./is.mjs";
import { TokenList } from "./tokenlist.mjs";
import { instanceSymbol } from "../constants.mjs";
export { Observer };
/**
* Manages a callback function that is executed asynchronously when updated.
*
* The `update` method is called with the subject object as its `this` context.
* For this reason, the callback should be a regular function, not an arrow function,
* if you need access to the subject via `this`.
*
* @example
* // Basic usage with a regular function to access the subject
* const observer = new Observer(function() {
* console.log("Subject updated:", this); // `this` refers to `mySubject`
* });
*
* // Usage with arguments
* const greeter = new Observer(function(greeting) {
* console.log(greeting, this.name); // "Hello", "World"
* }, "Hello");
*
* const mySubject = { name: "World" };
* observer.update(mySubject);
* greeter.update(mySubject);
*
* @license AGPLv3
* @since 1.0.0
*/
class Observer extends Base {
/**
* Stores promises for updates that are scheduled but not yet executed.
* This prevents multiple executions for the same subject within one microtask cycle.
* @type {Map<object, Promise<*>>}
*/
#pendingUpdates = new Map();
#callback;
#arguments;
#tags = new TokenList();
/**
* @param {function} callback The function to execute on update.
* @param {...*} args Additional arguments to pass to the callback.
*/
constructor(callback, ...args) {
super();
if (typeof callback !== "function") {
throw new Error("Observer callback must be a function.");
}
this.#callback = callback;
this.#arguments = args;
}
/**
* This method is called by the `instanceof` operator.
* @returns {symbol}
* @since 2.1.0
*/
static get [instanceSymbol]() {
return Symbol.for("@schukai/monster/types/observer");
}
// Getter for properties for cleaner access if needed
get callback() {
return this.#callback;
}
get arguments() {
return this.#arguments;
}
/**
* Schedules the callback for execution with the given subject.
* If multiple updates for the same subject are requested in the same event loop tick,
* the callback is only executed once. All callers will receive the same promise.
*
* @param {object} subject The subject object that triggered the update.
* @returns {Promise<*>} A promise that resolves with the return value of the callback,
* or rejects if the callback throws an error.
*/
update(subject) {
if (!isObject(subject)) {
return Promise.reject(new Error("Subject must be an object."));
}
if (this.#pendingUpdates.has(subject)) {
return this.#pendingUpdates.get(subject);
}
const promise = new Promise((resolve, reject) => {
queueMicrotask(async () => {
try {
const result = await this.#callback.apply(subject, this.#arguments);
resolve(result);
} catch (e) {
reject(e);
} finally {
this.#pendingUpdates.delete(subject);
}
});
});
this.#pendingUpdates.set(subject, promise);
return promise;
}
/**
* @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 {string[]}
*/
getTags() {
return this.#tags.entries();
}
/**
* @param {string} tag
* @returns {boolean}
*/
hasTag(tag) {
return this.#tags.contains(tag);
}
}