Repository: maxnowack/signaldb
Pull request #1302 (2025-01-13 19:20:25)

feat: add isReady method to Collection class

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/maxnowack/signaldb/pull/1302?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Max+Nowack) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 100.00%. Comparing base [(`befcd65`)](https://app.codecov.io/gh/maxnowack/signaldb/commit/befcd65510c862874ab89f2b5af1f16e1bbafa8f?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Max+Nowack) to head [(`2485653`)](https://app.codecov.io/gh/maxnowack/signaldb/commit/2485653a36744b386e8457b88bba3df4cb79a416?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Max+Nowack).

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff            @@
##              main     #1302   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           56        56           
  Lines         1734      1712   -22     
  Branches       389       385    -4     
=========================================
- Hits          1734      1712   -22     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/maxnowack/signaldb/pull/1302?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Max+Nowack).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Max+Nowack).


Patch:
diff --git a/docs/reference/core/collection/index.md b/docs/reference/core/collection/index.md
index 3953def..c6cc33e 100644
--- a/docs/reference/core/collection/index.md
+++ b/docs/reference/core/collection/index.md
@@ -75,6 +75,20 @@ Parameters
 
 ## Methods
 
+### `isReady()`
+
+Resolves when the persistence adapter finished initializing  and the collection is ready to be used.
+This is useful when you need to wait for the collection to be ready before executing any operations directly after creating it.
+
+Example:
+```ts
+const collection = new Collection({ persistence: /* ... */ })
+await collection.isReady()
+
+collection.insert({ name: 'Item 1' })
+// ...
+```
+
 ### `find(selector?: Selector<T>, options?: Options)`
 
 Returns a new cursor object for the items in the collection that match a given selector and options.
diff --git a/packages/base/core/CHANGELOG.md b/packages/base/core/CHANGELOG.md
index 1a97ff9..35d5531 100644
--- a/packages/base/core/CHANGELOG.md
+++ b/packages/base/core/CHANGELOG.md
@@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Added
 
-* `hasPersistence` method on `Collection` to check if a collection has persistence
+* `isReady` method on `Collection` to wait for the collection to be ready
 
 ### Changed
 
diff --git a/packages/base/core/src/Collection/index.ts b/packages/base/core/src/Collection/index.ts
index ac923ea..c2501c9 100644
--- a/packages/base/core/src/Collection/index.ts
+++ b/packages/base/core/src/Collection/index.ts
@@ -204,6 +204,7 @@ export default class Collection<
   private isDisposed = false
   private postBatchCallbacks = new Set<() => void>()
   private fieldTracking = false
+  private persistenceReadyPromise: Promise<void>
 
   /**
    * Initializes a new instance of the `Collection` class with optional configuration.
@@ -391,15 +392,13 @@ export default class Collection<
           this.emit('persistence.error', error instanceof Error ? error : new Error(error as string))
         })
     }
-    Collection.onCreationCallbacks.forEach(callback => callback(this))
-  }
+    this.persistenceReadyPromise = new Promise<void>((resolve, reject) => {
+      if (!this.persistenceAdapter) return resolve()
+      this.once('persistence.init', resolve)
+      this.once('persistence.error', reject)
+    })
 
-  /**
-   * Checks whether the collection is persisted.
-   * @returns A boolean indicating if the collection is persisted.
-   */
-  public hasPersistence() {
-    return !!this.persistenceAdapter
+    Collection.onCreationCallbacks.forEach(callback => callback(this))
   }
 
   /**
@@ -459,6 +458,23 @@ export default class Collection<
     this.fieldTracking = enable
   }
 
+  /**
+   * Resolves when the persistence adapter finished initializing
+   * and the collection is ready to be used.
+   * @returns A promise that resolves when the collection is ready.
+   * @example
+   * ```ts
+   * const collection = new Collection({
+   *   persistence: // ...
+   * })
+   * await collection.isReady()
+   *
+   * collection.insert({ name: 'Item 1' })
+   */
+  public async isReady() {
+    return this.persistenceReadyPromise
+  }
+
   private profile<ReturnValue>(
     fn: () => ReturnValue,
     measureFunction: (measuredTime: number) => void,
diff --git a/packages/base/sync/src/SyncManager.ts b/packages/base/sync/src/SyncManager.ts
index ce6695c..3462b49 100644
--- a/packages/base/sync/src/SyncManager.ts
+++ b/packages/base/sync/src/SyncManager.ts
@@ -143,30 +143,9 @@ export default class SyncManager<
     this.syncOperations.on('persistence.error', error => syncOperationsPersistence?.handler(error))
 
     this.persistenceReady = Promise.all([
-      new Promise<void>((resolve, reject) => {
-        if (!changesPersistence) {
-          resolve()
-          return
-        }
-        this.syncOperations.once('persistence.error', reject)
-        this.syncOperations.once('persistence.init', resolve)
-      }),
-      new Promise<void>((resolve, reject) => {
-        if (!snapshotsPersistence) {
-          resolve()
-          return
-        }
-        this.changes.once('persistence.error', reject)
-        this.changes.once('persistence.init', resolve)
-      }),
-      new Promise<void>((resolve, reject) => {
-        if (!syncOperationsPersistence) {
-          resolve()
-          return
-        }
-        this.snapshots.once('persistence.error', reject)
-        this.snapshots.once('persistence.init', resolve)
-      }),
+      this.syncOperations.isReady(),
+      this.changes.isReady(),
+      this.snapshots.isReady(),
     ]).then(() => { /* noop */ })
 
     this.changes.setMaxListeners(1000)
@@ -245,21 +224,10 @@ export default class SyncManager<
   ) {
     if (this.isDisposed) throw new Error('SyncManager is disposed')
 
-    const readyPromise = new Promise<void>((resolve, reject) => {
-      if (!collection.hasPersistence()) {
-        resolve()
-        return
-      }
-      collection.once('persistence.error', (error) => {
-        if (this.options.onError) this.options.onError(options, error)
-        reject(error)
-      })
-      collection.once('persistence.init', resolve)
-    })
     this.collections.set(options.name, {
       collection,
       options,
-      readyPromise,
+      readyPromise: collection.isReady(),
       syncPaused: true, // always start paused as the autostart will start it
     })
 


Test patch:
diff --git a/packages/base/core/__tests__/AutoFetchCollection.spec.ts b/packages/base/core/__tests__/AutoFetchCollection.spec.ts
index 7b5baed..7bf4cd5 100644
--- a/packages/base/core/__tests__/AutoFetchCollection.spec.ts
+++ b/packages/base/core/__tests__/AutoFetchCollection.spec.ts
@@ -93,8 +93,7 @@ it('should trigger persistence.error event when fetchQueryItems fails', async ()
   fetchQueryItems.mockRejectedValue(error)
 
   expect(collection.find({}).fetch()).toEqual([])
-
-  await waitForEvent(collection, 'persistence.error')
+  await expect(collection.isReady()).rejects.toThrow(error)
   expect(collection.find({}, { reactive: false }).fetch()).toEqual([])
 })
 
