@signaldb/core
Version:
SignalDB is a client-side database that provides a simple MongoDB-like interface to the data with first-class typescript support to achieve an optimistic UI. Data persistence can be achieved by using storage providers that store the data through a JSON in
147 lines (146 loc) • 5.45 kB
JavaScript
;
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
const isEqual = require("./index.cjs9.js");
const uniqueBy = require("./index.cjs29.js");
class Observer {
/**
* Creates a new instance of the `Observer` class.
* Sets up event bindings and initializes the callbacks for tracking changes in a collection.
* @param bindEvents - A function to bind external events to the observer. Must return a cleanup function to unbind those events.
*/
constructor(bindEvents) {
__publicField(this, "previousItems", []);
__publicField(this, "callbacks");
__publicField(this, "unbindEvents");
this.callbacks = {
added: [],
addedBefore: [],
changed: [],
changedField: [],
movedBefore: [],
removed: []
};
this.unbindEvents = bindEvents();
}
call(event, ...args) {
this.callbacks[event].forEach(({ callback, options }) => {
if (!options.skipInitial || !options.isInitial) {
callback(...args);
}
});
}
hasCallbacks(events) {
return events.some((event) => this.callbacks[event].length > 0);
}
/**
* Determines if the observer has no active callbacks registered for any events.
* @returns A boolean indicating whether the observer is empty (i.e., no callbacks are registered).
*/
isEmpty() {
return !this.hasCallbacks([
"added",
"addedBefore",
"changed",
"changedField",
"movedBefore",
"removed"
]);
}
/**
* Compares the previous state of items with the new state and triggers the appropriate callbacks
* for events such as added, removed, changed, or moved items.
* @param newItems - The new list of items to compare against the previous state.
*/
runChecks(newItems) {
const oldItemsMap = new Map(this.previousItems.map((item, index) => [
item.id,
{ item, index, beforeItem: this.previousItems[index + 1] || null }
]));
const newItemsMap = new Map(newItems.map((item, index) => [
item.id,
{ item, index, beforeItem: newItems[index + 1] || null }
]));
if (this.hasCallbacks(["changed", "changedField", "movedBefore", "removed"])) {
oldItemsMap.forEach(({ item: oldItem, index, beforeItem: oldBeforeItem }) => {
var _a;
const newItem = newItemsMap.get(oldItem.id);
if (newItem) {
if (this.hasCallbacks(["changed", "changedField"]) && !isEqual(newItem.item, oldItem)) {
this.call("changed", newItem.item);
if (this.hasCallbacks(["changedField"])) {
const keys = uniqueBy([
...Object.keys(newItem.item),
...Object.keys(oldItem)
], (value) => value);
keys.forEach((key) => {
if (isEqual(newItem.item[key], oldItem[key]))
return;
this.call("changedField", newItem.item, key, oldItem[key], newItem.item[key]);
});
}
}
if (newItem.index !== index && ((_a = newItem.beforeItem) == null ? void 0 : _a.id) !== (oldBeforeItem == null ? void 0 : oldBeforeItem.id)) {
this.call("movedBefore", newItem.item, newItem.beforeItem);
}
} else {
this.call("removed", oldItem);
}
});
}
if (this.hasCallbacks(["added", "addedBefore"])) {
newItems.forEach((newItem, index) => {
const oldItem = oldItemsMap.get(newItem.id);
if (oldItem)
return;
this.call("added", newItem);
this.call("addedBefore", newItem, newItems[index + 1] || null);
});
}
this.previousItems = newItems;
Object.keys(this.callbacks).forEach((key) => {
const event = key;
const callbacks = this.callbacks[event];
this.callbacks[event] = callbacks.map((callback) => ({
...callback,
options: {
...callback.options,
isInitial: false
}
}));
});
}
/**
* Stops the observer by unbinding all events and cleaning up resources.
*/
stop() {
this.unbindEvents();
}
/**
* Registers callbacks for specific events to observe changes in the collection.
* @param callbacks - An object containing the callbacks for various events (e.g., 'added', 'removed').
* @param skipInitial - A boolean indicating whether to skip invoking the callbacks for the initial state of the collection.
*/
addCallbacks(callbacks, skipInitial = false) {
Object.keys(callbacks).forEach((key) => {
const typedKey = key;
this.callbacks[typedKey].push({
callback: callbacks[typedKey],
options: { skipInitial, isInitial: true }
});
});
}
/**
* Removes the specified callbacks for specific events, unregistering them from the observer.
* @param callbacks - An object containing the callbacks to be removed for various events.
*/
removeCallbacks(callbacks) {
Object.keys(callbacks).forEach((key) => {
const typedKey = key;
const index = this.callbacks[typedKey].findIndex(({ callback }) => callback === callbacks[typedKey]);
this.callbacks[typedKey].splice(index, 1);
});
}
}
module.exports = Observer;