Repository: maxnowack/signaldb
Pull request #1185 (2024-12-05 10:42:45)

fix: offset items after deletions during batch

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/maxnowack/signaldb/pull/1185?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 [(`3c91841`)](https://app.codecov.io/gh/maxnowack/signaldb/commit/3c91841c62946ab1decec2dc0e3a9e621d60c500?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Max+Nowack) to head [(`9e9de8c`)](https://app.codecov.io/gh/maxnowack/signaldb/commit/9e9de8c04f57a5c99ecee613280ebef656d5069b?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     #1185   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           55        55           
  Lines         1556      1562    +6     
  Branches       351       353    +2     
=========================================
+ Hits          1556      1562    +6     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/maxnowack/signaldb/pull/1185?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/packages/signaldb/src/Collection/index.ts b/packages/signaldb/src/Collection/index.ts
index 0797e8f..67e0562 100644
--- a/packages/signaldb/src/Collection/index.ts
+++ b/packages/signaldb/src/Collection/index.ts
@@ -396,6 +396,18 @@ export default class Collection<T extends BaseItem<I> = BaseItem, I = any, U = T
     return { item, index }
   }
 
+  private deleteFromIdIndex(id: I, index: number) {
+    this.idIndex.delete(serializeValue(id))
+
+    // offset all indices after the deleted item -1, but only during batch operations
+    if (!this.batchOperationInProgress) return
+    this.idIndex.forEach(([currenIndex], key) => {
+      if (currenIndex > index) {
+        this.idIndex.set(key, new Set([currenIndex - 1]))
+      }
+    })
+  }
+
   private memory() {
     return this.options.memory as NonNullable<MemoryAdapter<T>>
   }
@@ -584,7 +596,7 @@ export default class Collection<T extends BaseItem<I> = BaseItem, I = any, U = T
     const { item, index } = this.getItemAndIndex(selector)
     if (item != null) {
       this.memory().splice(index, 1)
-      this.idIndex.delete(serializeValue(item.id))
+      this.deleteFromIdIndex(item.id, index)
       this.rebuildIndices()
       this.emit('removed', item)
     }
@@ -602,7 +614,7 @@ export default class Collection<T extends BaseItem<I> = BaseItem, I = any, U = T
       const index = this.memory().findIndex(doc => doc === item)
       if (index === -1) throw new Error('Cannot resolve index for item')
       this.memory().splice(index, 1)
-      this.idIndex.delete(serializeValue(item.id))
+      this.deleteFromIdIndex(item.id, index)
       this.rebuildIndices()
     })
 


Test patch:
diff --git a/packages/signaldb/__tests__/Collection.spec.ts b/packages/signaldb/__tests__/Collection.spec.ts
index 0297380..6567352 100644
--- a/packages/signaldb/__tests__/Collection.spec.ts
+++ b/packages/signaldb/__tests__/Collection.spec.ts
@@ -649,5 +649,16 @@ describe('Collection', () => {
       await col.dispose()
       expect(unregister).toHaveBeenCalledOnce()
     })
+
+    it('should not fail if id index gets modified during batch operation', () => {
+      const col = new Collection<{ id: string, name: string }>()
+      col.insert({ id: '1', name: 'John' })
+      col.insert({ id: '2', name: 'Jane' })
+      col.batch(() => {
+        col.removeOne({ id: '1' })
+
+        expect(col.find({ id: '2' }).fetch()).toEqual([{ id: '2', name: 'Jane' }])
+      })
+    })
   })
 })