Files
flotilla/src/app/util/storage.ts
T
2026-03-12 11:33:04 -07:00

327 lines
7.4 KiB
TypeScript

import {on, throttle, indexBy, fromPairs, batch} from "@welshman/lib"
import {throttled} from "@welshman/store"
import {
ALERT_ANDROID,
ALERT_EMAIL,
ALERT_IOS,
ALERT_STATUS,
ALERT_WEB,
APP_DATA,
BLOSSOM_SERVERS,
FOLLOWS,
MESSAGING_RELAYS,
MUTES,
PROFILE,
RELAY_ADD_MEMBER,
RELAY_JOIN,
RELAY_LEAVE,
RELAY_MEMBERS,
RELAY_REMOVE_MEMBER,
RELAYS,
ROOM_ADD_MEMBER,
ROOM_CREATE_PERMISSION,
ROOM_MEMBERS,
ROOM_ADMINS,
ROOM_META,
ROOM_DELETE,
ROOM_REMOVE_MEMBER,
ROOMS,
verifiedSymbol,
} from "@welshman/util"
import type {Zapper, TrustedEvent, RelayProfile} from "@welshman/util"
import type {RepositoryUpdate, WrapItem} from "@welshman/net"
import type {Handle, RelayStats} from "@welshman/app"
import {
tracker,
plaintext,
repository,
relaysByUrl,
relayStatsByUrl,
onRelayStats,
handlesByNip05,
zappersByLnurl,
onZapper,
onHandle,
wrapManager,
onRelay,
} from "@welshman/app"
import type {Unsubscriber} from "svelte/store"
import {db} from "@app/core/storage"
const kinds = {
meta: [PROFILE, FOLLOWS, MUTES, RELAYS, BLOSSOM_SERVERS, MESSAGING_RELAYS, APP_DATA, ROOMS],
alert: [ALERT_STATUS, ALERT_EMAIL, ALERT_WEB, ALERT_IOS, ALERT_ANDROID],
space: [RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER, RELAY_MEMBERS, RELAY_JOIN, RELAY_LEAVE],
room: [
ROOM_META,
ROOM_DELETE,
ROOM_ADMINS,
ROOM_MEMBERS,
ROOM_ADD_MEMBER,
ROOM_REMOVE_MEMBER,
ROOM_CREATE_PERMISSION,
],
}
const shouldPersistEvent = (event: TrustedEvent) =>
kinds.meta.includes(event.kind) ||
kinds.alert.includes(event.kind) ||
kinds.space.includes(event.kind) ||
kinds.room.includes(event.kind)
type TrackerItem = {id: string; relays: string[]}
type PlaintextItem = {key: string; value: string}
const loadCriticalEvents = async () => {
const table = db.table<TrustedEvent>("events")
const initialEvents = await table.getAll()
const keep: TrustedEvent[] = []
const drop: string[] = []
for (const event of initialEvents) {
if (shouldPersistEvent(event)) {
event[verifiedSymbol] = true
keep.push(event)
} else {
drop.push(event.id)
}
}
repository.load(keep)
if (drop.length > 0) {
void table.bulkDelete(drop)
}
}
const syncEvents = () => {
const table = db.table<TrustedEvent>("events")
return on(
repository,
"update",
batch(3000, async (updates: RepositoryUpdate[]) => {
const add: TrustedEvent[] = []
const remove = new Set<string>()
for (const update of updates) {
for (const event of update.added) {
if (shouldPersistEvent(event)) {
add.push(event)
remove.delete(event.id)
}
}
for (const id of update.removed) {
remove.add(id)
}
}
if (add.length > 0) {
await table.bulkPut(add)
}
if (remove.size > 0) {
await table.bulkDelete(remove)
}
}),
)
}
const loadCriticalTracker = async () => {
const table = db.table<TrackerItem>("tracker")
const relaysById = new Map<string, Set<string>>()
const stale: string[] = []
for (const {id, relays} of await table.getAll()) {
if (!repository.getEvent(id)) {
stale.push(id)
continue
}
relaysById.set(id, new Set(relays))
}
tracker.load(relaysById)
if (stale.length > 0) {
void table.bulkDelete(stale)
}
}
const syncTracker = () => {
const table = db.table<TrackerItem>("tracker")
const _onAdd = async (ids: Iterable<string>) => {
const items: TrackerItem[] = []
for (const id of ids) {
const event = repository.getEvent(id)
if (!event || !shouldPersistEvent(event)) continue
const relays = Array.from(tracker.getRelays(id))
if (relays.length === 0) continue
items.push({id, relays})
}
await table.bulkPut(items)
}
const _onRemove = async (ids: Iterable<string>) => {
await table.bulkDelete(Array.from(ids))
}
const onAdd = batch(3000, _onAdd)
const onRemove = batch(3000, _onRemove)
const onLoad = () => _onAdd(tracker.relaysById.keys())
const onClear = () => _onRemove(tracker.relaysById.keys())
tracker.on("add", onAdd)
tracker.on("remove", onRemove)
tracker.on("load", onLoad)
tracker.on("clear", onClear)
return () => {
tracker.off("add", onAdd)
tracker.off("remove", onRemove)
tracker.off("load", onLoad)
tracker.off("clear", onClear)
}
}
const loadCriticalRelays = async () => {
const table = db.table<RelayProfile>("relays")
relaysByUrl.set(indexBy(r => r.url, await table.getAll()))
}
const syncRelays = () => onRelay(batch(1000, db.table<RelayProfile>("relays").bulkPut))
const initRelayStats = async () => {
const table = db.table<RelayStats>("relayStats")
relayStatsByUrl.set(indexBy(r => r.url, await table.getAll()))
return onRelayStats(batch(1000, table.bulkPut))
}
const initHandles = async () => {
const table = db.table<Handle>("handles")
handlesByNip05.set(indexBy(r => r.nip05, await table.getAll()))
return onHandle(batch(1000, table.bulkPut))
}
const initZappers = async () => {
const table = db.table<Zapper>("zappers")
zappersByLnurl.set(indexBy(z => z.lnurl, await table.getAll()))
return onZapper(batch(3000, table.bulkPut))
}
const initPlaintext = async () => {
const table = db.table<PlaintextItem>("plaintext")
const initialRecords = await table.getAll()
plaintext.set(fromPairs(initialRecords.map(({key, value}) => [key, value])))
return throttled(3000, plaintext).subscribe($plaintext => {
table.bulkPut(Object.entries($plaintext).map(([key, value]) => ({key, value})))
})
}
const initWrapManager = async () => {
const table = db.table<WrapItem>("wrapManager")
wrapManager.load(await table.getAll())
const addOne = batch(3000, table.bulkPut)
const removeOne = throttle(3000, table.bulkDelete)
wrapManager.on("add", addOne)
wrapManager.on("remove", removeOne)
return () => {
wrapManager.off("add", addOne)
wrapManager.off("remove", removeOne)
}
}
type StorageSync = {
unsubscribe: Unsubscriber
ready: Promise<void>
}
export const sync = (): StorageSync => {
const unsubscribers: Unsubscriber[] = []
const deferredTimers: ReturnType<typeof setTimeout>[] = []
let stopped = false
const addUnsubscriber = (unsubscriber: Unsubscriber) => {
if (stopped) {
unsubscriber()
} else {
unsubscribers.push(unsubscriber)
}
}
const scheduleDeferred = (task: () => Promise<void>) => {
const timeout = setTimeout(() => {
if (stopped) return
void task()
}, 0)
deferredTimers.push(timeout)
}
const ready = (async () => {
await db.connect()
await Promise.all([loadCriticalEvents(), loadCriticalRelays()])
await loadCriticalTracker()
addUnsubscriber(syncEvents())
addUnsubscriber(syncTracker())
addUnsubscriber(syncRelays())
scheduleDeferred(async () => {
addUnsubscriber(await initRelayStats())
})
scheduleDeferred(async () => {
addUnsubscriber(await initHandles())
})
scheduleDeferred(async () => {
addUnsubscriber(await initZappers())
})
scheduleDeferred(async () => {
addUnsubscriber(await initPlaintext())
})
scheduleDeferred(async () => {
addUnsubscriber(await initWrapManager())
})
})()
const unsubscribe = () => {
stopped = true
for (const timeout of deferredTimers) {
clearTimeout(timeout)
}
unsubscribers.forEach(unsubscriber => unsubscriber())
}
return {unsubscribe, ready}
}