From e300449521e4ef552504f476bd0e7a9cc153a84d Mon Sep 17 00:00:00 2001 From: Huelsenfrucht Date: Mon, 9 Mar 2026 12:56:54 +0500 Subject: [PATCH 1/4] feat: report previous values onChanged --- packages/base/core/src/Collection/Observer.ts | 4 ++-- packages/base/core/src/Collection/index.ts | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/base/core/src/Collection/Observer.ts b/packages/base/core/src/Collection/Observer.ts index a7b161bc2..566854d0e 100644 --- a/packages/base/core/src/Collection/Observer.ts +++ b/packages/base/core/src/Collection/Observer.ts @@ -3,7 +3,7 @@ import uniqueBy from '../utils/uniqueBy' type AddedCallback = (item: T) => void type AddedBeforeCallback = (item: T, before: T) => void -type ChangedCallback = (item: T) => void +type ChangedCallback = (oldItem: T, newItem: T) => void type ChangedFieldCallback = ( item: T, field: Field, @@ -122,7 +122,7 @@ export default class Observer { 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 diff --git a/packages/base/core/src/Collection/index.ts b/packages/base/core/src/Collection/index.ts index f29593769..8ecd6aec4 100644 --- a/packages/base/core/src/Collection/index.ts +++ b/packages/base/core/src/Collection/index.ts @@ -40,7 +40,7 @@ export interface CollectionOptions, I, E extends BaseItem interface CollectionEvents { 'added': (item: T) => void, - 'changed': (item: T, modifier: Modifier) => void, + 'changed': (itemAfter: T, modifier: Modifier, itemBefore: T) => void, 'removed': (item: T) => void, 'persistence.init': () => void, @@ -814,7 +814,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)) @@ -875,8 +875,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)) @@ -919,7 +919,7 @@ export default class Collection< this.emit('validate', modifiedItem) this.memory().splice(index, 1, modifiedItem) this.rebuildIndices() - this.emit('changed', modifiedItem, replacement as Modifier) + this.emit('changed', modifiedItem, replacement as Modifier, item) } this.emit('replaceOne', selector, replacement) this.executeInDebugMode(callstack => this.emit('_debug.replaceOne', callstack, selector, replacement)) From 71c83382f2576333d097062fc1dd9682fd58c92b Mon Sep 17 00:00:00 2001 From: Huelsenfrucht Date: Mon, 9 Mar 2026 13:23:40 +0500 Subject: [PATCH 2/4] update tests for new changed event --- packages/base/core/__tests__/Collection.spec.ts | 8 ++++---- packages/base/core/__tests__/Cursor.spec.ts | 15 ++++++++++++--- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/packages/base/core/__tests__/Collection.spec.ts b/packages/base/core/__tests__/Collection.spec.ts index 8825533d2..f940fbae9 100644 --- a/packages/base/core/__tests__/Collection.spec.ts +++ b/packages/base/core/__tests__/Collection.spec.ts @@ -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', () => { @@ -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', () => { @@ -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', () => { diff --git a/packages/base/core/__tests__/Cursor.spec.ts b/packages/base/core/__tests__/Cursor.spec.ts index 8bd56ce40..a973ff9ad 100644 --- a/packages/base/core/__tests__/Cursor.spec.ts +++ b/packages/base/core/__tests__/Cursor.spec.ts @@ -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() }) @@ -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, @@ -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) => { From 88368a3b11d883339fa0720eb2701bf11bfda9ae Mon Sep 17 00:00:00 2001 From: Huelsenfrucht Date: Wed, 1 Apr 2026 12:37:43 +0500 Subject: [PATCH 3/4] fix: correct observer changed callback parameter order --- packages/base/core/src/Collection/Observer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/base/core/src/Collection/Observer.ts b/packages/base/core/src/Collection/Observer.ts index 566854d0e..1154d63f0 100644 --- a/packages/base/core/src/Collection/Observer.ts +++ b/packages/base/core/src/Collection/Observer.ts @@ -3,7 +3,7 @@ import uniqueBy from '../utils/uniqueBy' type AddedCallback = (item: T) => void type AddedBeforeCallback = (item: T, before: T) => void -type ChangedCallback = (oldItem: T, newItem: T) => void +type ChangedCallback = (item: T, before: T) => void type ChangedFieldCallback = ( item: T, field: Field, From da8396d555479bad1b76203d8368e471e0c979c4 Mon Sep 17 00:00:00 2001 From: Huelsenfrucht Date: Wed, 1 Apr 2026 12:51:01 +0500 Subject: [PATCH 4/4] chore: document unchanged item reporting --- docs/reference/core/collection/index.md | 2 +- docs/reference/core/cursor/index.md | 2 +- packages/base/core/CHANGELOG.md | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/reference/core/collection/index.md b/docs/reference/core/collection/index.md index f9d4eef93..31bf611fa 100644 --- a/docs/reference/core/collection/index.md +++ b/docs/reference/core/collection/index.md @@ -173,7 +173,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. diff --git a/docs/reference/core/cursor/index.md b/docs/reference/core/cursor/index.md index 550054211..114c15a14 100644 --- a/docs/reference/core/cursor/index.md +++ b/docs/reference/core/cursor/index.md @@ -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. diff --git a/packages/base/core/CHANGELOG.md b/packages/base/core/CHANGELOG.md index b3d13992a..da3d82457 100644 --- a/packages/base/core/CHANGELOG.md +++ b/packages/base/core/CHANGELOG.md @@ -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. + * Introduced the `transformAll` option when creating a `Collection`. This allows you to define a function that transform items after they are retrieved from persistence, enabling the integration of data from other collections or external sources (thanks @signalize!) ## [1.7.2] - 2026-01-07