Skip to content

Commit

Permalink
feat: add ability to batch changes instead of delaying them
Browse files Browse the repository at this point in the history
  • Loading branch information
maxnowack committed Sep 10, 2024
1 parent d368584 commit bceedc6
Show file tree
Hide file tree
Showing 5 changed files with 64 additions and 161 deletions.
12 changes: 12 additions & 0 deletions docs/collections/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,18 @@ Parameters

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 as the index would be rebuild on every change to the collection. To prevent this, you can use the `.batch()` method. This method will execute all operations inside the callback without rebuilding the index on every change. If you need to batch updates of multiple collections, you can use the global `Collection.batch()` method.

```js
collection.batch(() => {
collection.insert({ name: 'Item 1' })
collection.insert({ name: 'Item 2' })
//
})
```

## Events

The Collection class is equipped with a set of events that provide insights into the state and changes within the collection. These events, emitted by the class, can be crucial for implementing reactive behaviors and persistence management. Here is an overview of the events:
Expand Down
39 changes: 24 additions & 15 deletions packages/signaldb/__tests__/Collection.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -441,16 +441,15 @@ describe('Collection', () => {
return performance.now() - start
}

it('should be faster with id only queries', async () => {
it('should be faster with id only queries', () => {
const col = new Collection<{ id: string, name: string, num: number }>()

// create items
for (let i = 0; i < 1000; i += 1) {
col.insert({ id: i.toString(), name: 'John', num: i })
}

// wait for the next tick to ensure the indices are ready
await new Promise((resolve) => { setTimeout(resolve, 0) })
col.batch(() => {
for (let i = 0; i < 1000; i += 1) {
col.insert({ id: i.toString(), name: 'John', num: i })
}
})

const idQueryTime = measureTime(() => {
const item = col.findOne({ id: '999' })
Expand All @@ -470,19 +469,19 @@ describe('Collection', () => {
expect(percentage).toBeLessThan(10)
})

it('should be faster with field indices', async () => {
it('should be faster with field indices', () => {
const col1 = new Collection<{ id: string, name: string, num: number }>({
indices: [createIndex('num')],
})
const col2 = new Collection<{ id: string, name: string, num: number }>()

// create items
for (let i = 0; i < 1000; i += 1) {
col1.insert({ id: i.toString(), name: 'John', num: i })
col2.insert({ id: i.toString(), name: 'John', num: i })
}
// wait for the next tick to ensure the indices are ready
await new Promise((resolve) => { setTimeout(resolve, 0) })
Collection.batch(() => {
// create items
for (let i = 0; i < 10000; i += 1) {
col1.insert({ id: i.toString(), name: 'John', num: i })
col2.insert({ id: i.toString(), name: 'John', num: i })
}
})

const indexQueryTime = measureTime(() => {
const item = col1.findOne({ num: 999 })
Expand Down Expand Up @@ -580,5 +579,15 @@ describe('Collection', () => {

expect(col.findOne({ id: '1' })).toEqual({ id: '1', name: 'John' })
})

it('should disable indexing temporarily if indices are outdated', () => {
const col = new Collection<{ id: string, name: string }>()
col.batch(() => {
col.insert({ id: '1', name: 'John' })
col.insert({ id: '2', name: 'Jane' })

expect(col.find().fetch()).toEqual([{ id: '1', name: 'John' }, { id: '2', name: 'Jane' }])
})
})
})
})
41 changes: 28 additions & 13 deletions packages/signaldb/src/Collection/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import isEqual from '../utils/isEqual'
import randomId from '../utils/randomId'
import deepClone from '../utils/deepClone'
import type { Changeset, LoadResponse } from '../types/PersistenceAdapter'
import executeOncePerTick from '../utils/executeOncePerTick'
import serializeValue from '../utils/serializeValue'
import type Signal from '../types/Signal'
import createSignal from '../utils/createSignal'
Expand Down Expand Up @@ -109,13 +108,21 @@ function applyUpdates<T extends BaseItem<I> = BaseItem, I = any>(
export default class Collection<T extends BaseItem<I> = BaseItem, I = any, U = T> extends EventEmitter<CollectionEvents<T, U>> {
static collections: Collection<any, any>[] = []
static debugMode = false
static batchOperationInProgress = false
static enableDebugMode = () => {
Collection.debugMode = true
Collection.collections.forEach((collection) => {
collection.setDebugMode(true)
})
}

static batch(callback: () => void) {
Collection.batchOperationInProgress = true
Collection.collections.reduce((memo, collection) => () =>
collection.batch(() => memo()), callback)()
Collection.batchOperationInProgress = false
}

private options: CollectionOptions<T, I, U>
private persistenceAdapter: PersistenceAdapter<T, I> | null = null
private isPullingSignal: Signal<boolean>
Expand All @@ -124,6 +131,7 @@ export default class Collection<T extends BaseItem<I> = BaseItem, I = any, U = T
private indicesOutdated = false
private idIndex = new Map<string, Set<number>>()
private debugMode
private batchOperationInProgress = false

constructor(options?: CollectionOptions<T, I, U>) {
super()
Expand Down Expand Up @@ -334,11 +342,10 @@ export default class Collection<T extends BaseItem<I> = BaseItem, I = any, U = T

private rebuildIndices() {
this.indicesOutdated = true
this.rebuildIndicesOncePerTick()
if (this.batchOperationInProgress) return
this.rebuildAllIndices()
}

private rebuildIndicesOncePerTick = executeOncePerTick(this.rebuildAllIndices.bind(this))

private rebuildAllIndices() {
this.idIndex.clear()
// eslint-disable-next-line array-callback-return
Expand Down Expand Up @@ -410,17 +417,16 @@ export default class Collection<T extends BaseItem<I> = BaseItem, I = any, U = T
...options,
transform: this.transform.bind(this),
bindEvents: (requery) => {
const requeryOnce = executeOncePerTick(requery, true)
this.addListener('persistence.received', requeryOnce)
this.addListener('added', requeryOnce)
this.addListener('changed', requeryOnce)
this.addListener('removed', requeryOnce)
this.addListener('persistence.received', requery)
this.addListener('added', requery)
this.addListener('changed', requery)
this.addListener('removed', requery)
this.emit('observer.created', selector, options)
return () => {
this.removeListener('persistence.received', requeryOnce)
this.removeListener('added', requeryOnce)
this.removeListener('changed', requeryOnce)
this.removeListener('removed', requeryOnce)
this.removeListener('persistence.received', requery)
this.removeListener('added', requery)
this.removeListener('changed', requery)
this.removeListener('removed', requery)
this.emit('observer.disposed', selector, options)
}
},
Expand All @@ -441,6 +447,15 @@ export default class Collection<T extends BaseItem<I> = BaseItem, I = any, U = T
return returnValue
}

public batch(callback: () => void) {
this.batchOperationInProgress = true
callback()
this.batchOperationInProgress = false

// do stuff that wasn't executed during the batch operation
this.rebuildAllIndices()
}

public insert(item: Omit<T, 'id'> & Partial<Pick<T, 'id'>>) {
if (!item) throw new Error('Invalid item')
const newItem = { id: randomId(), ...item } as T
Expand Down
104 changes: 0 additions & 104 deletions packages/signaldb/src/utils/executeOncePerTick.spec.ts

This file was deleted.

29 changes: 0 additions & 29 deletions packages/signaldb/src/utils/executeOncePerTick.ts

This file was deleted.

0 comments on commit bceedc6

Please sign in to comment.