From c513d5a8a247b83e44e37903313da67d5f25cef8 Mon Sep 17 00:00:00 2001 From: Ben Briggs Date: Thu, 20 Mar 2025 13:32:55 -0500 Subject: [PATCH 01/10] Refresh Cache Based on Query --- src/Collection/index.d.ts | 10 ++++++---- src/Collection/index.js | 21 +++++++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/Collection/index.d.ts b/src/Collection/index.d.ts index af47bb402..a2bd2b5d8 100644 --- a/src/Collection/index.d.ts +++ b/src/Collection/index.d.ts @@ -1,16 +1,16 @@ // @flow -import { Observable, Subject } from '../utils/rx' -import type { ResultCallback } from '../utils/fp/Result' import type { ArrayOrSpreadFn } from '../utils/fp' +import type { ResultCallback } from '../utils/fp/Result' +import { Observable, Subject } from '../utils/rx' import type { Unsubscribe } from '../utils/subscriptions' -import Query from '../Query' import type Database from '../Database' import type Model from '../Model' import type { RecordId } from '../Model' +import Query from '../Query' import type { Clause } from '../QueryDescription' -import type { TableName, TableSchema } from '../Schema' import { DirtyRaw } from '../RawRecord' +import type { TableName, TableSchema } from '../Schema' import RecordCache from './RecordCache' @@ -69,6 +69,8 @@ export default class Collection { // This is useful when you're adding online-only features to an otherwise offline-first app disposableFromDirtyRaw(dirtyRaw: DirtyRaw): Record + refreshCache(clauses: Clause[]): Promise> + // *** Implementation details *** get table(): TableName diff --git a/src/Collection/index.js b/src/Collection/index.js index 5b8405e51..ceff595b7 100644 --- a/src/Collection/index.js +++ b/src/Collection/index.js @@ -194,6 +194,27 @@ export default class Collection { return this.modelClass._disposableFromDirtyRaw(this, dirtyRaw) } + refreshCache(clauses: Clause[]): Promise> { + return new Promise>((resolve) => { + this._unsafeFetchRaw(new Query(this, clauses), (results) => { + const updateCacheOperations: CollectionChangeSet = []; + const notifySubscribersOperations: CollectionChangeSet = []; + + results.value?.map(rawRecord => { + const record = this._cache.recordInsantiator(rawRecord) + + updateCacheOperations.push({ record, type: "created"}) + notifySubscribersOperations.push({ record, type: record._raw._status }) + }) + + this._applyChangesToCache(updateCacheOperations) + this._notify(notifySubscribersOperations) + + resolve(notifySubscribersOperations) + }) + }) + } + // *** Implementation details *** // See: Query.fetch From f19fab8b3a2e15ac6f3007b57e9da6e54fbe84cb Mon Sep 17 00:00:00 2001 From: Ben Briggs Date: Thu, 20 Mar 2025 13:34:59 -0500 Subject: [PATCH 02/10] Reverting import order to original --- src/Collection/index.d.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Collection/index.d.ts b/src/Collection/index.d.ts index a2bd2b5d8..d9a80dee4 100644 --- a/src/Collection/index.d.ts +++ b/src/Collection/index.d.ts @@ -1,16 +1,16 @@ // @flow -import type { ArrayOrSpreadFn } from '../utils/fp' -import type { ResultCallback } from '../utils/fp/Result' import { Observable, Subject } from '../utils/rx' +import type { ResultCallback } from '../utils/fp/Result' +import type { ArrayOrSpreadFn } from '../utils/fp' import type { Unsubscribe } from '../utils/subscriptions' +import Query from '../Query' import type Database from '../Database' import type Model from '../Model' import type { RecordId } from '../Model' -import Query from '../Query' import type { Clause } from '../QueryDescription' -import { DirtyRaw } from '../RawRecord' import type { TableName, TableSchema } from '../Schema' +import { DirtyRaw } from '../RawRecord' import RecordCache from './RecordCache' From 557cf0563cd9a1d088c1ff911ae2fb4685515e4a Mon Sep 17 00:00:00 2001 From: Ben Briggs Date: Mon, 24 Mar 2025 16:02:34 -0500 Subject: [PATCH 03/10] Sanitizing raw record --- src/Collection/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Collection/index.js b/src/Collection/index.js index ceff595b7..6bddc5a22 100644 --- a/src/Collection/index.js +++ b/src/Collection/index.js @@ -15,7 +15,7 @@ import type Database from '../Database' import type Model, { RecordId } from '../Model' import type { Clause } from '../QueryDescription' import { type TableName, type TableSchema } from '../Schema' -import { type DirtyRaw } from '../RawRecord' +import { type DirtyRaw, sanitizedRaw } from "../RawRecord"; import RecordCache from './RecordCache' @@ -201,8 +201,10 @@ export default class Collection { const notifySubscribersOperations: CollectionChangeSet = []; results.value?.map(rawRecord => { + rawRecord = sanitizedRaw(rawRecord, this.schema) const record = this._cache.recordInsantiator(rawRecord) + this._cache.delete(record); updateCacheOperations.push({ record, type: "created"}) notifySubscribersOperations.push({ record, type: record._raw._status }) }) From 9531da9cccd3cc27a51efdb57620371a585d09b5 Mon Sep 17 00:00:00 2001 From: Ben Briggs Date: Wed, 26 Mar 2025 15:58:25 -0500 Subject: [PATCH 04/10] Reducing diff --- src/Collection/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Collection/index.js b/src/Collection/index.js index 6bddc5a22..6092d1950 100644 --- a/src/Collection/index.js +++ b/src/Collection/index.js @@ -15,7 +15,7 @@ import type Database from '../Database' import type Model, { RecordId } from '../Model' import type { Clause } from '../QueryDescription' import { type TableName, type TableSchema } from '../Schema' -import { type DirtyRaw, sanitizedRaw } from "../RawRecord"; +import { type DirtyRaw, sanitizedRaw } from '../RawRecord' import RecordCache from './RecordCache' From 7abc40bf99c934db59dfbaa054ec57acc137011a Mon Sep 17 00:00:00 2001 From: Ben Briggs Date: Fri, 28 Mar 2025 08:18:18 -0500 Subject: [PATCH 05/10] Cleaning up --- src/Collection/index.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Collection/index.js b/src/Collection/index.js index 6092d1950..f876562fc 100644 --- a/src/Collection/index.js +++ b/src/Collection/index.js @@ -205,8 +205,10 @@ export default class Collection { const record = this._cache.recordInsantiator(rawRecord) this._cache.delete(record); - updateCacheOperations.push({ record, type: "created"}) - notifySubscribersOperations.push({ record, type: record._raw._status }) + updateCacheOperations.push({ record, type: 'created'}) + if (record._raw._status === 'created' || record._raw._status === 'updated' || record._raw._status === 'destroyed') { + notifySubscribersOperations.push({ record, type: record._raw._status }) + } }) this._applyChangesToCache(updateCacheOperations) From e9c5c3e592d6f4a5abcb2043fae1dcffb8abe700 Mon Sep 17 00:00:00 2001 From: Ben Briggs Date: Fri, 28 Mar 2025 08:46:12 -0500 Subject: [PATCH 06/10] Ran prettier --- src/Collection/index.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/Collection/index.js b/src/Collection/index.js index f876562fc..7bf2a4c08 100644 --- a/src/Collection/index.js +++ b/src/Collection/index.js @@ -197,16 +197,20 @@ export default class Collection { refreshCache(clauses: Clause[]): Promise> { return new Promise>((resolve) => { this._unsafeFetchRaw(new Query(this, clauses), (results) => { - const updateCacheOperations: CollectionChangeSet = []; - const notifySubscribersOperations: CollectionChangeSet = []; + const updateCacheOperations: CollectionChangeSet = [] + const notifySubscribersOperations: CollectionChangeSet = [] - results.value?.map(rawRecord => { + results.value?.map((rawRecord) => { rawRecord = sanitizedRaw(rawRecord, this.schema) const record = this._cache.recordInsantiator(rawRecord) - this._cache.delete(record); - updateCacheOperations.push({ record, type: 'created'}) - if (record._raw._status === 'created' || record._raw._status === 'updated' || record._raw._status === 'destroyed') { + this._cache.delete(record) + updateCacheOperations.push({ record, type: 'created' }) + if ( + record._raw._status === 'created' || + record._raw._status === 'updated' || + record._raw._status === 'destroyed' + ) { notifySubscribersOperations.push({ record, type: record._raw._status }) } }) From 202cc88ff0567cee08bf881dadb78b2d6591bdba Mon Sep 17 00:00:00 2001 From: Ben Briggs Date: Fri, 28 Mar 2025 08:49:18 -0500 Subject: [PATCH 07/10] Added CHANGELOG entry --- CHANGELOG-Unreleased.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG-Unreleased.md b/CHANGELOG-Unreleased.md index 394dd86b1..780bfdb83 100644 --- a/CHANGELOG-Unreleased.md +++ b/CHANGELOG-Unreleased.md @@ -11,6 +11,7 @@ - Added `Database#experimentalIsVerbose` option - Support for React Native 0.74+ +- Added ability to refresh collection cache using results of query ### Fixes From b2ce735293f997100a30b830b0cbca6123392534 Mon Sep 17 00:00:00 2001 From: Ben Briggs Date: Fri, 28 Mar 2025 10:27:23 -0500 Subject: [PATCH 08/10] Add test --- src/Collection/test.js | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/Collection/test.js b/src/Collection/test.js index 3607aab9b..d1197589d 100644 --- a/src/Collection/test.js +++ b/src/Collection/test.js @@ -355,3 +355,41 @@ describe('Collection observation', () => { expect(subscriber).toHaveBeenCalledTimes(4) }) }) + +describe('refresh cache', () => { + it('refreshes cache using data from database', async () => { + const { db, comments } = mockDatabase() + + // Create a new record + let targetID = null + await db.write(async () => { + const newComment = await comments.create((r) => { + r.body = 'comment body' + }) + + targetID = newComment.id + }) + + // Confirm the value was persisted + const originalComment = await comments.find(targetID) + expect(originalComment.body).toBe('comment body') + + // Change the value by accessing the DB driver directly + db.adapter.underlyingAdapter._driver.loki + .getCollection('mock_comments') + .findAndUpdate({ id: targetID }, (c) => { + c.body = 'updated comment body' + }) + + // Confirm the cache is stale + const staleComment = await comments.find(targetID) + expect(staleComment.body).toBe('comment body') + + // Refresh the cache + await comments.refreshCache([Q.where('id', targetID)]) + + // Confirm the cache has been updated + const refreshedComment = await comments.find(targetID) + expect(refreshedComment.body).toBe('updated comment body') + }) +}) From 9c871c0fbefc90824a930170c3cfb6c3342a05b7 Mon Sep 17 00:00:00 2001 From: Ben Briggs Date: Fri, 28 Mar 2025 10:32:22 -0500 Subject: [PATCH 09/10] Added doc comments --- src/Collection/index.d.ts | 19 +++++++++++++++---- src/Collection/index.js | 11 +++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/Collection/index.d.ts b/src/Collection/index.d.ts index d9a80dee4..84cfef032 100644 --- a/src/Collection/index.d.ts +++ b/src/Collection/index.d.ts @@ -1,16 +1,16 @@ // @flow -import { Observable, Subject } from '../utils/rx' -import type { ResultCallback } from '../utils/fp/Result' import type { ArrayOrSpreadFn } from '../utils/fp' +import type { ResultCallback } from '../utils/fp/Result' +import { Observable, Subject } from '../utils/rx' import type { Unsubscribe } from '../utils/subscriptions' -import Query from '../Query' import type Database from '../Database' import type Model from '../Model' import type { RecordId } from '../Model' +import Query from '../Query' import type { Clause } from '../QueryDescription' -import type { TableName, TableSchema } from '../Schema' import { DirtyRaw } from '../RawRecord' +import type { TableName, TableSchema } from '../Schema' import RecordCache from './RecordCache' @@ -69,6 +69,17 @@ export default class Collection { // This is useful when you're adding online-only features to an otherwise offline-first app disposableFromDirtyRaw(dirtyRaw: DirtyRaw): Record + /** + * Executes the provided query against the database and uses the results to + * refresh the internal cache. + * + * Note: This is only required when changes were made outside of WatermelonDB. + * + * Any observers of the affected data will be notified of the change. + * + * Returns a collection of modified records that were sent as notifications to + * subscribers. + */ refreshCache(clauses: Clause[]): Promise> // *** Implementation details *** diff --git a/src/Collection/index.js b/src/Collection/index.js index 7bf2a4c08..276d3e8f7 100644 --- a/src/Collection/index.js +++ b/src/Collection/index.js @@ -194,6 +194,17 @@ export default class Collection { return this.modelClass._disposableFromDirtyRaw(this, dirtyRaw) } + /** + * Executes the provided query against the database and uses the results to + * refresh the internal cache. + * + * Note: This is only required when changes were made outside of WatermelonDB. + * + * Any observers of the affected data will be notified of the change. + * + * Returns a collection of modified records that were sent as notifications to + * subscribers. + */ refreshCache(clauses: Clause[]): Promise> { return new Promise>((resolve) => { this._unsafeFetchRaw(new Query(this, clauses), (results) => { From 8db843bd92cfe8aef3779862b1ab1ddbcba4f893 Mon Sep 17 00:00:00 2001 From: Ben Briggs Date: Fri, 28 Mar 2025 10:35:04 -0500 Subject: [PATCH 10/10] Reverting automatic import sorting --- src/Collection/index.d.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Collection/index.d.ts b/src/Collection/index.d.ts index 84cfef032..efee6222e 100644 --- a/src/Collection/index.d.ts +++ b/src/Collection/index.d.ts @@ -1,16 +1,16 @@ // @flow -import type { ArrayOrSpreadFn } from '../utils/fp' -import type { ResultCallback } from '../utils/fp/Result' import { Observable, Subject } from '../utils/rx' +import type { ResultCallback } from '../utils/fp/Result' +import type { ArrayOrSpreadFn } from '../utils/fp' import type { Unsubscribe } from '../utils/subscriptions' +import Query from '../Query' import type Database from '../Database' import type Model from '../Model' import type { RecordId } from '../Model' -import Query from '../Query' import type { Clause } from '../QueryDescription' -import { DirtyRaw } from '../RawRecord' import type { TableName, TableSchema } from '../Schema' +import { DirtyRaw } from '../RawRecord' import RecordCache from './RecordCache'