diff --git a/packages/app/src/core.ts b/packages/app/src/core.ts index 77a7531..49e8246 100644 --- a/packages/app/src/core.ts +++ b/packages/app/src/core.ts @@ -3,12 +3,10 @@ import {Repository, Relay, LOCAL_RELAY_URL, getFilterResultCardinality} from "@w import type {TrustedEvent, Filter} from "@welshman/util" import {Tracker, subscribe as baseSubscribe} from "@welshman/net" import type {SubscribeRequestWithHandlers} from "@welshman/net" -import {createEventStore, custom} from "@welshman/store" +import {custom} from "@welshman/store" export const repository = new Repository() -export const events = createEventStore(repository) - export const relay = new Relay(repository) export const tracker = new Tracker() diff --git a/packages/app/src/storage.ts b/packages/app/src/storage.ts index 98cb03f..c788c52 100644 --- a/packages/app/src/storage.ts +++ b/packages/app/src/storage.ts @@ -3,15 +3,19 @@ import type {IDBPDatabase} from "idb" import {throttle} from "throttle-debounce" import {writable} from "svelte/store" import type {Unsubscriber, Writable} from "svelte/store" -import {randomInt, fromPairs} from "@welshman/lib" +import {indexBy, fromPairs} from "@welshman/lib" +import type {TrustedEvent, Repository} from "@welshman/util" import type {Tracker} from "@welshman/net" -import {withGetter, adapter, custom} from "@welshman/store" +import {withGetter, adapter, throttled, custom} from "@welshman/store" -export type Item = Record +export type IndexedDbAdapterOptions = { + migrate?: (items: any[]) => any[] +} export type IndexedDbAdapter = { keyPath: string - store: Writable + store: Writable + options?: IndexedDbAdapterOptions } export let db: IDBPDatabase @@ -49,21 +53,29 @@ export const bulkDelete = async (name: string, ids: string[]) => { export const initIndexedDbAdapter = async (name: string, adapter: IndexedDbAdapter) => { let prevRecords = await getAll(name) + if (adapter.options?.migrate) { + prevRecords = adapter.options.migrate(prevRecords) + } + adapter.store.set(prevRecords) adapter.store.subscribe( - throttle(randomInt(3000, 5000), async (newRecords: Item[]) => { + async (currentRecords: any[]) => { if (dead.get()) { return } - const currentIds = new Set(newRecords.map(item => item[adapter.keyPath])) + const currentIds = new Set(currentRecords.map(item => item[adapter.keyPath])) const removedRecords = prevRecords.filter(r => !currentIds.has(r[adapter.keyPath])) - prevRecords = newRecords + const prevRecordsById = indexBy(item => item[adapter.keyPath], prevRecords) + const updatedRecords = currentRecords.filter(r => r !== prevRecordsById.get(r[adapter.keyPath])) - if (newRecords.length > 0) { - await bulkPut(name, newRecords) + prevRecords = currentRecords + + if (updatedRecords.length > 0) { + + await bulkPut(name, updatedRecords) } if (removedRecords.length > 0) { @@ -72,7 +84,7 @@ export const initIndexedDbAdapter = async (name: string, adapter: IndexedDbAdapt removedRecords.map(item => item[adapter.keyPath]), ) } - }), + }, ) } @@ -121,36 +133,47 @@ export const clearStorage = async () => { await deleteDB(db.name) } +export type StorageAdapterOptions = IndexedDbAdapterOptions & { + throttle?: number +} + export const storageAdapters = { - fromObjectStore: (store: Writable>) => ({ + fromObjectStore: (store: Writable>, options: StorageAdapterOptions = {}) => ({ + options, keyPath: "key", - store: adapter({ + store: throttled(options.throttle || 0, adapter({ store: store, forward: ($data: Record) => Object.entries($data).map(([key, value]) => ({key, value})), backward: (data: {key: string, value: T}[]) => fromPairs(data.map(({key, value}) => [key, value])), - }), + })), }), - fromMapStore: (store: Writable>) => ({ + fromMapStore: (store: Writable>, options: StorageAdapterOptions = {}) => ({ + options, keyPath: "key", - store: adapter({ + store: throttled(options.throttle || 0, adapter({ store: store, forward: ($data: Map) => Array.from($data.entries()).map(([key, value]) => ({key, value})), backward: (data: {key: string, value: T}[]) => new Map(data.map(({key, value}) => [key, value])), - }), + })), }), - fromTracker: (tracker: Tracker) => ({ + fromTracker: (tracker: Tracker, options: StorageAdapterOptions = {}) => ({ + options, keyPath: 'key', store: custom(setter => { - const onUpdate = () => + let onUpdate = () => setter( Array.from(tracker.data.entries()) .map(([key, urls]) => ({key, value: Array.from(urls)})) ) + if (options.throttle) { + onUpdate = throttle(options.throttle, onUpdate) + } + onUpdate() tracker.on('update', onUpdate) @@ -160,4 +183,22 @@ export const storageAdapters = { tracker.load(new Map(data.map(({key, value}) => [key, new Set(value)]))), }), }), + fromRepository: (repository: Repository, options: StorageAdapterOptions = {}) => ({ + options, + keyPath: 'id', + store: custom(setter => { + let onUpdate = () => setter(repository.dump()) + + if (options.throttle) { + onUpdate = throttle(options.throttle, onUpdate) + } + + onUpdate() + repository.on('update', onUpdate) + + return () => repository.off('update', onUpdate) + }, { + set: (events: TrustedEvent[]) => repository.load(events), + }), + }), } diff --git a/packages/store/src/index.ts b/packages/store/src/index.ts index ae3bcf2..fc4188b 100644 --- a/packages/store/src/index.ts +++ b/packages/store/src/index.ts @@ -1,13 +1,13 @@ import {throttle} from "throttle-debounce" import {derived, writable} from "svelte/store" -import type {Readable, Updater, Writable, Subscriber, Unsubscriber} from "svelte/store" +import type {Readable, Writable, Subscriber, Unsubscriber} from "svelte/store" import {identity, ensurePlural, getJson, setJson, batch, partition, first} from "@welshman/lib" import type {Maybe} from "@welshman/lib" import type {Repository} from "@welshman/util" import {matchFilters, getIdAndAddress, getIdFilters} from "@welshman/util" import type {Filter, TrustedEvent} from "@welshman/util" -// Generic store utils +// Sync with localstorage export const synced = (key: string, defaultValue: T) => { const init = getJson(key) @@ -18,6 +18,8 @@ export const synced = (key: string, defaultValue: T) => { return store } +// Getters + export const getter = (store: Readable) => { let value: T @@ -37,6 +39,20 @@ export function withGetter(store: Readable | Writable) { return {...store, get: getter(store)} } +// Throttle + +export const throttled = >(delay: number, store: S) => { + if (delay) { + const {subscribe} = store + + store = {...store, subscribe: (f: Subscriber) => subscribe(throttle(delay, f))} + } + + return store +} + +// Custom store + type Start = (set: Subscriber) => Unsubscriber export type CustomStoreOpts = { @@ -96,6 +112,8 @@ export const custom = (start: Start, opts: CustomStoreOpts = {}): Writa } } +// Simple adapter + export const adapter = ({ store, forward, @@ -110,54 +128,14 @@ export const adapter = ({ update: (f: (x: Target) => Target) => store.update((x: Source) => backward(f(forward(x)))), }) -export const throttled = (delay: number, store: Readable) => - custom(set => store.subscribe(throttle(delay, set))) - // Event related stores -export const createEventStore = ( - repository: Repository, - migrate?: (events: TrustedEvent[]) => TrustedEvent[], -): Writable => { - let subs: Subscriber[] = [] - - const onUpdate = () => { - const $events = repository.dump() - - for (const sub of subs) { - sub($events) - } - } - - const setEvents = (events: TrustedEvent[]) => { - if (migrate) { - events = migrate(events) - } - - repository.load(events) - } - - return { - set: (events: TrustedEvent[]) => setEvents(events), - update: (f: Updater) => setEvents(f(repository.dump())), - subscribe: (f: Subscriber) => { - f(repository.dump()) - - subs.push(f) - - if (subs.length === 1) { - repository.on("update", onUpdate) - } - - return () => { - subs = subs.filter(x => x !== f) - - if (subs.length === 0) { - repository.off("update", onUpdate) - } - } - }, - } +export type DeriveEventsMappedOptions = { + filters: Filter[] + eventToItem: (event: TrustedEvent) => Maybe> + itemToEvent: (item: T) => TrustedEvent + throttle?: number + includeDeleted?: boolean } export const deriveEventsMapped = (repository: Repository, { @@ -166,13 +144,7 @@ export const deriveEventsMapped = (repository: Repository, { itemToEvent, throttle = 0, includeDeleted = false, -}: { - filters: Filter[] - eventToItem: (event: TrustedEvent) => Maybe> - itemToEvent: (item: T) => TrustedEvent - throttle?: number - includeDeleted?: boolean -}) => +}: DeriveEventsMappedOptions) => custom(setter => { let data: T[] = [] const deferred = new Set() @@ -267,7 +239,9 @@ export const deriveEventsMapped = (repository: Repository, { return () => repository.off("update", onUpdate) }, {throttle}) -export const deriveEvents = (repository: Repository, opts: {filters: Filter[], includeDeleted?: boolean}) => +export type DeriveEventsOptions = Omit, "itemToEvent" | "eventToItem"> + +export const deriveEvents = (repository: Repository, opts: DeriveEventsOptions) => deriveEventsMapped(repository, { ...opts, eventToItem: identity,