Skip to content

Commit

Permalink
feat: add first draft of auto fetch collection class
Browse files Browse the repository at this point in the history
resolves #394
  • Loading branch information
maxnowack committed Apr 17, 2024
1 parent 80e3302 commit 412c49d
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 0 deletions.
101 changes: 101 additions & 0 deletions packages/signaldb/src/AutoFetchCollection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import type { BaseItem } from './Collection'
import type { ReplicatedCollectionOptions } from './ReplicatedCollection'
import ReplicatedCollection from './ReplicatedCollection'
import type Selector from './types/Selector'
import uniqueBy from './utils/uniqueBy'

interface AutoFetchOptions<T extends { id: I } & Record<string, any>, I> {
fetchQueryItems: (selector: Selector<T>) => ReturnType<ReplicatedCollectionOptions<T, I>['pull']>,
}
export type AutoFetchCollectionOptions<
T extends BaseItem<I>,
I,
U = T,
> = Omit<ReplicatedCollectionOptions<T, I, U>, 'pull'> & AutoFetchOptions<T, I>

export default class AutoFetchCollection<
T extends BaseItem<I> = BaseItem,
I = any,
U = T,
> extends ReplicatedCollection<T, I, U> {
private activeObservers = new Map<Selector<T>, number>()
private idQueryCache = new Map<I, Selector<T>[]>()
private itemsCache: T[] = []

Check warning on line 23 in packages/signaldb/src/AutoFetchCollection.ts

View check run for this annotation

Codecov / codecov/patch

packages/signaldb/src/AutoFetchCollection.ts#L21-L23

Added lines #L21 - L23 were not covered by tests
private fetchQueryItems: (selector: Selector<T>) => ReturnType<ReplicatedCollectionOptions<T, I>['pull']>
private triggerReload: null | (() => void | Promise<void>) = null

Check warning on line 25 in packages/signaldb/src/AutoFetchCollection.ts

View check run for this annotation

Codecov / codecov/patch

packages/signaldb/src/AutoFetchCollection.ts#L25

Added line #L25 was not covered by tests

constructor(options: AutoFetchCollectionOptions<T, I, U>) {
super({

Check warning on line 28 in packages/signaldb/src/AutoFetchCollection.ts

View check run for this annotation

Codecov / codecov/patch

packages/signaldb/src/AutoFetchCollection.ts#L27-L28

Added lines #L27 - L28 were not covered by tests
...options,
pull: () => Promise.resolve({ items: this.itemsCache }),
registerRemoteChange: (onChange) => {
this.triggerReload = onChange
return Promise.resolve()

Check warning on line 33 in packages/signaldb/src/AutoFetchCollection.ts

View check run for this annotation

Codecov / codecov/patch

packages/signaldb/src/AutoFetchCollection.ts#L30-L33

Added lines #L30 - L33 were not covered by tests
},
})

this.fetchQueryItems = options.fetchQueryItems

Check warning on line 37 in packages/signaldb/src/AutoFetchCollection.ts

View check run for this annotation

Codecov / codecov/patch

packages/signaldb/src/AutoFetchCollection.ts#L37

Added line #L37 was not covered by tests
this.on('observer.created', selector => this.handleObserverCreation(selector ?? {}))
this.on('observer.disposed', selector => this.handleObserverDisposal(selector ?? {}))
}

private handleObserverCreation(selector: Selector<T>) {

Check warning on line 42 in packages/signaldb/src/AutoFetchCollection.ts

View check run for this annotation

Codecov / codecov/patch

packages/signaldb/src/AutoFetchCollection.ts#L42

Added line #L42 was not covered by tests
const activeObservers = this.activeObservers.get(selector) ?? 0
// increment the count of observers for this query
this.activeObservers.set(selector, activeObservers + 1)

Check warning on line 45 in packages/signaldb/src/AutoFetchCollection.ts

View check run for this annotation

Codecov / codecov/patch

packages/signaldb/src/AutoFetchCollection.ts#L45

Added line #L45 was not covered by tests

// if this is the first observer for this query, fetch the data
if (activeObservers === 0) {
this.fetchQueryItems(selector)
.then((response) => {

Check warning on line 50 in packages/signaldb/src/AutoFetchCollection.ts

View check run for this annotation

Codecov / codecov/patch

packages/signaldb/src/AutoFetchCollection.ts#L49-L50

Added lines #L49 - L50 were not covered by tests
if (!response.items) throw new Error('AutoFetchCollection currently only works with a full item response')

if (!this.itemsCache) {
// if this is the first response, cache it
this.itemsCache = response.items

Check warning on line 55 in packages/signaldb/src/AutoFetchCollection.ts

View check run for this annotation

Codecov / codecov/patch

packages/signaldb/src/AutoFetchCollection.ts#L55

Added line #L55 was not covered by tests
} else if (response.items && this.itemsCache) {
// merge the response into the cache
this.itemsCache = uniqueBy([...this.itemsCache, ...response.items], 'id')

Check warning on line 58 in packages/signaldb/src/AutoFetchCollection.ts

View check run for this annotation

Codecov / codecov/patch

packages/signaldb/src/AutoFetchCollection.ts#L58

Added line #L58 was not covered by tests
}

response.items.forEach((item) => {

Check warning on line 61 in packages/signaldb/src/AutoFetchCollection.ts

View check run for this annotation

Codecov / codecov/patch

packages/signaldb/src/AutoFetchCollection.ts#L61

Added line #L61 was not covered by tests
const queries = this.idQueryCache.get(item.id) ?? []
queries.push(selector)
this.idQueryCache.set(item.id, queries)

Check warning on line 64 in packages/signaldb/src/AutoFetchCollection.ts

View check run for this annotation

Codecov / codecov/patch

packages/signaldb/src/AutoFetchCollection.ts#L63-L64

Added lines #L63 - L64 were not covered by tests
})

if (!this.triggerReload) throw new Error('No triggerReload method found. Looks like your persistence adapter was not registered')
void this.triggerReload()

Check warning on line 68 in packages/signaldb/src/AutoFetchCollection.ts

View check run for this annotation

Codecov / codecov/patch

packages/signaldb/src/AutoFetchCollection.ts#L68

Added line #L68 was not covered by tests
})
.catch((error: Error) => {
this.emit('persistence.error', error)

Check warning on line 71 in packages/signaldb/src/AutoFetchCollection.ts

View check run for this annotation

Codecov / codecov/patch

packages/signaldb/src/AutoFetchCollection.ts#L70-L71

Added lines #L70 - L71 were not covered by tests
})
}
}

private handleObserverDisposal(selector: Selector<T>) {

Check warning on line 76 in packages/signaldb/src/AutoFetchCollection.ts

View check run for this annotation

Codecov / codecov/patch

packages/signaldb/src/AutoFetchCollection.ts#L76

Added line #L76 was not covered by tests
const activeObservers = this.activeObservers.get(selector) ?? 0
if (activeObservers > 1) {
// decrement the count of observers for this query
this.activeObservers.set(selector, activeObservers - 1)
return

Check warning on line 81 in packages/signaldb/src/AutoFetchCollection.ts

View check run for this annotation

Codecov / codecov/patch

packages/signaldb/src/AutoFetchCollection.ts#L80-L81

Added lines #L80 - L81 were not covered by tests
}

// if this is the last observer for this query, remove the query from the cache
this.activeObservers.delete(selector)

Check warning on line 85 in packages/signaldb/src/AutoFetchCollection.ts

View check run for this annotation

Codecov / codecov/patch

packages/signaldb/src/AutoFetchCollection.ts#L85

Added line #L85 was not covered by tests

// remove the query from the cache
this.idQueryCache.forEach((queries, id) => {
const updatedQueries = queries.filter(query => query !== selector)

Check warning on line 89 in packages/signaldb/src/AutoFetchCollection.ts

View check run for this annotation

Codecov / codecov/patch

packages/signaldb/src/AutoFetchCollection.ts#L88-L89

Added lines #L88 - L89 were not covered by tests
if (updatedQueries.length === 0) {
this.idQueryCache.delete(id)
this.itemsCache = this.itemsCache.filter(item => item.id !== id)
} else {
this.idQueryCache.set(id, updatedQueries)

Check warning on line 94 in packages/signaldb/src/AutoFetchCollection.ts

View check run for this annotation

Codecov / codecov/patch

packages/signaldb/src/AutoFetchCollection.ts#L91-L94

Added lines #L91 - L94 were not covered by tests
}
})

if (!this.triggerReload) throw new Error('No triggerReload method found. Looks like your persistence adapter was not registered')
void this.triggerReload()

Check warning on line 99 in packages/signaldb/src/AutoFetchCollection.ts

View check run for this annotation

Codecov / codecov/patch

packages/signaldb/src/AutoFetchCollection.ts#L99

Added line #L99 was not covered by tests
}
}
5 changes: 5 additions & 0 deletions packages/signaldb/src/Collection/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ interface CollectionEvents<T extends BaseItem, U = T> {
'persistence.transmitted': () => void,
'persistence.received': () => void,

'observer.created': <O extends FindOptions<T>>(selector?: Selector<T>, options?: O) => void,
'observer.disposed': <O extends FindOptions<T>>(selector?: Selector<T>, options?: O) => void,

'_debug.getItems': (callstack: string, selector: Selector<T> | undefined, measuredTime: number) => void,
'_debug.find': <O extends FindOptions<T>>(callstack: string, selector: Selector<T> | undefined, options: O | undefined, cursor: Cursor<T, U>) => void,
'_debug.findOne': <O extends FindOptions<T>>(callstack: string, selector: Selector<T>, options: O | undefined, item: U | undefined) => void,
Expand Down Expand Up @@ -338,11 +341,13 @@ export default class Collection<T extends BaseItem<I> = BaseItem, I = any, U = T
this.addListener('added', requeryOnce)
this.addListener('changed', requeryOnce)
this.addListener('removed', requeryOnce)
this.emit('observer.created', selector, options)
return () => {
this.removeListener('persistence.received', requeryOnce)
this.removeListener('added', requeryOnce)
this.removeListener('changed', requeryOnce)
this.removeListener('removed', requeryOnce)
this.emit('observer.disposed', selector, options)
}
},
})
Expand Down
49 changes: 49 additions & 0 deletions packages/signaldb/src/utils/uniqueBy.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { it, expect } from 'vitest'
import uniqueBy from './uniqueBy'

