diff --git a/packages/lib/Store.ts b/packages/lib/Store.ts index c6fec46..66e2d46 100644 --- a/packages/lib/Store.ts +++ b/packages/lib/Store.ts @@ -17,8 +17,8 @@ export interface Readable { } export class Writable implements Readable { - private value: T - private subs: Subscriber[] = [] + value: T + subs: Subscriber[] = [] constructor(defaultValue: T, t?: number) { this.value = defaultValue @@ -71,11 +71,11 @@ export class Writable implements Readable { } export class Derived implements Readable { - private callerSubs: Subscriber[] = [] - private mySubs: Unsubscriber[] = [] - private stores: Derivable - private getValue: (values: any) => T - private latestValue: T | undefined + callerSubs: Subscriber[] = [] + mySubs: Unsubscriber[] = [] + stores: Derivable + getValue: (values: any) => T + latestValue: T | undefined constructor(stores: Derivable, getValue: (values: any) => T, t = 0) { this.stores = stores @@ -142,8 +142,8 @@ export class Derived implements Readable { export class Key implements Readable { readonly pk: string readonly key: string - private base: Writable> - private store: Readable + base: Writable> + store: Readable constructor(base: Writable>, pk: string, key: string) { if (!(base.get() instanceof Map)) { @@ -205,8 +205,8 @@ export class Key implements Readable { export class DerivedKey implements Readable { readonly pk: string readonly key: string - private base: Readable> - private store: Readable + base: Readable> + store: Readable constructor(base: Readable>, pk: string, key: string) { if (!(base.get() instanceof Map)) { diff --git a/packages/lib/Tools.ts b/packages/lib/Tools.ts index d278fb6..532888c 100644 --- a/packages/lib/Tools.ts +++ b/packages/lib/Tools.ts @@ -17,6 +17,8 @@ export const last = (xs: T[], ...args: unknown[]) => xs[xs.length - 1] export const identity = (x: T, ...args: unknown[]) => x +export const always = (x: T, ...args: unknown[]) => () => x + export const inc = (x: number | Nil) => (x || 0) + 1 export const dec = (x: number | Nil) => (x || 0) - 1 diff --git a/packages/util/Filters.ts b/packages/util/Filters.ts index 2e5121e..e5ef04c 100644 --- a/packages/util/Filters.ts +++ b/packages/util/Filters.ts @@ -128,7 +128,7 @@ export const intersectFilters = (groups: Filter[][]) => { return unionFilters(result) } -export const getIdFilters = (idsOrAddresses: Iterable) => { +export const getIdFilters = (idsOrAddresses: string[]) => { const ids = [] const aFilters = [] diff --git a/packages/util/Kinds.ts b/packages/util/Kinds.ts index c65fcdd..c906b8d 100644 --- a/packages/util/Kinds.ts +++ b/packages/util/Kinds.ts @@ -12,7 +12,7 @@ export enum Kind { Note = 1, Relay = 2, DM = 4, - EventDeletion = 5, + Delete = 5, Repost = 6, Reaction = 7, BadgeAward = 8, diff --git a/packages/util/Repository.ts b/packages/util/Repository.ts index 56893f7..b8935d2 100644 --- a/packages/util/Repository.ts +++ b/packages/util/Repository.ts @@ -1,6 +1,8 @@ +import {throttle} from 'throttle-debounce' import type {Readable, Subscriber, Invalidator} from '@welshman/lib' -import {Derived, chunk, sleep, uniq, omit, now, range, identity} from '@welshman/lib' -import {matchFilter} from './Filters' +import {Derived, Emitter, writable, first, always, chunk, sleep, uniq, omit, now, range, identity} from '@welshman/lib' +import {Kind} from './Kinds' +import {matchFilter, getIdFilters, matchFilters} from './Filters' import {encodeAddress, addressFromEvent} from './Address' import {isReplaceable} from './Events' import type {Filter} from './Filters' @@ -10,7 +12,11 @@ export const DAY = 86400 const getDay = (ts: number) => Math.floor(ts / DAY) -export class Repository implements Readable> { +export type RepositoryOptions = { + throttle?: number +} + +export class Repository extends Emitter implements Readable> { eventsById = new Map() eventsByAddress = new Map() eventsByTag = new Map() @@ -19,6 +25,14 @@ export class Repository implements Readable> { deletes = new Map() subs: Subscriber[] = [] + constructor(private options: RepositoryOptions) { + super() + + if (options.throttle) { + this.notify = throttle(options.throttle, this.notify.bind(this)) + } + } + // Methods for implementing store interface get() { @@ -41,10 +55,45 @@ export class Repository implements Readable> { return new Derived>(this, identity, t) } - notify() { + filter(getFilters: () => Filter[]) { + const store = writable([]) + + const onNotify = (event?: E) => { + const filters = getFilters() + + if (!event || matchFilters(filters, event)) { + store.set(Array.from(this.query(filters))) + } + } + + const subscribe = store.subscribe.bind(store) + + store.subscribe = (f: Subscriber) => { + if (store.subs.length === 0) { + this.on('notify', onNotify) + onNotify() + } + + const unsubscribe = subscribe(f) + + return () => { + unsubscribe() + + if (store.subs.length === 0) { + this.off('notify', onNotify) + } + } + } + + return store + } + + notify(event?: E) { for (const sub of this.subs) { sub(this) } + + this.emit('notify', event) } // Load/dump @@ -75,16 +124,20 @@ export class Repository implements Readable> { : this.eventsById.get(idOrAddress) } + watchEvent(idOrAddress: string) { + return this.filter(always(getIdFilters([idOrAddress]))).derived(first) + } + *query(filters: Filter[]) { for (let filter of filters) { let events: Iterable = this.eventsById.values() if (filter.ids) { - filter = omit(['ids'], filter) events = filter.ids!.map(id => this.eventsById.get(id)).filter(identity) as E[] + filter = omit(['ids'], filter) } else if (filter.authors) { - filter = omit(['authors'], filter) events = uniq(filter.authors!.flatMap(pubkey => this.eventsByAuthor.get(pubkey) || [])) + filter = omit(['authors'], filter) } else if (filter.since || filter.until) { const sinceDay = getDay(filter.since || 0) const untilDay = getDay(filter.since || now()) @@ -172,7 +225,7 @@ export class Repository implements Readable> { if (tag[0].length === 1) { this._updateIndex(this.eventsByTag, tag.slice(0, 2).join(':'), event, duplicate) - if (event.kind === 5) { + if (event.kind === Kind.Delete) { const id = tag[1] const ts = Math.max(event.created_at, this.deletes.get(tag[1]) || 0) @@ -182,7 +235,12 @@ export class Repository implements Readable> { } if (!this.isDeleted(event)) { - this.notify() + // Deletes are tricky, re-evaluate all subscriptions if that's what we're dealing with + if (event.kind === Kind.Delete) { + this.notify() + } else { + this.notify(event) + } } }