Repository: maxnowack/signaldb
Pull request #320 (2023-11-01 14:48:08)

feat: add possibility to apply changes from persistence adapter


Patch:
diff --git a/src/Collection/index.ts b/src/Collection/index.ts
index a8acf76..0ce3a46 100644
--- a/src/Collection/index.ts
+++ b/src/Collection/index.ts
@@ -52,13 +52,29 @@ export default class Collection<T extends BaseItem<I> = BaseItem, I = any, U = T
       let ongoingSaves = 0
       const loadPersistentData = async () => {
         // load items from persistence adapter and push them into memory
-        const { items } = await persistenceAdapter.load()
+        const { items, changes } = await persistenceAdapter.load()
 
-        // as we overwrite all items, we need to discard if there are ongoing saves
-        if (ongoingSaves > 0) return
+        if (items) {
+          // as we overwrite all items, we need to discard if there are ongoing saves
+          if (ongoingSaves > 0) return
 
-        // push new items to this.memory() and delete old ones
-        this.memory().splice(0, this.memoryArray().length, ...items)
+          // push new items to this.memory() and delete old ones
+          this.memory().splice(0, this.memoryArray().length, ...items)
+        } else if (changes) {
+          changes.added.forEach((item) => {
+            this.memory().push(item)
+          })
+          changes.modified.forEach((item) => {
+            const index = this.memory().findIndex(doc => doc.id === item.id)
+            if (index === -1) throw new Error('Cannot resolve index for item')
+            this.memory().splice(index, 1, item)
+          })
+          changes.removed.forEach((item) => {
+            const index = this.memory().findIndex(doc => doc.id === item.id)
+            if (index === -1) throw new Error('Cannot resolve index for item')
+            this.memory().splice(index, 1)
+          })
+        }
         this.rebuildIndexes()
 
         this.emit('persistence.received')
diff --git a/src/types/PersistenceAdapter.ts b/src/types/PersistenceAdapter.ts
index 24361b8..f10f0ac 100644
--- a/src/types/PersistenceAdapter.ts
+++ b/src/types/PersistenceAdapter.ts
@@ -4,9 +4,11 @@ interface Changeset<T> {
   removed: T[],
 }
 
+type LoadResponse<T> = { items: T[], changes?: never } | { changes: Changeset<T>, items?: never }
+
 // eslint-disable-next-line max-len
 export default interface PersistenceAdapter<T extends { id: I } & Record<string, any>, I> {
-  load(): Promise<{ items: T[], changes?: Changeset<T> }>,
+  load(): Promise<LoadResponse<T>>,
   save(items: T[], changes: Changeset<T>): Promise<void>,
   register(onChange: () => void | Promise<void>): Promise<void>,
 }


Test patch:
diff --git a/__tests__/persistence.spec.ts b/__tests__/persistence.spec.ts
index ca62294..1bc483d 100644
--- a/__tests__/persistence.spec.ts
+++ b/__tests__/persistence.spec.ts
@@ -1,28 +1,68 @@
 import fs from 'fs/promises'
 import { describe, it, expect, vi } from 'vitest'
+import type { PersistenceAdapter } from '../src/index'
 import { Collection, createFilesystemAdapter } from '../src/index'
 import waitForEvent from '../src/utils/waitForEvent'
 
 // eslint-disable-next-line max-len
-function memoryPersistenceAdapter<T extends { id: I } & Record<string, any>, I = any>(initialData: T[] = []) {
+function memoryPersistenceAdapter<T extends { id: I } & Record<string, any>, I = any>(
+  initialData: T[] = [],
+  transmitChanges = false,
+) {
   // not really a "persistence adapter", but it works for testing
   let items = [...initialData]
+  const changes: {
+    added: T[],
+    modified: T[],
+    removed: T[],
+  } = {
+    added: [],
+    modified: [],
+    removed: [],
+  }
   let onChange: () => void | Promise<void> = () => { /* do nothing */ }
   return {
     register: (changeCallback: () => void | Promise<void>) => {
       onChange = changeCallback
       return Promise.resolve()
     },
-    load: () => Promise.resolve({ items }),
+    load: () => {
+      const currentChanges = { ...changes }
+      changes.added = []
+      changes.modified = []
+      changes.removed = []
+      const hasChanges = currentChanges.added.length > 0
+        || currentChanges.modified.length > 0
+        || currentChanges.removed.length > 0
+      if (transmitChanges && hasChanges) {
+        return Promise.resolve({ changes: currentChanges })
+      }
+      return Promise.resolve({ items })
+    },
     save: (newSnapshot: T[]) => {
       items = [...newSnapshot]
       return Promise.resolve()
     },
     addNewItem: (item: T) => {
       items.push(item)
+      changes.added.push(item)
       void onChange()
     },
-  }
+    changeItem: (item: T) => {
+      items = items.map(i => (i.id === item.id ? item : i))
+      changes.modified.push(item)
+      void onChange()
+    },
+    removeItem: (item: T) => {
+      items = items.filter(i => i.id !== item.id)
+      changes.removed.push(item)
+      void onChange()
+    },
+  } as (PersistenceAdapter<T, I> & {
+    addNewItem: (item: T) => void,
+    changeItem: (item: T) => void,
+    removeItem: (item: T) => void,
+  })
 }
 
 describe('Persistence', () => {
@@ -45,7 +85,7 @@ describe('Persistence', () => {
     expect((await persistence.load()).items).toEqual([{ id: '1', name: 'John' }])
   })
 
-  it('should get changes from persistence adapter', async () => {
+  it('should get items from persistence adapter', async () => {
     const persistence = memoryPersistenceAdapter([{ id: '1', name: 'John' }])
     const collection = new Collection({ persistence })
     await waitForEvent(collection, 'persistence.init')
@@ -62,6 +102,32 @@ describe('Persistence', () => {
     ])
   })
 
+  it('should get changes from persistence adapter', async () => {
+    const persistence = memoryPersistenceAdapter([{ id: '1', name: 'John' }], true)
+    const collection = new Collection({ persistence })
+    await waitForEvent(collection, 'persistence.init')
+    const items = collection.find().fetch()
+    expect(items).toEqual([{ id: '1', name: 'John' }])
+
+    persistence.addNewItem({ id: '2', name: 'Jane' })
+    await waitForEvent(collection, 'persistence.received')
+    expect(collection.find().fetch()).toEqual([
+      { id: '1', name: 'John' },
+      { id: '2', name: 'Jane' },
+    ])
+
+    persistence.changeItem({ id: '1', name: 'Johnny' })
+    await waitForEvent(collection, 'persistence.received')
+    expect(collection.find().fetch()).toEqual([
+      { id: '1', name: 'Johnny' },
+      { id: '2', name: 'Jane' },
+    ])
+
+    persistence.removeItem({ id: '2', name: 'Jane' })
+    await waitForEvent(collection, 'persistence.received')
+    expect(collection.find().fetch()).toEqual([{ id: '1', name: 'Johnny' }])
+  })
+
   it('should remove item from persistence adapter', async () => {
     const persistence = memoryPersistenceAdapter([{ id: '1', name: 'John' }, { id: '2', name: 'Jane' }])
     const collection = new Collection({ persistence })