2026-07-28 16:16:26 +00:00
|
|
|
import {writable} from "svelte/store"
|
|
|
|
|
import type {Unsubscriber} from "svelte/store"
|
|
|
|
|
import {deleteDB} from "idb"
|
|
|
|
|
import {SecureStorage} from "@aparajita/capacitor-secure-storage"
|
|
|
|
|
import {Preferences} from "@capacitor/preferences"
|
2026-08-18 18:32:39 +00:00
|
|
|
import {noop, on, throttle, batch, call, makeQueue} from "@welshman/lib"
|
2026-07-28 16:16:26 +00:00
|
|
|
import type {Maybe} from "@welshman/lib"
|
2025-09-30 23:11:49 +00:00
|
|
|
import {
|
2025-11-05 00:25:21 +00:00
|
|
|
ALERT_ANDROID,
|
|
|
|
|
ALERT_EMAIL,
|
|
|
|
|
ALERT_IOS,
|
|
|
|
|
ALERT_STATUS,
|
|
|
|
|
ALERT_WEB,
|
2025-10-24 20:36:59 +00:00
|
|
|
APP_DATA,
|
|
|
|
|
BLOSSOM_SERVERS,
|
2025-09-30 23:11:49 +00:00
|
|
|
FOLLOWS,
|
2025-11-20 23:12:16 +00:00
|
|
|
MESSAGING_RELAYS,
|
2025-09-30 23:11:49 +00:00
|
|
|
MUTES,
|
2025-10-24 20:36:59 +00:00
|
|
|
PROFILE,
|
|
|
|
|
RELAY_ADD_MEMBER,
|
|
|
|
|
RELAY_JOIN,
|
|
|
|
|
RELAY_LEAVE,
|
|
|
|
|
RELAY_MEMBERS,
|
|
|
|
|
RELAY_REMOVE_MEMBER,
|
2025-09-30 23:11:49 +00:00
|
|
|
RELAYS,
|
2025-10-24 20:36:59 +00:00
|
|
|
ROOM_ADD_MEMBER,
|
|
|
|
|
ROOM_CREATE_PERMISSION,
|
|
|
|
|
ROOM_MEMBERS,
|
2025-11-03 19:10:16 +00:00
|
|
|
ROOM_ADMINS,
|
2025-10-24 20:36:59 +00:00
|
|
|
ROOM_META,
|
2026-07-28 16:16:26 +00:00
|
|
|
ROOM_PINS,
|
2025-10-30 21:59:39 +00:00
|
|
|
ROOM_DELETE,
|
2025-10-24 20:36:59 +00:00
|
|
|
ROOM_REMOVE_MEMBER,
|
2025-09-30 23:11:49 +00:00
|
|
|
ROOMS,
|
2026-08-31 16:07:06 +00:00
|
|
|
hexTags,
|
|
|
|
|
tagValues,
|
2025-10-13 19:46:56 +00:00
|
|
|
verifiedSymbol,
|
2025-09-30 23:11:49 +00:00
|
|
|
} from "@welshman/util"
|
2026-07-28 16:16:26 +00:00
|
|
|
import type {Handle, TrustedEvent} from "@welshman/util"
|
|
|
|
|
import {withGetter} from "@welshman/store"
|
2025-10-17 17:19:21 +00:00
|
|
|
import type {RepositoryUpdate, WrapItem} from "@welshman/net"
|
2026-07-28 16:16:26 +00:00
|
|
|
import {Relay, Zapper} from "@welshman/domain"
|
|
|
|
|
import type {RelayInfo, ZapperValues} from "@welshman/domain"
|
|
|
|
|
import {Handles, Plaintext, Relays, RelayStats, User, Zappers} from "@welshman/app"
|
|
|
|
|
import type {AppPolicy, IApp, RelayStatsItem} from "@welshman/app"
|
2026-06-08 22:33:38 +00:00
|
|
|
import {IDB} from "@lib/indexeddb"
|
2026-07-28 16:16:26 +00:00
|
|
|
import {appPolicies} from "@app/core"
|
2026-06-08 22:33:38 +00:00
|
|
|
|
|
|
|
|
export const kv = call(() => {
|
2026-08-17 23:10:00 +00:00
|
|
|
const enqueue = makeQueue()
|
2026-06-08 22:33:38 +00:00
|
|
|
|
|
|
|
|
const get = async <T>(key: string): Promise<T | undefined> => {
|
|
|
|
|
const result = await Preferences.get({key})
|
|
|
|
|
if (!result.value) return undefined
|
|
|
|
|
try {
|
|
|
|
|
return JSON.parse(result.value)
|
|
|
|
|
} catch (e) {
|
|
|
|
|
return undefined
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const set = async <T>(key: string, value: T): Promise<void> => {
|
2026-08-17 23:10:00 +00:00
|
|
|
await enqueue(() => Preferences.set({key, value: JSON.stringify(value)}))
|
2026-06-08 22:33:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const clear = async () => {
|
2026-08-17 23:10:00 +00:00
|
|
|
await enqueue(() => Preferences.clear())
|
2026-06-08 22:33:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {get, set, clear}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
export const ss = call(() => {
|
2026-08-17 23:10:00 +00:00
|
|
|
const enqueue = makeQueue()
|
2026-06-08 22:33:38 +00:00
|
|
|
|
|
|
|
|
const get = async <T>(key: string): Promise<T | undefined> => {
|
|
|
|
|
let value = await SecureStorage.getItem(key)
|
|
|
|
|
|
|
|
|
|
if (!value) {
|
|
|
|
|
const legacy = await Preferences.get({key})
|
|
|
|
|
|
|
|
|
|
if (legacy.value) {
|
|
|
|
|
value = legacy.value
|
|
|
|
|
await SecureStorage.setItem(key, legacy.value)
|
|
|
|
|
await Preferences.remove({key})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!value) return undefined
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
return JSON.parse(value)
|
|
|
|
|
} catch (e) {
|
|
|
|
|
return undefined
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 23:10:00 +00:00
|
|
|
// Android's SecureStorage rejects undefined
|
2026-06-08 22:33:38 +00:00
|
|
|
const set = async <T>(key: string, value: T): Promise<void> => {
|
2026-08-17 23:10:00 +00:00
|
|
|
await enqueue(() =>
|
|
|
|
|
value === undefined
|
|
|
|
|
? SecureStorage.removeItem(key)
|
|
|
|
|
: SecureStorage.setItem(key, JSON.stringify(value)),
|
|
|
|
|
)
|
2026-06-08 22:33:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const clear = async () => {
|
2026-08-17 23:10:00 +00:00
|
|
|
await enqueue(() => SecureStorage.clear())
|
2026-06-08 22:33:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {get, set, clear}
|
|
|
|
|
})
|
|
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
const TABLES = [
|
|
|
|
|
{name: "events", keyPath: "id"},
|
|
|
|
|
{name: "relays", keyPath: "url"},
|
|
|
|
|
{name: "relayStats", keyPath: "url"},
|
|
|
|
|
{name: "handles", keyPath: "nip05"},
|
|
|
|
|
{name: "zappers", keyPath: "lnurl"},
|
|
|
|
|
{name: "plaintext", keyPath: "key"},
|
|
|
|
|
{name: "wrapManager", keyPath: "id"},
|
|
|
|
|
]
|
2025-09-30 23:11:49 +00:00
|
|
|
|
2026-04-10 17:28:51 +00:00
|
|
|
const FLUSH_INTERVAL = 3000
|
|
|
|
|
|
|
|
|
|
const idleWrite = <T>(f: (xs: T[]) => void): ((xs: T[]) => void) => {
|
|
|
|
|
if (typeof requestIdleCallback !== "undefined") {
|
|
|
|
|
return (xs: T[]) => requestIdleCallback(() => f(xs))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return f
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-12 01:49:22 +00:00
|
|
|
const kinds = {
|
2025-11-20 23:12:16 +00:00
|
|
|
meta: [PROFILE, FOLLOWS, MUTES, RELAYS, BLOSSOM_SERVERS, MESSAGING_RELAYS, APP_DATA, ROOMS],
|
2025-11-12 01:49:22 +00:00
|
|
|
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: [
|
2025-10-24 20:36:59 +00:00
|
|
|
ROOM_META,
|
2025-10-30 21:59:39 +00:00
|
|
|
ROOM_DELETE,
|
2025-11-03 19:10:16 +00:00
|
|
|
ROOM_ADMINS,
|
2025-10-24 20:36:59 +00:00
|
|
|
ROOM_MEMBERS,
|
|
|
|
|
ROOM_ADD_MEMBER,
|
|
|
|
|
ROOM_REMOVE_MEMBER,
|
|
|
|
|
ROOM_CREATE_PERMISSION,
|
2026-07-15 21:23:43 +00:00
|
|
|
ROOM_PINS,
|
2025-11-12 01:49:22 +00:00
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-18 16:20:30 +00:00
|
|
|
const isRelayScoped = (event: TrustedEvent) =>
|
|
|
|
|
kinds.space.includes(event.kind) || kinds.room.includes(event.kind)
|
|
|
|
|
|
2026-08-31 16:07:06 +00:00
|
|
|
const isMembershipChange = (event: TrustedEvent) =>
|
|
|
|
|
event.kind === ROOM_ADD_MEMBER || event.kind === ROOM_REMOVE_MEMBER
|
|
|
|
|
|
|
|
|
|
const shouldPersistEvent = (event: TrustedEvent, pubkey: string) =>
|
|
|
|
|
isMembershipChange(event)
|
|
|
|
|
? tagValues(hexTags("p"), event.tags).includes(pubkey)
|
|
|
|
|
: kinds.meta.includes(event.kind) || kinds.alert.includes(event.kind) || isRelayScoped(event)
|
2026-03-12 18:33:04 +00:00
|
|
|
|
2026-08-18 18:32:39 +00:00
|
|
|
type EventItem = {id: string; event: TrustedEvent; relays: string[]}
|
2025-11-12 01:49:22 +00:00
|
|
|
|
2026-03-12 18:33:04 +00:00
|
|
|
type PlaintextItem = {key: string; value: string}
|
|
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
/**
|
|
|
|
|
* Caches an app's repository, tracker and local collections in indexeddb. Everything stored
|
|
|
|
|
* here belongs to a single identity, so each gets its own database, named for the pubkey the
|
|
|
|
|
* policy below requires before it builds one.
|
|
|
|
|
*/
|
|
|
|
|
class Storage {
|
|
|
|
|
ready: Promise<void>
|
2025-11-12 01:49:22 +00:00
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
private db: IDB
|
|
|
|
|
private unsubscribers: Unsubscriber[] = []
|
|
|
|
|
private timeouts: ReturnType<typeof setTimeout>[] = []
|
|
|
|
|
private stopped = false
|
2025-11-12 01:49:22 +00:00
|
|
|
|
2026-08-31 16:07:06 +00:00
|
|
|
private readonly pubkey: string
|
|
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
constructor(private readonly app: IApp) {
|
|
|
|
|
// Every identity used to share one database; drop it rather than leave it on disk.
|
|
|
|
|
void deleteDB("flotilla-9gl")
|
2025-11-12 01:49:22 +00:00
|
|
|
|
2026-08-31 16:07:06 +00:00
|
|
|
this.pubkey = User.require(app).pubkey
|
|
|
|
|
this.db = new IDB({name: `flotilla-9gl-${this.pubkey}`, stores: TABLES})
|
2026-07-28 16:16:26 +00:00
|
|
|
this.ready = this.start()
|
2026-03-12 18:33:04 +00:00
|
|
|
}
|
|
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
close = () => this.db.close()
|
2025-09-30 23:11:49 +00:00
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
clear = () => this.db.clear()
|
2025-09-30 23:11:49 +00:00
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
cleanup = () => {
|
|
|
|
|
this.stopped = true
|
|
|
|
|
this.timeouts.forEach(clearTimeout)
|
|
|
|
|
this.unsubscribers.forEach(call)
|
|
|
|
|
this.close()
|
|
|
|
|
}
|
2025-09-30 23:11:49 +00:00
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
private start = async () => {
|
|
|
|
|
await this.db.connect()
|
2025-10-07 00:01:25 +00:00
|
|
|
|
2026-08-18 16:20:30 +00:00
|
|
|
const [, unsubscribeRelays] = await Promise.all([this.loadCriticalData(), this.initRelays()])
|
2025-09-30 23:11:49 +00:00
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
this.addUnsubscriber(this.syncEvents())
|
|
|
|
|
this.addUnsubscriber(this.syncTracker())
|
|
|
|
|
this.addUnsubscriber(unsubscribeRelays)
|
2025-09-30 23:11:49 +00:00
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
const defer = (init: () => Promise<Unsubscriber>) => {
|
|
|
|
|
this.timeouts.push(
|
|
|
|
|
setTimeout(async () => {
|
|
|
|
|
if (!this.stopped) {
|
|
|
|
|
this.addUnsubscriber(await init())
|
|
|
|
|
}
|
|
|
|
|
}, 0),
|
|
|
|
|
)
|
|
|
|
|
}
|
2025-09-30 23:11:49 +00:00
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
defer(this.initRelayStats)
|
|
|
|
|
defer(this.initHandles)
|
|
|
|
|
defer(this.initZappers)
|
|
|
|
|
defer(this.initPlaintext)
|
|
|
|
|
defer(this.initWrapManager)
|
2026-03-12 18:33:04 +00:00
|
|
|
}
|
2025-09-30 23:11:49 +00:00
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
private addUnsubscriber = (unsubscriber: Unsubscriber) => {
|
|
|
|
|
if (this.stopped) {
|
|
|
|
|
unsubscriber()
|
|
|
|
|
} else {
|
|
|
|
|
this.unsubscribers.push(unsubscriber)
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-30 23:11:49 +00:00
|
|
|
|
2026-08-18 16:20:30 +00:00
|
|
|
/**
|
2026-08-18 18:32:39 +00:00
|
|
|
* Each row holds an event together with the relays it came from, since a relay-scoped event
|
|
|
|
|
* that lost its provenance can never be keyed to a space again — it sits in the repository
|
|
|
|
|
* invisible, and negentropy reconciles it away as already-present, so it never comes back.
|
|
|
|
|
* Drop those and let the next sync pull them down again.
|
2026-08-18 16:20:30 +00:00
|
|
|
*/
|
|
|
|
|
private loadCriticalData = async () => {
|
2026-08-18 18:32:39 +00:00
|
|
|
const table = this.db.table<EventItem>("events")
|
|
|
|
|
const items: EventItem[] = []
|
|
|
|
|
const stale: string[] = []
|
|
|
|
|
|
|
|
|
|
for (const item of await table.getAll()) {
|
|
|
|
|
// Rows written before provenance was stored inline hold a bare event
|
|
|
|
|
if (
|
|
|
|
|
item.event &&
|
2026-08-31 16:07:06 +00:00
|
|
|
shouldPersistEvent(item.event, this.pubkey) &&
|
2026-08-18 18:32:39 +00:00
|
|
|
(!isRelayScoped(item.event) || item.relays.length > 0)
|
|
|
|
|
) {
|
|
|
|
|
item.event[verifiedSymbol] = true
|
|
|
|
|
items.push(item)
|
2026-07-28 16:16:26 +00:00
|
|
|
} else {
|
2026-08-18 18:32:39 +00:00
|
|
|
stale.push(item.id)
|
2026-07-28 16:16:26 +00:00
|
|
|
}
|
|
|
|
|
}
|
2025-10-07 00:01:25 +00:00
|
|
|
|
2026-08-18 18:32:39 +00:00
|
|
|
this.app.repository.load(items.map(item => item.event))
|
2026-08-18 16:20:30 +00:00
|
|
|
|
2026-08-18 18:32:39 +00:00
|
|
|
const relaysById = new Map<string, Set<string>>()
|
2026-08-18 16:20:30 +00:00
|
|
|
|
2026-08-18 18:32:39 +00:00
|
|
|
for (const {id, relays} of items) {
|
|
|
|
|
// Anything the repository rejected was superseded by an event that arrived before we
|
|
|
|
|
// finished loading
|
|
|
|
|
if (this.app.repository.getEvent(id)) {
|
|
|
|
|
relaysById.set(id, new Set(relays))
|
|
|
|
|
} else {
|
|
|
|
|
stale.push(id)
|
|
|
|
|
}
|
2026-08-18 16:20:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.app.tracker.load(relaysById)
|
|
|
|
|
|
2026-08-18 18:32:39 +00:00
|
|
|
if (stale.length > 0) {
|
|
|
|
|
void table.bulkDelete(stale)
|
2026-07-28 16:16:26 +00:00
|
|
|
}
|
|
|
|
|
}
|
2025-10-24 16:38:57 +00:00
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
private syncEvents = () => {
|
2026-08-18 18:32:39 +00:00
|
|
|
const table = this.db.table<EventItem>("events")
|
2026-07-28 16:16:26 +00:00
|
|
|
|
|
|
|
|
return on(
|
|
|
|
|
this.app.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) {
|
2026-08-31 16:07:06 +00:00
|
|
|
if (shouldPersistEvent(event, this.pubkey)) {
|
2026-07-28 16:16:26 +00:00
|
|
|
add.push(event)
|
|
|
|
|
remove.delete(event.id)
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-24 16:38:57 +00:00
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
for (const id of update.removed) {
|
|
|
|
|
remove.add(id)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-12 18:33:04 +00:00
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
if (add.length > 0) {
|
2026-08-17 17:17:12 +00:00
|
|
|
// The ingest policy tracks an event before publishing it, so by the time the
|
2026-08-18 18:32:39 +00:00
|
|
|
// repository reports it, its provenance is already in the tracker
|
|
|
|
|
await table.bulkPut(
|
|
|
|
|
add.map(event => ({
|
|
|
|
|
id: event.id,
|
|
|
|
|
event,
|
|
|
|
|
relays: Array.from(this.app.tracker.getRelays(event.id)),
|
|
|
|
|
})),
|
|
|
|
|
)
|
2026-07-28 16:16:26 +00:00
|
|
|
}
|
2025-10-24 16:38:57 +00:00
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
if (remove.size > 0) {
|
2026-08-18 18:32:39 +00:00
|
|
|
await table.bulkDelete(remove)
|
2026-07-28 16:16:26 +00:00
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
)
|
2026-03-12 18:33:04 +00:00
|
|
|
}
|
|
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
private syncTracker = () => {
|
2026-08-18 18:32:39 +00:00
|
|
|
const table = this.db.table<EventItem>("events")
|
2025-09-30 23:11:49 +00:00
|
|
|
|
2026-08-18 18:32:39 +00:00
|
|
|
const _onChange = async (ids: Iterable<string>) => {
|
|
|
|
|
const items: EventItem[] = []
|
2025-09-30 23:11:49 +00:00
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
for (const id of ids) {
|
|
|
|
|
const event = this.app.repository.getEvent(id)
|
2025-10-07 00:01:25 +00:00
|
|
|
|
2026-08-17 17:17:12 +00:00
|
|
|
// A brand-new event is tracked before it's published, so it isn't queryable here —
|
|
|
|
|
// syncEvents persists its provenance along with the event itself. This pass only
|
2026-08-18 18:32:39 +00:00
|
|
|
// records relay changes for events we already have.
|
2026-08-31 16:07:06 +00:00
|
|
|
if (event && shouldPersistEvent(event, this.pubkey)) {
|
2026-08-18 18:32:39 +00:00
|
|
|
items.push({id, event, relays: Array.from(this.app.tracker.getRelays(id))})
|
|
|
|
|
}
|
2026-07-28 16:16:26 +00:00
|
|
|
}
|
2025-10-07 00:01:25 +00:00
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
await table.bulkPut(items)
|
|
|
|
|
}
|
2025-09-30 23:11:49 +00:00
|
|
|
|
2026-08-18 18:32:39 +00:00
|
|
|
const onAdd = batch(3000, _onChange)
|
|
|
|
|
const onRemove = batch(3000, _onChange)
|
|
|
|
|
const onLoad = () => _onChange(this.app.tracker.relaysById.keys())
|
2026-07-28 16:16:26 +00:00
|
|
|
|
|
|
|
|
this.app.tracker.on("add", onAdd)
|
|
|
|
|
this.app.tracker.on("remove", onRemove)
|
|
|
|
|
this.app.tracker.on("load", onLoad)
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
this.app.tracker.off("add", onAdd)
|
|
|
|
|
this.app.tracker.off("remove", onRemove)
|
|
|
|
|
this.app.tracker.off("load", onLoad)
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-30 23:11:49 +00:00
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
private initRelays = async () => {
|
|
|
|
|
const table = this.db.table<RelayInfo & {url: string}>("relays")
|
2026-03-12 18:33:04 +00:00
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
for (const row of await table.getAll()) {
|
|
|
|
|
this.app.use(Relays).set(row.url, new Relay(row.url, row))
|
|
|
|
|
}
|
2025-10-07 00:01:25 +00:00
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
const enqueue = batch(
|
|
|
|
|
FLUSH_INTERVAL,
|
|
|
|
|
idleWrite((relays: Relay[]) => table.bulkPut(relays.map(relay => ({...relay})))),
|
|
|
|
|
)
|
2025-10-07 00:01:25 +00:00
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
return this.app.use(Relays).onItem((_url, relay) => {
|
|
|
|
|
if (relay) enqueue(relay)
|
|
|
|
|
})
|
|
|
|
|
}
|
2026-03-12 18:33:04 +00:00
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
private initRelayStats = async () => {
|
|
|
|
|
const table = this.db.table<RelayStatsItem>("relayStats")
|
2025-09-30 23:11:49 +00:00
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
for (const row of await table.getAll()) {
|
|
|
|
|
this.app.use(RelayStats).set(row.url, row)
|
|
|
|
|
}
|
2026-03-12 18:33:04 +00:00
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
const enqueue = batch(FLUSH_INTERVAL, idleWrite(table.bulkPut))
|
2026-03-12 18:33:04 +00:00
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
return this.app.use(RelayStats).onItem((_url, stats) => {
|
|
|
|
|
if (stats) enqueue(stats)
|
|
|
|
|
})
|
2026-03-12 18:33:04 +00:00
|
|
|
}
|
2025-09-30 23:11:49 +00:00
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
private initHandles = async () => {
|
|
|
|
|
const table = this.db.table<Handle>("handles")
|
2026-03-12 18:33:04 +00:00
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
for (const row of await table.getAll()) {
|
|
|
|
|
this.app.use(Handles).set(row.nip05, row)
|
2026-03-12 18:33:04 +00:00
|
|
|
}
|
|
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
const enqueue = batch(FLUSH_INTERVAL, idleWrite(table.bulkPut))
|
2025-11-12 01:49:22 +00:00
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
return this.app.use(Handles).onItem((_nip05, handle) => {
|
|
|
|
|
if (handle) enqueue(handle)
|
|
|
|
|
})
|
2026-03-12 18:33:04 +00:00
|
|
|
}
|
2025-10-17 17:19:21 +00:00
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
private initZappers = async () => {
|
|
|
|
|
const table = this.db.table<ZapperValues>("zappers")
|
2025-10-17 17:19:21 +00:00
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
for (const row of await table.getAll()) {
|
|
|
|
|
// Validation is meaningless without these, and rows cached before they were required
|
|
|
|
|
// won't have them.
|
|
|
|
|
if (row.pubkey && row.nostrPubkey) {
|
|
|
|
|
this.app.use(Zappers).set(row.lnurl, new Zapper(row))
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-12 18:33:04 +00:00
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
const enqueue = batch(
|
|
|
|
|
FLUSH_INTERVAL,
|
|
|
|
|
idleWrite((zappers: Zapper[]) => table.bulkPut(zappers.map(zapper => ({...zapper})))),
|
|
|
|
|
)
|
2026-03-12 18:33:04 +00:00
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
return this.app.use(Zappers).onItem((_lnurl, zapper) => {
|
|
|
|
|
if (zapper) enqueue(zapper)
|
2025-11-12 01:49:22 +00:00
|
|
|
})
|
2026-07-28 16:16:26 +00:00
|
|
|
}
|
2025-10-17 17:19:21 +00:00
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
private initPlaintext = async () => {
|
|
|
|
|
const table = this.db.table<PlaintextItem>("plaintext")
|
2025-11-03 22:43:27 +00:00
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
for (const {key, value} of await table.getAll()) {
|
|
|
|
|
this.app.use(Plaintext).set(key, value)
|
|
|
|
|
}
|
2025-11-03 22:43:27 +00:00
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
const enqueue = batch(FLUSH_INTERVAL, idleWrite(table.bulkPut))
|
2025-10-21 15:27:30 +00:00
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
return this.app.use(Plaintext).onItem((key, value) => {
|
|
|
|
|
if (value) enqueue({key, value})
|
2026-03-12 18:33:04 +00:00
|
|
|
})
|
2026-07-28 16:16:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private initWrapManager = async () => {
|
|
|
|
|
const table = this.db.table<WrapItem>("wrapManager")
|
|
|
|
|
|
|
|
|
|
this.app.wrapManager.load(await table.getAll())
|
2026-03-12 18:33:04 +00:00
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
const addOne = batch(3000, table.bulkPut)
|
|
|
|
|
const removeOne = throttle(3000, table.bulkDelete)
|
2025-11-12 01:49:22 +00:00
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
this.app.wrapManager.on("add", addOne)
|
|
|
|
|
this.app.wrapManager.on("remove", removeOne)
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
this.app.wrapManager.off("add", addOne)
|
|
|
|
|
this.app.wrapManager.off("remove", removeOne)
|
2025-11-12 01:49:22 +00:00
|
|
|
}
|
2026-07-28 16:16:26 +00:00
|
|
|
}
|
|
|
|
|
}
|
2025-11-12 01:49:22 +00:00
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
// The current app's cache, which only exists once there's an identity to cache for.
|
|
|
|
|
export const storage = withGetter(writable<Maybe<Storage>>(undefined))
|
|
|
|
|
|
|
|
|
|
// Storage is scoped to one app's repository, tracker and caches, so it's built and torn down
|
|
|
|
|
// with the app rather than living on as a module-level singleton.
|
|
|
|
|
export const storagePolicy: AppPolicy = $app => {
|
|
|
|
|
if ($app.user) {
|
|
|
|
|
const $storage = new Storage($app)
|
|
|
|
|
|
|
|
|
|
storage.set($storage)
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
$storage.cleanup()
|
|
|
|
|
storage.set(undefined)
|
|
|
|
|
}
|
2026-03-12 18:33:04 +00:00
|
|
|
}
|
|
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
return noop
|
2026-03-12 18:33:04 +00:00
|
|
|
}
|
2026-07-28 16:16:26 +00:00
|
|
|
|
|
|
|
|
appPolicies.push(storagePolicy)
|