Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/reference/core/collection/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ Enables or disables field tracking for the collection. See [Field-Level Reactivi
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:

* `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 is passed the modified item.
* `changed`: Fired when an existing item in the collection undergoes modification. The event handler is passed both the modified item and the item before modification.
* `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.

Expand Down
2 changes: 1 addition & 1 deletion docs/reference/core/cursor/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ This method allows observation of changes in the cursor items. It uses callbacks
* `callbacks`: An object of Callback functions for different observation events.
* `added(item: T)`gets called when a new item was added to the cursor
* `addedBefore(item: T, before: T)`gets called when a new item was added to the cursor and also indicates the position of the new item
* `changed(item: T)`gets called when an item in the cursor was changed
* `changed(item: T, before: T)`gets called when an item in the cursor was changed, passing the state after and before the change
* `movedBefore(item: T, before: T)`gets called when an item moved its position in the cursor
* `removed(item: T)`gets called when an item was removed from the cursor
* `skipInitial`: A boolean to decide whether to skip the initial observation event.
Expand Down
2 changes: 2 additions & 0 deletions packages/base/core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

* Added the previous state (before the update) as an argument to `'changed'` event handlers. This provides additional information to handlers, enabling e.g. history functionality.

## [1.8.1] - 2026-03-17

### Fixed
Expand Down
8 changes: 4 additions & 4 deletions packages/base/core/__tests__/Collection.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ describe('Collection', () => {

collection.updateOne({ id: '1' }, { $set: { name: 'Jane' } })

expect(eventHandler).toHaveBeenCalledWith({ id: '1', name: 'Jane' }, { $set: { name: 'Jane' } })
expect(eventHandler).toHaveBeenCalledWith({ id: '1', name: 'Jane' }, { $set: { name: 'Jane' } }, { id: '1', name: 'John' })
})

it('should not throw an error if no item matches the selector', () => {
Expand Down Expand Up @@ -272,8 +272,8 @@ describe('Collection', () => {
collection.updateMany({ name: 'John' }, { $set: { name: 'Jane' } })

expect(eventHandler).toHaveBeenCalledTimes(2)
expect(eventHandler).toHaveBeenCalledWith({ id: '1', name: 'Jane' }, { $set: { name: 'Jane' } })
expect(eventHandler).toHaveBeenCalledWith({ id: '3', name: 'Jane' }, { $set: { name: 'Jane' } })
expect(eventHandler).toHaveBeenCalledWith({ id: '1', name: 'Jane' }, { $set: { name: 'Jane' } }, { id: '1', name: 'John' })
expect(eventHandler).toHaveBeenCalledWith({ id: '3', name: 'Jane' }, { $set: { name: 'Jane' } }, { id: '3', name: 'John' })
})

it('should throw an error if trying to update the item id to a value that already exists', () => {
Expand Down Expand Up @@ -312,7 +312,7 @@ describe('Collection', () => {

collection.replaceOne({ id: '1' }, { name: 'Jack' })

expect(eventHandler).toHaveBeenCalledWith({ id: '1', name: 'Jack' }, { name: 'Jack' })
expect(eventHandler).toHaveBeenCalledWith({ id: '1', name: 'Jack' }, { name: 'Jack' }, { id: '1', name: 'John' })
})

it('should not throw an error if no item matches the selector', () => {
Expand Down
15 changes: 12 additions & 3 deletions packages/base/core/__tests__/Cursor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,10 @@ describe('Cursor', () => {
await wait() // Wait for all operations to finish
expect(callbacks.added).not.toHaveBeenCalled()
expect(callbacks.addedBefore).not.toHaveBeenCalled()
expect(callbacks.changed).toHaveBeenCalledWith(expect.objectContaining({ id: 1, name: 'item1_modified' }))
expect(callbacks.changed).toHaveBeenCalledWith(
expect.objectContaining({ id: 1, name: 'item1_modified' }),
expect.objectContaining({ id: 1, name: 'Item 1' }),
)
expect(callbacks.movedBefore).not.toHaveBeenCalled()
expect(callbacks.removed).not.toHaveBeenCalled()
})
Expand Down Expand Up @@ -289,7 +292,10 @@ describe('Cursor', () => {
await wait() // Wait for all operations to finish
expect(callbacks.added).not.toHaveBeenCalled()
expect(callbacks.addedBefore).not.toHaveBeenCalled()
expect(callbacks.changed).toHaveBeenCalledWith(expect.objectContaining({ id: 2, name: 'Item 30' }))
expect(callbacks.changed).toHaveBeenCalledWith(
expect.objectContaining({ id: 2, name: 'Item 30' }),
expect.objectContaining({ id: 2, name: 'Item 2' }),
)
expect(callbacks.movedBefore).toHaveBeenCalledWith(
expect.objectContaining({ id: 2, name: 'Item 30' }),
null,
Expand Down Expand Up @@ -357,7 +363,10 @@ describe('Cursor', () => {
await new Promise((resolve) => {
setTimeout(resolve, 0)
})
expect(callbacks.changed).toHaveBeenCalledWith(expect.objectContaining({ id: 1, name: 'item1_modified' }))
expect(callbacks.changed).toHaveBeenCalledWith(
expect.objectContaining({ id: 1, name: 'item1_modified' }),
expect.objectContaining({ id: 1, name: 'Item 1' }),
)

col.updateOne({ id: 1 }, { $set: { count: 42 } }) // Move existing item
await new Promise((resolve) => {
Expand Down
4 changes: 2 additions & 2 deletions packages/base/core/src/Collection/Observer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import uniqueBy from '../utils/uniqueBy'

type AddedCallback<T> = (item: T) => void
type AddedBeforeCallback<T> = (item: T, before: T) => void
type ChangedCallback<T> = (item: T) => void
type ChangedCallback<T> = (item: T, before: T) => void
type ChangedFieldCallback<T> = <Field extends keyof T>(
item: T,
field: Field,
Expand Down Expand Up @@ -122,7 +122,7 @@ export default class Observer<T extends { id: any }> {
if (newItem) {
if (this.hasCallbacks(['changed', 'changedField']) // If the item exists but has changed, call 'changed' callback
&& !isEqual(newItem.item, oldItem)) {
this.call('changed', newItem.item)
this.call('changed', newItem.item, oldItem)

if (this.hasCallbacks(['changedField'])) {
// check for changed fields and call 'changedField' callback
Expand Down
10 changes: 5 additions & 5 deletions packages/base/core/src/Collection/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export interface CollectionOptions<T extends BaseItem<I>, I, E extends BaseItem

interface CollectionEvents<T extends BaseItem, E extends BaseItem = T, U = E> {
'added': (item: T) => void,
'changed': (item: T, modifier: Modifier<T>) => void,
'changed': (itemAfter: T, modifier: Modifier<T>, itemBefore: T) => void,
'removed': (item: T) => void,

'persistence.init': () => void,
Expand Down Expand Up @@ -839,7 +839,7 @@ export default class Collection<
this.emit('validate', modifiedItem)
this.memory().splice(index, 1, modifiedItem)
this.rebuildIndices()
this.emit('changed', modifiedItem, restModifier)
this.emit('changed', modifiedItem, restModifier, item)
}
this.emit('updateOne', selector, modifier)
this.executeInDebugMode(callstack => this.emit('_debug.updateOne', callstack, selector, modifier))
Expand Down Expand Up @@ -900,8 +900,8 @@ export default class Collection<
this.memory().splice(index, 1, item)
})
this.rebuildIndices()
changes.forEach(({ item }) => {
this.emit('changed', item, restModifier)
changes.forEach(({ item: changedItem }, changeIndex) => {
this.emit('changed', changedItem, restModifier, items[changeIndex])
})
this.emit('updateMany', selector, modifier)
this.executeInDebugMode(callstack => this.emit('_debug.updateMany', callstack, selector, modifier))
Expand Down Expand Up @@ -944,7 +944,7 @@ export default class Collection<
this.emit('validate', modifiedItem)
this.memory().splice(index, 1, modifiedItem)
this.rebuildIndices()
this.emit('changed', modifiedItem, replacement as Modifier<T>)
this.emit('changed', modifiedItem, replacement as Modifier<T>, item)
}
this.emit('replaceOne', selector, replacement)
this.executeInDebugMode(callstack => this.emit('_debug.replaceOne', callstack, selector, replacement))
Expand Down
Loading