diff --git a/packages/base/core/__tests__/Collection.spec.ts b/packages/base/core/__tests__/Collection.spec.ts
index 987b2bd..62604d0 100644
--- a/packages/base/core/__tests__/Collection.spec.ts
+++ b/packages/base/core/__tests__/Collection.spec.ts
@@ -719,14 +719,19 @@ describe('Collection', () => {
       })
     })
 
-    it('should return if a collection is being persisted or not', () => {
-      const col1 = new Collection<{ id: string, name: string }>()
-      expect(col1.hasPersistence()).toBe(false)
-
-      const col2 = new Collection<{ id: string, name: string }>({
+    it('should wait until a collection is ready', async () => {
+      const col1 = new Collection<{ id: string, name: string }>({
         persistence: memoryPersistenceAdapter(),
       })
-      expect(col2.hasPersistence()).toBe(true)
+      let persistenceInit = false
+      col1.once('persistence.init', () => {
+        persistenceInit = true
+      })
+      await expect(col1.isReady()).resolves.toBeUndefined()
+      expect(persistenceInit).toBe(true)
+
+      const col2 = new Collection<{ id: string, name: string }>()
+      await expect(col2.isReady()).resolves.toBeUndefined()
     })
   })
 })
diff --git a/packages/base/core/__tests__/persistence.spec.ts b/packages/base/core/__tests__/persistence.spec.ts
index 838f580..4600cc9 100644
--- a/packages/base/core/__tests__/persistence.spec.ts
+++ b/packages/base/core/__tests__/persistence.spec.ts
@@ -137,7 +137,7 @@ describe('Persistence', () => {
     })
     const fn = vi.fn()
     collection.on('persistence.error', fn)
-    await waitForEvent(collection, 'persistence.error')
+    await expect(collection.isReady()).rejects.toThrowError('test')
     expect(fn).toHaveBeenCalledWith(new Error('test'))
   })
 
@@ -153,7 +153,7 @@ describe('Persistence', () => {
     })
     const fn = vi.fn()
     collection.on('persistence.error', fn)
-    await waitForEvent(collection, 'persistence.error')
+    await expect(collection.isReady()).rejects.toThrowError('test')
     expect(fn).toHaveBeenCalledWith(new Error('test'))
   })
 
@@ -194,7 +194,7 @@ describe('Persistence', () => {
     })
     const fn = vi.fn()
     collection.on('persistence.error', fn)
-    await waitForEvent(collection, 'persistence.error')
+    await expect(collection.isReady()).rejects.toThrowError('test')
     expect(fn).toHaveBeenCalledWith(new Error('test'))
   })
 
@@ -210,7 +210,7 @@ describe('Persistence', () => {
     })
     const fn = vi.fn()
     collection.on('persistence.error', fn)
-    await waitForEvent(collection, 'persistence.error')
+    await expect(collection.isReady()).rejects.toThrowError('test')
     expect(fn).toHaveBeenCalledWith(new Error('test'))
   })