AI refactor

This commit is contained in:
Jon Staab
2026-06-16 09:22:26 -07:00
parent 28339976b9
commit ea9cc0bf26
34 changed files with 2601 additions and 227 deletions
+92 -78
View File
@@ -1,93 +1,107 @@
import {Subscriber} from 'svelte/store'
import {call} from '@welshman/lib'
import {writable} from "svelte/store"
import type {Readable, Unsubscriber} from "svelte/store"
import type {Maybe} from "@welshman/lib"
import {getter, makeDeriveItem, makeLoadItem, makeForceLoadItem} from "@welshman/store"
import type {MakeLoadItemOptions} from "@welshman/store"
import type {ClientContext} from "./client.js"
/**
* Base class for a reactive, keyed collection of "local" (non-event) data —
* things like relay stats or NIP-11 profiles that aren't backed by the
* repository. The collection owns its own map and is its own Svelte store: its
* `subscribe` emits the underlying `Map`.
*
* Subclasses reach the client through the `ClientContext` seam, never the
* concrete `Client`, so they never create a dependency cycle.
*/
export class ClientData<T> {
selfSubscribers: Subscriber<ClientData<T>>[] = []
clearSubscribers: Subscriber<ClientData<T>>[] = []
itemSubscribers: Subscriber<T>[] = []
data = new Map<string, T>()
protected index = writable(new Map<string, T>())
protected getIndex = getter(this.index)
protected itemSubscribers: ((key: string, value: Maybe<T>) => void)[] = []
public derive: (key?: string, ...args: any[]) => Readable<Maybe<T>>
// Svelte store-like methods
private notify = () => {
for (const subscriber of this.selfSubscribers) {
subscriber(this)
}
constructor(protected readonly ctx: ClientContext) {
this.derive = makeDeriveItem(this.index)
}
subscribe = (subscriber: Subscriber<ClientData<T>>) => {
subscriber(this)
this.selfSubscribers.push(subscriber)
subscribe = this.index.subscribe
return () => {
this.selfSubscribers.splice(this.selfSubscribers.indexOf(subscriber), 1)
}
}
get = (key: string): Maybe<T> => this.getIndex().get(key)
derived = (key: string) => {
return readable(this.get(key), set => {
const subscribers = [
this.onClear(() => set(undefined)),
this.onItem(set)
]
getAll = (): T[] => Array.from(this.getIndex().values())
return () => subscribers.forEach(call)
})
}
keys = () => this.getIndex().keys()
// EventEmitter-like methods
private emitClear = () => {
for (const subscriber of this.clearSubscribers) {
subscriber(this)
}
this.notify()
}
onClear = (subscriber: Subscriber<T>) => {
this.clearSubscribers.push(subscriber)
return () => {
this.clearSubscribers.splice(this.clearSubscribers.indexOf(subscriber), 1)
}
}
private emitItem = (item: T) => {
for (const subscriber of this.itemSubscribers) {
subscriber(item)
}
this.notify()
}
onItem = (subscriber: Subscriber<T>) => {
this.itemSubscribers.push(subscriber)
return () => {
this.itemSubscribers.splice(this.itemSubscribers.indexOf(subscriber), 1)
}
}
// Map-like methods
clear = () => {
this.data.clear()
this.emitClear()
}
delete = (key: string) => {
this.data.delete(key)
this.emitItem(key, undefined)
}
values = () => this.getIndex().values()
set = (key: string, value: T) => {
this.data.set(key, value)
this.index.update($items => {
$items.set(key, value)
return $items
})
this.emitItem(key, value)
}
get = (key: string) => this.data.get(key)
values = () => this.data.values()
items = () => this.data.items()
keys = () => this.data.keys()
delete = (key: string) => {
this.index.update($items => {
$items.delete(key)
return $items
})
this.emitItem(key, undefined)
}
clear = () => {
const keys = Array.from(this.getIndex().keys())
this.index.set(new Map())
for (const key of keys) {
this.emitItem(key, undefined)
}
}
onItem = (subscriber: (key: string, value: Maybe<T>) => void): Unsubscriber => {
this.itemSubscribers.push(subscriber)
return () => {
const i = this.itemSubscribers.indexOf(subscriber)
if (i !== -1) this.itemSubscribers.splice(i, 1)
}
}
protected emitItem = (key: string, value: Maybe<T>) => {
for (const subscriber of this.itemSubscribers) {
subscriber(key, value)
}
}
}
/**
* A `ClientData` collection that knows how to lazily load items by key from the
* network. Subclasses implement `fetch`; `load`/`forceLoad`/`derive` are derived
* from it (with per-key caching and backoff via `makeLoadItem`).
*/
export abstract class LoadableData<T> extends ClientData<T> {
load: (key: string, ...args: any[]) => Promise<Maybe<T>>
forceLoad: (key: string, ...args: any[]) => Promise<Maybe<T>>
abstract fetch(key: string, ...args: any[]): Promise<unknown>
constructor(ctx: ClientContext, options: MakeLoadItemOptions = {}) {
super(ctx)
// Subclasses implement `fetch` as an arrow field, whose initializer runs
// *after* super() — so `this.fetch` is undefined here. makeLoadItem captures
// its loadItem eagerly, so we defer the lookup to call time via this wrapper.
const fetch = (key: string, ...args: any[]) => this.fetch(key, ...args)
this.load = makeLoadItem(fetch, this.get, options)
this.forceLoad = makeForceLoadItem(fetch, this.get)
this.derive = makeDeriveItem(this.index, this.load)
}
}