it('uniqueBy should return an array with unique items based on the provided key', () => {
const arr = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' },
{ id: 3, name: 'John' },
{ id: 4, name: 'Jane' },
]

const result = uniqueBy(arr, 'name')
expect(result).toEqual([
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' },
])
})

it('uniqueBy should return an array with unique items based on the provided function', () => {
const arr = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' },
{ id: 3, name: 'John' },
{ id: 4, name: 'Jane' },
]

const result = uniqueBy(arr, item => item.name)
expect(result).toEqual([
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' },
])
})

it('uniqueBy should preserve the order of the unique items', () => {
const arr = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' },
{ id: 3, name: 'John' },
{ id: 4, name: 'Jane' },
]

const result = uniqueBy(arr, 'name')
expect(result).toEqual([
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' },
])
expect(result[0]).toBe(arr[0])
expect(result[1]).toBe(arr[1])
})
7 changes: 7 additions & 0 deletions packages/signaldb/src/utils/uniqueBy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function uniqueBy<T>(arr: T[], fn: keyof T | ((item: T) => any)) {
const set = new Set<any>()
return arr.filter((el) => {
const value = typeof fn === 'function' ? fn(el) : el[fn]
return !set.has(value) && set.add(value)
})
}

0 comments on commit 412c49d

Please sign in to comment.