Upgrade to v2
This guide explains what changed in SignalDB v2 and how to migrate your application safely. It focuses on API changes, the new DataAdapter/StorageAdapter split, and practical before/after examples.
Summary of Breaking Changes
- CRUD methods on
Collectionare async now.insert,insertMany,updateOne,updateMany,replaceOne,removeOne,removeManyreturn Promises.- Return values changed to explicit IDs or counts (see examples below).
- Indices are configured as simple field names:
indices: string[]. - Persistence was renamed and redesigned:
PersistenceAdapter→StorageAdapter(new API shape).createPersistenceAdapter→createStorageAdapter.combinePersistenceAdapterswas removed.
AutoFetchCollectionwas removed. UseAutoFetchDataAdapterinstead.- Persistence events on
Collectionwere removed. UseisPulling()/isPushing()/isLoading()reactive helpers. createMemoryAdapterand thememoryoption were removed.- Readiness API changed:
.isReady()(promise) was renamed to.ready(). A new reactive.isReady()getter was added. - Collection constructor: prefer
new Collection(name, dataAdapter, options?); deprecatedpersistenceoption is still accepted and wrapped byDefaultDataAdapter.
Core CRUD API (async)
All write operations are asynchronous. Update your call sites to await them and adapt to the new return values.
Before (v1):
const id = Posts.insert({ title: 'Hello' })
Posts.updateOne({ id }, { $set: { title: 'Hi' } })
Posts.removeOne({ id })After (v2):
const id = await Posts.insert({ title: 'Hello' }) // returns the inserted ID
await Posts.updateOne({ id }, { $set: { title: 'Hi' } }) // resolves to number of updated items
await Posts.removeOne({ id }) // resolves to number of removed itemsReturn values in v2:
insert(item)→Promise<I>(inserted ID)insertMany(items)→Promise<I[]>(inserted IDs)updateOne(...)→Promise<number>(0 or 1)updateMany(...)→Promise<number>replaceOne(...)→Promise<number>(0 or 1)removeOne(...)→Promise<number>(0 or 1)removeMany(...)→Promise<number>
Readiness and Loading
- Promise-based readiness:
await collection.ready()replacesawait collection.isReady(). - Reactive readiness:
collection.isReady()now returns a reactive boolean. - Loading states:
collection.isPulling()andcollection.isPushing()are reactive booleans.collection.isLoading()is reactive and true if pulling or pushing. It starts asfalseby default.
Before (v1):
await collection.isReady()After (v2):
await collection.ready()
// reactive checks (in a reactive context)
collection.isReady()
collection.isLoading() // or isPulling()/isPushing()Indices Configuration
Indices are now defined by field names directly.
Before (v1):
import { createIndex } from '@signaldb/core'
const Posts = new Collection({
indices: [
createIndex('title'),
createIndex('author.id'),
],
})After (v2):
const Posts = new Collection({
indices: ['title', 'author.id'],
})Remove any usages of createIndex and createIndexProvider. Use string field paths instead (dot-notation supported).
Storage vs. Persistence
The persistence layer has been renamed to Storage and modernized.
PersistenceAdapter→StorageAdapter(new API).createPersistenceAdapter→createStorageAdapter.combinePersistenceAdapterswas removed.- Persistence-related
Collectionevents were removed. Use loading helpers (isPulling/isPushing/isLoading) when you need UI feedback.
New StorageAdapter API surface:
interface StorageAdapter<T extends { id: I }, I> {
// lifecycle
setup(): Promise<void>
teardown(): Promise<void>
// reads
readAll(): Promise<T[]>
readIds(ids: I[]): Promise<T[]>
// indices
createIndex(field: string): Promise<void>
dropIndex(field: string): Promise<void>
readIndex(field: string): Promise<Map<any, Set<I>>>
// writes
insert(items: T[]): Promise<void>
replace(items: T[]): Promise<void>
remove(items: T[]): Promise<void>
removeAll(): Promise<void>
}Migration tips from old PersistenceAdapter:
- Move one-time initialization into
setup(), cleanup intoteardown(). - Replace “load everything” with
readAll(); selective lookups go throughreadIds(). - Replace “save/patch” with explicit
insert,replace,remove, andremoveAlloperations. - Provide index operations (
createIndex,dropIndex,readIndex) if your storage can accelerate queries (dot-notation field names are passed in).
Minimal in-memory example for testing:
import { createStorageAdapter } from '@signaldb/core'
type Item = { id: string; [k: string]: any }
export const memoryStorage = () => {
let items: Item[] = []
return createStorageAdapter<Item, string>({
async setup() {},
async teardown() {},
async readAll() { return items },
async readIds(ids) { return items.filter(i => ids.includes(i.id)) },
async createIndex(field) { /* no-op for memory */ },
async dropIndex(field) { /* no-op */ },
async readIndex(field) { return new Map() },
async insert(newItems) { items = [...items, ...newItems] },
async replace(newItems) {
const byId = new Map(items.map(i => [i.id, i]))
for (const it of newItems) byId.set(it.id, it)
items = [...byId.values()]
},
async remove(toRemove) {
const ids = new Set(toRemove.map(i => i.id))
items = items.filter(i => !ids.has(i.id))
},
async removeAll() { items = [] },
})
}Note: For simple upgrades, you can still pass the deprecated persistence option to the Collection constructor; v2 wraps it with DefaultDataAdapter. Prefer the explicit DataAdapter + StorageAdapter setup for new code.
New DataAdapter Layer
v2 introduces a DataAdapter abstraction to separate collection behavior from storage mechanics and to enable advanced scenarios.
DefaultDataAdapter: simple, standard choice that works with aStorageAdapter.AsyncDataAdapter: async-first flow with explicit storage setup.WorkerDataAdapter/WorkerDataAdapterHost: run data operations in a Web Worker.AutoFetchDataAdapter: replacement forAutoFetchCollection(if you previously relied on it).
Standard setup with DefaultDataAdapter (recommended baseline):
import { Collection, DefaultDataAdapter } from '@signaldb/core'
const dataAdapter = new DefaultDataAdapter({
storage: (name) => myStorageFor(name), // returns a StorageAdapter for the given collection name
})
const Posts = new Collection('posts', dataAdapter, {
indices: ['id', 'authorId', 'createdAt'],
})If you previously used AutoFetchCollection, migrate to AutoFetchDataAdapter. Keep the same Collection constructor pattern shown above but swap the adapter class.
Collection Constructor Changes
The Collection constructor now supports two forms:
new Collection(options?)(legacy-compatible). The deprecatedpersistenceoption still works and is wrapped byDefaultDataAdapter.new Collection(name, dataAdapter, options?)(recommended): pass a collection name and aDataAdapterinstance explicitly.
Example migrating from v1: Before (v1):
const Posts = new Collection({
name: 'posts',
persistence: /* old adapter */
})After (v2):
import { DefaultDataAdapter } from '@signaldb/core'
const dataAdapter = new DefaultDataAdapter({
storage: (name) => myStorageFor(name),
})
const Posts = new Collection('posts', dataAdapter, {
indices: ['title']
})Removed Memory Adapter
createMemoryAdapter and the memory option were removed. For tests or ephemeral storage, implement a trivial in-memory StorageAdapter with createStorageAdapter that keeps items in a local array or Map.
Event Changes
All persistence-level events on Collection were removed. Use these instead:
collection.isPulling()/collection.isPushing()/collection.isLoading()(reactive)- Existing CRUD events remain:
added,changed,removedand their corresponding action events (insert,updateOne,updateMany,replaceOne,removeOne,removeMany).
Migration Checklist
- Update all write calls to
awaitthe new async methods and handle new return values. - Replace index providers with
indices: string[]. - Rename persistence APIs to storage (
createStorageAdapter,StorageAdapter), removecombinePersistenceAdapters. - Replace
AutoFetchCollectionwithAutoFetchDataAdapter. - Switch
await collection.isReady()toawait collection.ready()and use reactivecollection.isReady()where needed. - Replace persistence events with
isPulling/isPushing/isLoading. - Remove
createMemoryAdapterand thememoryoption; implement an in-memoryStorageAdapterif necessary.
If you run into something not covered here, see the changelog for @signaldb/core and the API reference, or open a discussion/issue.