@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
JavaScript
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 };