UNPKG

@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

779 lines (778 loc) 30.3 kB
import deepClone from "./index6.mjs"; import isEqual from "./index7.mjs"; import Cursor from "./index10.mjs"; import EventEmitter from "./index11.mjs"; import match from "./index12.mjs"; import modify from "./index13.mjs"; import randomId from "./index14.mjs"; import serializeValue from "./index15.mjs"; import createSignal from "./index16.mjs"; import getIndexInfo from "./index18.mjs"; import { createExternalIndex } from "./index22.mjs"; //#region src/Collection/index.ts /** * Checks if there are any pending updates in the given changeset. * @template T - The type of the items in the changeset. * @param pendingUpdates - The changeset to check for pending updates. * @returns `true` if there are pending updates, otherwise `false`. */ function hasPendingUpdates(pendingUpdates) { return pendingUpdates.added.length > 0 || pendingUpdates.modified.length > 0 || pendingUpdates.removed.length > 0; } /** * Applies updates (add, modify, remove) to a collection of current items. * @template T - The type of the items being updated. * @template I - The type of the unique identifier for the items. * @param currentItems - The current list of items. * @param changeset - The changeset containing added, modified, and removed items. * @param changeset.added An array of items to be added to the collection. * @param changeset.modified An array of items to replace existing items in the collection. Matching is based on item `id`. * @param changeset.removed An array of items to be removed from the collection. Matching is based on item `id`. * @returns A new array with the updates applied. */ function applyUpdates(currentItems, { added, modified, removed }) { const items = [...currentItems]; added.forEach((item) => { items.push(item); }); modified.forEach((item) => { const index = items.findIndex(({ id }) => id === item.id); if (index === -1) return; items[index] = item; }); removed.forEach((item) => { const index = items.findIndex(({ id }) => id === item.id); if (index === -1) return; items.splice(index, 1); }); return items; } /** * Represents a collection of data items with support for in-memory operations, * persistence, reactivity, and event-based notifications. The collection provides * CRUD operations, observer patterns, and batch operations. * @template T - The type of the items stored in the collection. * @template I - The type of the unique identifier for the items. * @template U - The transformed item type after applying transformations (default is T). */ var Collection = class Collection extends EventEmitter { static collections = []; static debugMode = false; static batchOperationInProgress = false; static fieldTracking = false; static onCreationCallbacks = []; static onDisposeCallbacks = []; static getCollections() { return Collection.collections; } static onCreation(callback) { Collection.onCreationCallbacks.push(callback); } static onDispose(callback) { Collection.onDisposeCallbacks.push(callback); } /** * Enables debug mode for all collections. */ static enableDebugMode = () => { Collection.debugMode = true; Collection.collections.forEach((collection) => { collection.setDebugMode(true); }); }; /** * Enables field tracking for all collections. * @param enable - A boolean indicating whether to enable field tracking. */ static setFieldTracking = (enable) => { Collection.fieldTracking = enable; Collection.collections.forEach((collection) => { collection.setFieldTracking(enable); }); }; /** * Executes a batch operation, allowing multiple modifications to the collection * while deferring index rebuilding until all operations in the batch are completed. * This improves performance by avoiding repetitive index recalculations and * provides atomicity for the batch of operations. * @param callback - The batch operation to execute. */ static batch(callback) { Collection.batchOperationInProgress = true; Collection.collections.reduce((memo, collection) => () => collection.batch(() => memo()), callback)(); Collection.batchOperationInProgress = false; } name; options; persistenceAdapter = null; isPullingSignal; isPushingSignal; indexProviders = []; indicesOutdated = false; idIndex = /* @__PURE__ */ new Map(); debugMode; batchOperationInProgress = false; isDisposed = false; postBatchCallbacks = /* @__PURE__ */ new Set(); fieldTracking = false; persistenceReadyPromise; pendingUpdates = { added: [], modified: [], removed: [] }; /** * Initializes a new instance of the `Collection` class with optional configuration. * Sets up memory, persistence, reactivity, and indices as specified in the options. * @template T - The type of the items stored in the collection. * @template I - The type of the unique identifier for the items. * @template U - The transformed item type after applying transformations (default is T). * @param options - Optional configuration for the collection. * @param options.name - An optional name for the collection. * @param options.memory - The in-memory adapter for storing items. * @param options.reactivity - The reactivity adapter for observing changes in the collection. * @param options.transform - A transformation function to apply to items when retrieving them. * @param options.persistence - The persistence adapter for saving and loading items. * @param options.indices - An array of index providers for optimized querying. * @param options.enableDebugMode - A boolean to enable or disable debug mode. * @param options.fieldTracking - A boolean to enable or disable field tracking by default. * @param options.transformAll - A function that will be able to solve the n+1 problem */ constructor(options) { super(); Collection.collections.push(this); this.name = options?.name ?? `${this.constructor.name}-${randomId()}`; this.options = { memory: [], ...options }; this.fieldTracking = this.options.fieldTracking ?? Collection.fieldTracking; this.debugMode = this.options.enableDebugMode ?? Collection.debugMode; this.indexProviders = [createExternalIndex("id", this.idIndex), ...this.options.indices || []]; this.rebuildIndices(); this.isPullingSignal = createSignal(this.options.reactivity, !!options?.persistence); this.isPushingSignal = createSignal(this.options.reactivity, false); this.on("persistence.pullStarted", () => { this.isPullingSignal.set(true); }); this.on("persistence.pullCompleted", () => { this.isPullingSignal.set(false); }); this.on("persistence.pushStarted", () => { this.isPushingSignal.set(true); }); this.on("persistence.pushCompleted", () => { this.isPushingSignal.set(false); }); this.persistenceAdapter = this.options.persistence ?? null; if (this.persistenceAdapter) { let ongoingSaves = 0; let isInitialized = false; const saveQueue = { added: [], modified: [], removed: [] }; let isFlushing = false; const flushQueue = () => { if (!this.persistenceAdapter) throw new Error("Persistence adapter not found"); if (ongoingSaves <= 0) this.emit("persistence.pushStarted"); if (isFlushing) return; if (!hasPendingUpdates(saveQueue)) return; isFlushing = true; ongoingSaves += 1; const currentItems = this.memoryArray(); const changes = { ...saveQueue }; saveQueue.added = []; saveQueue.modified = []; saveQueue.removed = []; this.persistenceAdapter.save(currentItems, changes).then(() => { this.emit("persistence.transmitted"); }).catch((error) => { this.emit("persistence.error", error instanceof Error ? error : new Error(error)); }).finally(() => { ongoingSaves -= 1; isFlushing = false; flushQueue(); if (ongoingSaves <= 0) this.emit("persistence.pushCompleted"); }); }; this.on("added", (item) => { if (!isInitialized) { this.pendingUpdates.added.push(item); return; } saveQueue.added.push(item); flushQueue(); }); this.on("changed", (item) => { if (!isInitialized) { this.pendingUpdates.modified.push(item); return; } saveQueue.modified.push(item); flushQueue(); }); this.on("removed", (item) => { if (!isInitialized) { this.pendingUpdates.removed.push(item); return; } saveQueue.removed.push(item); flushQueue(); }); this.persistenceAdapter.register((data) => this.loadPersistentData(data, ongoingSaves > 0)).then(async () => { if (!this.persistenceAdapter) throw new Error("Persistence adapter not found"); let currentItems = this.memoryArray(); await this.loadPersistentData(); while (hasPendingUpdates(this.pendingUpdates)) { const added = this.pendingUpdates.added.splice(0); const modified = this.pendingUpdates.modified.splice(0); const removed = this.pendingUpdates.removed.splice(0); currentItems = applyUpdates(this.memoryArray(), { added, modified, removed }); await this.persistenceAdapter.save(currentItems, { added, modified, removed }).then(() => { this.emit("persistence.transmitted"); }); } await this.loadPersistentData(); isInitialized = true; setTimeout(() => this.emit("persistence.init"), 0); }).catch((error) => { this.emit("persistence.error", error instanceof Error ? error : new Error(error)); }); } this.persistenceReadyPromise = new Promise((resolve, reject) => { if (!this.persistenceAdapter) return resolve(); this.once("persistence.init", resolve); this.once("persistence.error", reject); }); Collection.onCreationCallbacks.forEach((callback) => callback(this)); } /** * Resets the collection's data by clearing the in-memory items and reloading from the persistence adapter. * @returns A promise that resolves when the data has been reset and reloaded. */ async resetData() { if (hasPendingUpdates(this.pendingUpdates)) await new Promise((resolve) => { this.on("persistence.transmitted", resolve); }); this.options.memory = []; await this.loadPersistentData(); } /** * Loads data from the persistence adapter and updates the in-memory collection accordingly. * @param data - Optional data to load, containing either a full list of items or a set of changes. If not provided, data will be loaded from the persistence adapter. * @param hasOngoingSaves - A boolean indicating whether there are ongoing save operations. If `true`, the method will skip loading data to avoid conflicts with pending updates. * @returns A promise that resolves when the data has been loaded and the in-memory collection has been updated. */ async loadPersistentData(data, hasOngoingSaves = false) { if (!this.persistenceAdapter) throw new Error("Persistence adapter not found"); this.emit("persistence.pullStarted"); const { items, changes } = data ?? await this.persistenceAdapter.load(); if (items) { if (hasOngoingSaves) return; this.memory().splice(0, this.memoryArray().length, ...items); this.idIndex.clear(); this.memory().map((item, index) => { this.idIndex.set(serializeValue(item.id), new Set([index])); }); } else if (changes) { changes.added.forEach((item) => { const index = this.memory().findIndex((document) => document.id === item.id); if (index !== -1) { this.memory().splice(index, 1, item); return; } this.memory().push(item); const itemIndex = this.memory().findIndex((document) => document === item); this.idIndex.set(serializeValue(item.id), new Set([itemIndex])); }); changes.modified.forEach((item) => { const index = this.memory().findIndex((document) => document.id === item.id); if (index === -1) throw new Error("Cannot resolve index for item"); this.memory().splice(index, 1, item); }); changes.removed.forEach((item) => { const index = this.memory().findIndex((document) => document.id === item.id); if (index === -1) throw new Error("Cannot resolve index for item"); this.memory().splice(index, 1); }); } this.rebuildIndices(); this.emit("persistence.received"); setTimeout(() => this.emit("persistence.pullCompleted"), 0); } /** * Checks whether the collection is currently performing a pull operation * ⚡️ this function is reactive! * (loading data from the persistence adapter). * @returns A boolean indicating if the collection is in the process of pulling data. */ isPulling() { return this.isPullingSignal.get() ?? false; } /** * Checks whether the collection is currently performing a push operation * ⚡️ this function is reactive! * (saving data to the persistence adapter). * @returns A boolean indicating if the collection is in the process of pushing data. */ isPushing() { return this.isPushingSignal.get() ?? false; } /** * Checks whether the collection is currently performing either a pull or push operation, * ⚡️ this function is reactive! * indicating that it is loading or saving data. * @returns A boolean indicating if the collection is in the process of loading or saving data. */ isLoading() { const isPulling = this.isPulling(); const isPushing = this.isPushing(); return isPulling || isPushing; } /** * Retrieves the current debug mode status of the collection. * @returns A boolean indicating whether debug mode is enabled for the collection. */ getDebugMode() { return this.debugMode; } /** * Enables or disables debug mode for the collection. * When debug mode is enabled, additional debugging information and events are emitted. * @param enable - A boolean indicating whether to enable (`true`) or disable (`false`) debug mode. */ setDebugMode(enable) { this.debugMode = enable; } /** * Enables or disables field tracking for the collection. * @param enable - A boolean indicating whether to enable (`true`) or disable (`false`) field tracking. */ setFieldTracking(enable) { this.fieldTracking = enable; } /** * Resolves when the persistence adapter finished initializing * and the collection is ready to be used. * @returns A promise that resolves when the collection is ready. * @example * ```ts * const collection = new Collection({ * persistence: // ... * }) * await collection.isReady() * * collection.insert({ name: 'Item 1' }) */ async isReady() { return this.persistenceReadyPromise; } profile(fn, measureFunction) { if (!this.debugMode) return fn(); const startTime = performance.now(); const result = fn(); measureFunction(performance.now() - startTime); return result; } executeInDebugMode(fn) { if (!this.debugMode) return; fn((/* @__PURE__ */ new Error()).stack || ""); } rebuildIndices() { this.indicesOutdated = true; if (this.batchOperationInProgress) return; this.rebuildAllIndices(); } rebuildAllIndices() { this.idIndex.clear(); this.memory().map((item, index) => { this.idIndex.set(serializeValue(item.id), new Set([index])); }); this.indexProviders.forEach((index) => index.rebuild(this.memoryArray())); this.indicesOutdated = false; } getIndexInfo(selector) { if (selector != null && Object.keys(selector).length === 1 && "id" in selector && typeof selector.id !== "object") return { matched: true, positions: [...this.idIndex.get(serializeValue(selector.id)) || []], optimizedSelector: {} }; if (selector == null) return { matched: false, positions: [], optimizedSelector: {} }; if (this.indicesOutdated) return { matched: false, positions: [], optimizedSelector: selector }; return getIndexInfo(this.indexProviders, selector); } getItemAndIndex(selector) { const memory = this.memoryArray(); const indexInfo = this.getIndexInfo(selector); const item = (indexInfo.matched ? indexInfo.positions.map((index) => memory[index]).filter((item) => item != null) : memory).find((document) => match(document, selector)); const index = indexInfo.matched && indexInfo.positions.find((itemIndex) => memory[itemIndex] === item) || memory.findIndex((document) => document === item); if (item == null) return { item: null, index: -1 }; if (index === -1) throw new Error("Cannot resolve index for item"); return { item, index }; } deleteFromIdIndex(id, index) { this.idIndex.delete(serializeValue(id)); if (!this.batchOperationInProgress) return; this.idIndex.forEach(([currenIndex], key) => { if (currenIndex > index) this.idIndex.set(key, new Set([currenIndex - 1])); }); } memory() { return this.options.memory; } memoryArray() { return this.memory().map((item) => item); } transform(item) { if (!this.options.transform) return item; return this.options.transform(item); } transformAll(items, fields) { if (!this.options.transformAll) return items; return this.options.transformAll(items, fields); } getItems(selector) { return this.profile(() => { const indexInfo = this.getIndexInfo(selector); const matchItems = (item) => { if (indexInfo.optimizedSelector == null) return true; if (Object.keys(indexInfo.optimizedSelector).length <= 0) return true; return match(item, indexInfo.optimizedSelector); }; this.emit("getItems", selector); const memory = this.memoryArray(); if (!indexInfo.matched) { if (isEqual(selector, {})) return memory; return memory.filter(matchItems); } const items = indexInfo.positions.map((index) => memory[index]).filter((item) => item != null); if (isEqual(indexInfo.optimizedSelector, {})) return items; return items.filter(matchItems); }, (measuredTime) => this.executeInDebugMode((callstack) => this.emit("_debug.getItems", callstack, selector, measuredTime))); } /** * Disposes the collection, unregisters persistence adapters, clears memory, and * cleans up all resources used by the collection. * @returns A promise that resolves when the collection is disposed. */ async dispose() { if (this.persistenceAdapter?.unregister) await this.persistenceAdapter.unregister(); this.persistenceAdapter = null; this.memory().map(() => this.memory().pop()); this.idIndex.clear(); this.indexProviders = []; this.isDisposed = true; this.removeAllListeners(); Collection.collections = Collection.collections.filter((collection) => collection !== this); Collection.onDisposeCallbacks.forEach((callback) => callback(this)); } /** * Finds multiple items in the collection based on a selector and optional options. * Returns a cursor for reactive data queries. * @template O - The options type for the find operation. * @param [selector] - The criteria to select items. * @param [options] - Options for the find operation, such as limit and sort. * @returns A cursor to fetch and observe the matching items. */ find(selector, options) { if (this.isDisposed) throw new Error("Collection is disposed"); if (selector !== void 0 && (!selector || typeof selector !== "object")) throw new Error("Invalid selector"); const cursor = new Cursor(() => this.getItems(selector), { reactive: this.options.reactivity, fieldTracking: this.fieldTracking, ...options, transform: this.transform.bind(this), transformAll: this.transformAll.bind(this), bindEvents: (requery) => { const handleRequery = () => { if (this.batchOperationInProgress) { this.postBatchCallbacks.add(requery); return; } requery(); }; this.addListener("persistence.received", handleRequery); this.addListener("added", handleRequery); this.addListener("changed", handleRequery); this.addListener("removed", handleRequery); this.emit("observer.created", selector, options); return () => { this.removeListener("persistence.received", handleRequery); this.removeListener("added", handleRequery); this.removeListener("changed", handleRequery); this.removeListener("removed", handleRequery); this.emit("observer.disposed", selector, options); }; } }); this.emit("find", selector, options, cursor); this.executeInDebugMode((callstack) => this.emit("_debug.find", callstack, selector, options, cursor)); return cursor; } /** * Finds a single item in the collection based on a selector and optional options. * ⚡️ this function is reactive! * Returns the found item or undefined if no item matches. * @template O - The options type for the find operation. * @param selector - The criteria to select the item. * @param [options] - Options for the find operation, such as projection. * @returns The found item or `undefined`. */ findOne(selector, options) { if (this.isDisposed) throw new Error("Collection is disposed"); const returnValue = this.find(selector, { limit: 1, ...options }).fetch()[0] || void 0; this.emit("findOne", selector, options, returnValue); this.executeInDebugMode((callstack) => this.emit("_debug.findOne", callstack, selector, options, returnValue)); return returnValue; } /** * Performs a batch operation, deferring index rebuilds and allowing multiple * modifications to be made atomically. Executes any post-batch callbacks afterwards. * @param callback - The batch operation to execute. */ batch(callback) { this.batchOperationInProgress = true; callback(); this.batchOperationInProgress = false; this.rebuildAllIndices(); this.postBatchCallbacks.forEach((callback_) => callback_()); this.postBatchCallbacks.clear(); } /** * Inserts a single item into the collection. Generates a unique ID if not provided. * @param item - The item to insert. * @returns The ID of the inserted item. * @throws {Error} If the collection is disposed or the item has an invalid ID. */ insert(item) { if (this.isDisposed) throw new Error("Collection is disposed"); if (!item) throw new Error("Invalid item"); const newItem = { id: (this.options.primaryKeyGenerator ?? randomId)(item), ...item }; this.emit("validate", newItem); if (this.idIndex.has(serializeValue(newItem.id))) throw new Error("Item with same id already exists"); this.memory().push(newItem); const itemIndex = this.memory().findIndex((document) => document === newItem); this.idIndex.set(serializeValue(newItem.id), new Set([itemIndex])); this.rebuildIndices(); this.emit("added", newItem); this.emit("insert", newItem); this.executeInDebugMode((callstack) => this.emit("_debug.insert", callstack, newItem)); return newItem.id; } /** * Inserts multiple items into the collection. Generates unique IDs for items if not provided. * @param items - The items to insert. * @returns An array of IDs of the inserted items. * @throws {Error} If the collection is disposed or the items are invalid. */ insertMany(items) { if (this.isDisposed) throw new Error("Collection is disposed"); if (!items) throw new Error("Invalid items"); if (items.length === 0) return []; const ids = []; this.batch(() => { items.forEach((item) => { ids.push(this.insert(item)); }); }); return ids; } /** * Updates a single item in the collection that matches the given selector. * @param selector - The criteria to select the item to update. * @param modifier - The modifications to apply to the item. * @param [options] - Optional settings for the update operation. * @param [options.upsert] - If `true`, creates a new item if no item matches the selector. * @returns The number of items updated (0 or 1). * @throws {Error} If the collection is disposed or invalid arguments are provided. */ updateOne(selector, modifier, options) { if (this.isDisposed) throw new Error("Collection is disposed"); if (!selector) throw new Error("Invalid selector"); if (!modifier) throw new Error("Invalid modifier"); const { $setOnInsert, ...restModifier } = modifier; const { item, index } = this.getItemAndIndex(selector); if (item == null) { if (options?.upsert) { const newItem = modify({}, { ...restModifier, $set: { ...$setOnInsert, ...restModifier.$set } }); if (newItem.id != null && this.getItemAndIndex({ id: newItem.id }).item != null) throw new Error("Item with same id already exists"); this.insert(newItem); } } else { const modifiedItem = modify(deepClone(item), restModifier); if (item.id !== modifiedItem.id && this.getItemAndIndex({ id: modifiedItem.id }).item != null) throw new Error("Item with same id already exists"); this.emit("validate", modifiedItem); this.memory().splice(index, 1, modifiedItem); this.rebuildIndices(); this.emit("changed", modifiedItem, restModifier); } this.emit("updateOne", selector, modifier); this.executeInDebugMode((callstack) => this.emit("_debug.updateOne", callstack, selector, modifier)); if (item == null && !options?.upsert) return 0; return 1; } /** * Updates multiple items in the collection that match the given selector. * @param selector - The criteria to select the items to update. * @param modifier - The modifications to apply to the items. * @param [options] - Optional settings for the update operation. * @param [options.upsert] - If `true`, creates new items if no items match the selector. * @returns The number of items updated. * @throws {Error} If the collection is disposed or invalid arguments are provided. */ updateMany(selector, modifier, options) { if (this.isDisposed) throw new Error("Collection is disposed"); if (!selector) throw new Error("Invalid selector"); if (!modifier) throw new Error("Invalid modifier"); const { $setOnInsert, ...restModifier } = modifier; const items = this.getItems(selector); if (items.length === 0 && options?.upsert) { const newItem = modify({}, { ...restModifier, $set: { ...$setOnInsert, ...restModifier.$set } }); if (newItem.id != null && this.getItemAndIndex({ id: newItem.id }).item != null) throw new Error("Item with same id already exists"); this.insert(newItem); } const changes = items.map((item) => { const { index } = this.getItemAndIndex({ id: item.id }); if (index === -1) throw new Error(`Cannot resolve index for item with id '${item.id}'`); const modifiedItem = modify(deepClone(item), restModifier); if (item.id !== modifiedItem.id && this.getItemAndIndex({ id: modifiedItem.id }).item != null) throw new Error(`Item with same id '${modifiedItem.id}' already exists`); this.emit("validate", modifiedItem); return { item: modifiedItem, index }; }); changes.forEach(({ item, index }) => { this.memory().splice(index, 1, item); }); this.rebuildIndices(); changes.forEach(({ item }) => { this.emit("changed", item, restModifier); }); this.emit("updateMany", selector, modifier); this.executeInDebugMode((callstack) => this.emit("_debug.updateMany", callstack, selector, modifier)); return changes.length === 0 && options?.upsert ? 1 : changes.length; } /** * Replaces a single item in the collection that matches the given selector. * @param selector - The criteria to select the item to replace. * @param replacement - The item to replace the selected item with. * @param [options] - Optional settings for the replace operation. * @param [options.upsert] - If `true`, creates a new item if no item matches the selector. * @returns The number of items replaced (0 or 1). * @throws {Error} If the collection is disposed or invalid arguments are provided. */ replaceOne(selector, replacement, options) { if (this.isDisposed) throw new Error("Collection is disposed"); if (!selector) throw new Error("Invalid selector"); const { item, index } = this.getItemAndIndex(selector); if (item == null) { if (options?.upsert) { if (replacement.id != null && this.getItemAndIndex({ id: replacement.id }).item != null) throw new Error("Item with same id already exists"); this.insert(replacement); } } else { if (item.id !== replacement.id && this.getItemAndIndex({ id: replacement.id }).item != null) throw new Error("Item with same id already exists"); const modifiedItem = { id: item.id, ...replacement }; this.emit("validate", modifiedItem); this.memory().splice(index, 1, modifiedItem); this.rebuildIndices(); this.emit("changed", modifiedItem, replacement); } this.emit("replaceOne", selector, replacement); this.executeInDebugMode((callstack) => this.emit("_debug.replaceOne", callstack, selector, replacement)); if (item == null && !options?.upsert) return 0; return 1; } /** * Removes a single item from the collection that matches the given selector. * @param selector - The criteria to select the item to remove. * @returns The number of items removed (0 or 1). * @throws {Error} If the collection is disposed or invalid arguments are provided. */ removeOne(selector) { if (this.isDisposed) throw new Error("Collection is disposed"); if (!selector) throw new Error("Invalid selector"); const { item, index } = this.getItemAndIndex(selector); if (item != null) { this.memory().splice(index, 1); this.deleteFromIdIndex(item.id, index); this.rebuildIndices(); this.emit("removed", item); } this.emit("removeOne", selector); this.executeInDebugMode((callstack) => this.emit("_debug.removeOne", callstack, selector)); return item == null ? 0 : 1; } /** * Removes multiple items from the collection that match the given selector. * @param selector - The criteria to select the items to remove. * @returns The number of items removed. * @throws {Error} If the collection is disposed or invalid arguments are provided. */ removeMany(selector) { if (this.isDisposed) throw new Error("Collection is disposed"); if (!selector) throw new Error("Invalid selector"); const items = this.getItems(selector); items.forEach((item) => { const index = this.memory().findIndex((document) => document === item); if (index === -1) throw new Error("Cannot resolve index for item"); this.memory().splice(index, 1); this.deleteFromIdIndex(item.id, index); this.rebuildIndices(); }); items.forEach((item) => { this.emit("removed", item); }); this.emit("removeMany", selector); this.executeInDebugMode((callstack) => this.emit("_debug.removeMany", callstack, selector)); return items.length; } }; //#endregion export { Collection as default };