@fimbul-works/observable
Version:
A lightweight, strongly-typed TypeScript library for reactive programming patterns, providing observable collections, values, and event handling.
258 lines (257 loc) • 8.18 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ObservableMap = void 0;
const signal_js_1 = require("./signal.js");
/**
* A Map implementation that notifies observers when its contents change.
* Emits CollectionEvents for add, update, delete, and clear operations.
*
* @template K The type of keys in the map
* @template V The type of values in the map
* @implements {Observable<CollectionEvent<K, V>>}
*/
class ObservableMap {
/** Internal signal used to emit collection events */
#signal = new signal_js_1.Signal();
/** Internal map instance that stores the actual data */
#map = new Map();
/**
* Creates a new ObservableMap instance.
* @param entries - Optional initial entries for the map
*/
constructor(entries) {
if (entries) {
for (const [key, value] of entries) {
this.#map.set(key, value);
}
}
}
/** Iterator implementation */
*[Symbol.iterator]() {
yield* this.#map.entries();
}
/**
* Returns the number of entries in the map.
* @returns The number of key-value pairs in the map
*/
get size() {
return this.#map.size;
}
/**
* Sets a value for the specified key in the map.
* Emits an 'add' event for new entries or an 'update' event for existing ones.
* @param key - The key to set
* @param value - The value to associate with the key
* @returns {this} The map instance for method chaining
*/
set(key, value) {
const exists = this.#map.has(key);
const oldValue = this.#map.get(key);
if (oldValue !== undefined && oldValue === value)
return this;
this.#map.set(key, value);
this.#signal.emit({
type: exists ? "update" : "add",
key,
value,
oldValue,
});
return this;
}
/**
* Sets a value for the specified key and waits for all change handlers to complete.
* @param key - The key to set
* @param value - The value to associate with the key
* @returns {Promise<this>} Promise resolving to the map instance for method chaining
*/
async setAsync(key, value) {
const exists = this.#map.has(key);
const oldValue = this.#map.get(key);
if (oldValue !== undefined && oldValue === value)
return this;
this.#map.set(key, value);
await this.#emitAsync({
type: exists ? "update" : "add",
key,
value,
oldValue,
});
return this;
}
/**
* Retrieves the value associated with a key.
* @param key - The key to look up
* @returns {V | undefined} The value associated with the key, or undefined if the key doesn't exist
*/
get(key) {
return this.#map.get(key);
}
/**
* Checks if a key exists in the map.
* @param key - The key to check
* @returns {boolean} True if the key exists, false otherwise
*/
has(key) {
return this.#map.has(key);
}
/**
* Removes a key and its associated value from the map.
* Emits a 'delete' event if the key existed.
* @param key - The key to remove
* @returns {boolean} True if the key was removed, false if it didn't exist
*/
delete(key) {
const value = this.#map.get(key);
const result = this.#map.delete(key);
if (result) {
this.#signal.emit({
type: "delete",
key,
oldValue: value,
});
}
return result;
}
/**
* Deletes a key-value pair and waits for all change handlers to complete.
* @param key - The key to delete
* @returns {Promise<boolean>} Promise resolving to true if the key was deleted, false otherwise
*/
async deleteAsync(key) {
const value = this.#map.get(key);
const result = this.#map.delete(key);
if (result) {
await this.#emitAsync({
type: "delete",
key,
oldValue: value,
});
}
return result;
}
/**
* Removes all entries from the map.
* Emits a 'clear' event.
*/
clear() {
if (this.#map.size === 0)
return;
this.#map.clear();
this.#signal.emit({
type: "clear",
key: null,
});
}
/**
* Clears the map and waits for all change handlers to complete.
* @returns {Promise<void>}
*/
async clearAsync() {
if (this.#map.size === 0)
return;
this.#map.clear();
await this.#emitAsync({
type: "clear",
key: null,
});
}
/**
* Returns an iterator over the map's values.
* @returns {IterableIterator<V>} An iterator over the map's values
*/
values() {
return this.#map.values();
}
/**
* Returns an iterator over the map's keys.
* @returns {IterableIterator<K>} An iterator over the map's keys
*/
keys() {
return this.#map.keys();
}
/**
* Returns an iterator over the map's entries.
* @returns {IterableIterator<[K, V]>} An iterator over the map's key-value pairs
*/
entries() {
return this.#map.entries();
}
/**
* Registers a function to be called when the map changes.
* @param fn - Function to be called with collection events
* @returns {() => void} A cleanup function that removes the listener
*/
onChange(fn) {
return this.#signal.connect(fn);
}
/**
* Executes a provided function once per each key/value pair in the Map, in insertion order.
* @param fn - Function to be called
*/
forEach(callbackfn, thisArg) {
this.#map.forEach(callbackfn, thisArg);
}
/**
* Creates a new ObservableMap with the results of calling a provided function on every element.
* @template U The type of values in the new map
* @param callbackfn - Function that produces a value for the new map
* @returns A new ObservableMap with each value transformed by the function
*/
map(callbackfn) {
const result = new ObservableMap();
for (const [key, value] of this) {
result.set(key, callbackfn(value, key, this));
}
return result;
}
/**
* Creates a new ObservableMap with all elements that pass the test.
* @param predicate - Function to test each entry of the map
* @returns A new ObservableMap with the entries that passed the test
*/
filter(predicate) {
const result = new ObservableMap();
for (const [key, value] of this) {
if (predicate(value, key, this)) {
result.set(key, value);
}
}
return result;
}
/**
* Converts the map to a plain object.
* Note: Keys must be strings or symbols.
* @returns A plain object with the same entries as the map
* @throws {TypeError} If any key is not a string or symbol
*/
toObject() {
const obj = {};
for (const [key, value] of this) {
if (typeof key !== "string" && typeof key !== "symbol") {
throw new TypeError("toObject() requires string or symbol keys");
}
obj[key] = value;
}
return obj;
}
/**
* Converts the map to a JSON-serializable format.
* Note: Values must be JSON-serializable.
* @returns An array of entries suitable for JSON serialization
*/
toJSON() {
return Array.from(this.#map.entries()).map(([key, value]) => [
String(key),
value,
]);
}
/**
* Emits a change event asynchronously and waits for all handlers to complete.
* @param event - The collection event to emit
* @returns {Promise<number>} Promise resolving to the number of handlers called
*/
async #emitAsync(event) {
return this.#signal.emitAsync(event);
}
}
exports.ObservableMap = ObservableMap;