Collection
import { Collection } from '@signaldb/core'The Collection class is designed to manage and manipulate collections of data, with options for reactivity, transformations and a data adapter that decides where the data operations actually happen. Collections are schemaless, meaning that you don't need to define a schema for your data before you start using it. This allows you to store any data you want without worrying about defining a schema first. However, it's recommended that you define a typescript interface for the documents in the collection, so that you can benefit from type safety when working with the data.
Static Methods
setFieldTracking(enable: boolean)
Enables or disables field tracking for all collections. See Field-Level Reactivity for more information.
batch(collections?: Collection[], callback: () => void | Promise<void>)
If you need to execute many operations at once in multiple collections, you can use the global Collection.batch() method. It runs the callback inside the instance batch() of each collection, so their live queries are updated once when the batch ends instead of after every write. It returns a promise if the callback does. Pass the collections you are writing to — see the instance method for why that matters.
getCollections()
Returns an array of all collections that have been created.
onCreation(callback: (collection: Collection) => void)
Registers a callback that will be called whenever a new collection is created. The callback will receive the newly created collection as an argument.
onDispose(callback: (collection: Collection) => void)
Registers a callback that will be called whenever a collection is disposed. The callback will receive the disposed collection as an argument.
enableDebugMode()
Enables debug mode for all collections. This will enable measurements for query timings and other debug information. It also switches on reportLargeQueries() at 500 rows.
reportLargeQueries(rows: number | null)
Reports each live query holding more than rows rows once, together with the stack that registered it. Pass null to switch it off again.
Collection.reportLargeQueries(500)A reactive query registered from a long-lived place — a module scope, a store, a component that is never unmounted — keeps its cost for the lifetime of the application, and there is otherwise nothing to see: the query works, and its price only shows up much later as an application that has grown slow. This is how you find it.
Constructor
const collection = new Collection<T, I, U>(name: string, dataAdapter: DataAdapter, options?: CollectionOptions<T, I, U>)Constructs a new Collection object. The name identifies the collection — to its data adapter, which uses it to find the collection's storage, and in the developer tools.
Parameters
name: The name of the collection.dataAdapter: The DataAdapter that answers this collection's data operations. One data adapter usually serves every collection of an application.options(Optional): An object specifying various options for the collection. Options include:- reactivity: A ReactivityAdapter for enabling reactivity.
- transform: A transformation function to be applied to items. The document that should be transformed is passed as the only parameter. The function should return the transformed document (e.g.
(doc: T) => U) - transformAll: A function that receives all items of a query result at once and returns the transformed list. Useful for resolving relations without an N+1 query — see ORM. An
async: trueread calls it with{ async: true }as a third argument and awaits a promise returned for it; build it withreactiveOrAsyncto read related collections the same way the query is read. - indices: An array of field names to index, e.g.
['authorId', 'status']. - primaryKeyGenerator: A function that generates a unique ID for the item. If not provided, a default generator will be used.
- fieldTracking: Enables field-level reactivity for this collection. Defaults to the value set with the static
setFieldTracking(). - enableDebugMode: Enables debug mode for this collection. Defaults to
trueonce the staticenableDebugMode()was called.
A collection can also be constructed with options alone (new Collection(options?)), in which case it uses a DefaultDataAdapter of its own. That form still accepts the deprecated name option and the deprecated persistence option — a storage adapter, which is then passed to that DefaultDataAdapter. Without persistence, the data is kept in memory only. Prefer the new Collection(name, dataAdapter, options?) form.
Methods
ready()
Resolves when the storage adapter finished initializing and the collection is ready to be used. This is useful when you need to wait for the collection to be ready before executing any operations directly after creating it.
Example:
const collection = new Collection('items', dataAdapter)
await collection.ready()
await collection.insert({ name: 'Item 1' })
// ...isReady()
⚡️ this function is reactive!
Returns whether the collection is ready, as a reactive value. Use this where you want to render something while the collection is still initializing; use ready() where you want to wait for it.
const collection = new Collection('items', dataAdapter)
effect(() => {
if (!collection.isReady()) return
console.log(collection.find().fetch())
})find(selector?: Selector<T>, options?: Options)
Returns a new cursor object for the items in the collection that match a given selector and options. Also check out the queries section.
Parameters
selector(Optional): A selector object describing the items to match. Omit it to match all items.options(Optional): Options for the cursor —sort,skip,limit,fields,reactive,fieldTracking, andasync.
Pass async: true to get a cursor whose methods resolve to their result instead of returning it. That is what you want outside a reactive scope when the data adapter cannot answer on the spot — see queries that are not answered immediately.
const posts = await collection.find({ status: 'published' }, { async: true }).fetch()findOne(selector: Selector<T>, options?: Options)
Behaves the same like .find() but doesn't return a cursor. Instead it will directly return the first found document, or undefined. The selector is required; pass {} to match any document. With async: true it returns a promise resolving to that document.
Loading state
Three reactive methods report what the collection is currently doing. All of them register a dependency in a reactive scope.
isLoading(): ⚡️ reactive — whetherisPulling()orisPushing()istrue. Initiallyfalse.isPulling(): ⚡️ reactive — whether afind(…, { async: true })query is currently running.isPushing(): ⚡️ reactive — whether a write is currently running.
None of them describes the initial load from storage: use ready() or isReady() for that. Whether a single query has been answered yet is told by Cursor#isLoading().
insert(item: Omit<T, 'id'> & Partial<Pick<T, 'id'>>)
Inserts an item into the collection and returns a promise resolving to the ID of the newly inserted item. Also check out the data manipulation section.
insertMany(items: Array<Omit<T, 'id'> & Partial<Pick<T, 'id'>>>)
Inserts multiple items into the collection and returns a promise resolving to the IDs of the newly inserted items.
Parameters
items: The items to be inserted into the collection.
updateMany(selector: Selector<T>, modifier: Modifier<T>, options?: { upsert?: boolean })
Updates multiple items in the collection that match a given selector with the specified modifier and returns a promise resolving to the number of updated items. Also check out the data manipulation section.
Parameters
selector: A selector object describing the items to match.modifier: An object describing how to modify the matching items.options: An object with additional options. Currently onlyupsertis supported, which will insert a document based on the modifier, if the selector doesn't match any documents.
updateOne(selector: Selector<T>, modifier: Modifier<T>, options?: { upsert?: boolean })
Behaves the same like .updateMany() but only updates the first found document.
replaceOne(selector: Selector<T>, replacement: Omit<T, 'id'> & Partial<Pick<T, 'id'>>, options?: { upsert?: boolean })
Replaces a single item in the collection that matches a given selector with the specified replacement and returns a promise resolving to 0 or 1. Also check out the data manipulation section.
Parameters
selector: A selector object describing the items to match.replacement: The new item that should replace the existing one.options: An object with additional options. Currently onlyupsertis supported, which will insert a document based on the replacement, if the selector doesn't match any documents.
removeMany(selector: Selector<T>)
Removes multiple items from the collection that match a given selector and returns a promise resolving to the number of removed items.
Parameters
selector: A selector object describing the items to match.
removeOne(selector: Selector<T>)
Behaves the same like .removeMany() but only removes the first found document.
batch(callback: () => void)
If you need to execute many operations at once, things can get slow because every write updates the collection's live queries. To prevent this, you can use the .batch() method: the live queries of this collection are updated once, when the callback has finished, instead of after every write. It returns a promise if the callback does. A batch is not a transaction — writes that succeeded before a failing one are not rolled back.
await collection.batch(async () => {
await collection.insert({ name: 'Item 1' })
await collection.insert({ name: 'Item 2' })
// …
})If the writes span several collections, pass them to the static Collection.batch():
await Collection.batch([posts, authors], async () => {
await posts.insert({ title: 'Foo', authorId: 'a1' })
await authors.updateOne({ id: 'a1' }, { $inc: { postCount: 1 } })
})WARNING
Collection.batch() without a list of collections batches every collection in the process and defers every live query everywhere until the batch ends. That is the right thing for a handful of writes belonging to one event, and the wrong thing around anything whose length depends on the data. Name the collections you are actually writing to.
TIP
For an operation that spans awaits — a write, then whatever derives from it — reach for reactiveTransaction instead. It holds only the notification of reactive scopes, not the query pipeline, so it is safe to keep open for as long as the operation takes, and every scope updates once at the end rather than once per phase.
isBatchOperationInProgress()
Returns whether this collection is currently batching: during its own batch(), during a scoped Collection.batch([...]) that names it, and during an unscoped Collection.batch() covering every collection. A batch scoped to other collections does not affect it.
dispose()
Disposes the collection and all its resources and returns a promise that resolves when that is done. It tears down the collection's storage through its data adapter, removes all event listeners and cleans up all internal data structures. A disposed collection throws when it is used afterwards.
getDebugMode() / setDebugMode(enable: boolean)
Reads or changes whether debug mode is enabled for this collection. In debug mode, the collection measures its queries and emits additional events for the developer tools.
name
The name the collection was created with.
setFieldTracking(enabled: boolean)
Enables or disables field tracking for the collection. See Field-Level Reactivity for more information.
Events
The Collection class is equipped with a set of events that provide insights into the state and changes within the collection. Subscribe with on(event, listener), unsubscribe with off(event, listener), or use once(event, listener) for a single call:
const onChanged = (item, modifier, previousItem) => {
console.log('changed', item.id, previousItem, '→', item)
}
collection.on('changed', onChanged)
// …
collection.off('changed', onChanged)Here is an overview of the events:
added: Triggered when a new item is added to the collection. The event handler receives the added item as an argument.changed: Fired when an existing item in the collection undergoes modification. The event handler receives the modified item, the modifier that was applied and — when the data adapter reports it — the item as it was before the modification. All data adapters shipped with SignalDB report it; a custom one that does not simply omits the third argument.removed: Signaled when an item is removed or deleted from the collection. The event handler receives the removed item.validate: Emitted when an item should be validated. The event handler receives the item as an argument. Validate the item inside of the event handler and throw an error if the item is invalid. This will prevent the item from being inserted or updated.
In addition to that, the collection will fire events for each executed method. For example, if you call .updateOne(), the collection will fire an updateOne event. The event handler will receive the selector and the modifier as arguments.
find: Emitted when thefindmethod is called. The event handler receives the selector, options and the cursor as arguments.findOne: Triggered when thefindOnemethod is called. The event handler receives the selector, options and the returned item as arguments.insert: Fired when theinsertmethod is called. The event handler receives the inserted item as an argument.updateMany: Emitted when theupdateManymethod is called. The event handler receives the selector and the modifier as arguments.updateOne: Triggered when theupdateOnemethod is called. The event handler receives the selector and the modifier as arguments.replaceOne: Emitted when thereplaceOnemethod is called. The event handler receives the selector and the replacement as arguments.removeMany: Emitted when theremoveManymethod is called. The event handler receives the selector as an argument.removeOne: Triggered when theremoveOnemethod is called. The event handler receives the selector as an argument.
In addition to these, there are events about the queries a collection is serving:
query.error: A query backing at least one live cursor failed and will not deliver results. The event handler receives the error, the selector and the options. This event matters more than it looks: a cursor whose query failed keeps returning its neutral empty result, which is indistinguishable from a query that legitimately matched nothing. Listening here is the only way to tell the two apart.observer.created: A live query was registered. The event handler receives the selector and options.observer.disposed: A live query was given up. The event handler receives the selector and options.getItems: Items were read for a selector. The event handler receives the selector.
Removed in v2
The persistence.* events no longer exist. Use ready() / isReady() for the initial load, Cursor#isLoading() for a single query, the promise a write returns for its completion or failure, and the data adapter's onError option and query.error for other failures. See the upgrade guide.
These events empower developers to build dynamic and responsive applications by reacting to changes in the collection and facilitating synchronization with external data sources.