diff --git a/src/app/storage.ts b/src/app/storage.ts index fec09344..880d4e9c 100644 --- a/src/app/storage.ts +++ b/src/app/storage.ts @@ -3,7 +3,7 @@ import type {Unsubscriber} from "svelte/store" import {deleteDB} from "idb" import {SecureStorage} from "@aparajita/capacitor-secure-storage" import {Preferences} from "@capacitor/preferences" -import {noop, on, throttle, batch, call, makeQueue} from "@welshman/lib" +import {noop, on, throttle, batch, call, addToMapKey, makeQueue} from "@welshman/lib" import type {Maybe} from "@welshman/lib" import { ALERT_ANDROID, @@ -146,11 +146,13 @@ const kinds = { ], } +// Space and room events mean nothing apart from the relay they came from — welshman keys them +// per relay, and only counts room state the relay itself signed. +const isRelayScoped = (event: TrustedEvent) => + kinds.space.includes(event.kind) || kinds.room.includes(event.kind) + const shouldPersistEvent = (event: TrustedEvent) => - kinds.meta.includes(event.kind) || - kinds.alert.includes(event.kind) || - kinds.space.includes(event.kind) || - kinds.room.includes(event.kind) + kinds.meta.includes(event.kind) || kinds.alert.includes(event.kind) || isRelayScoped(event) type TrackerItem = {id: string; relays: string[]} @@ -191,9 +193,7 @@ class Storage { private start = async () => { await this.db.connect() - const [, unsubscribeRelays] = await Promise.all([this.loadCriticalEvents(), this.initRelays()]) - - await this.loadCriticalTracker() + const [, unsubscribeRelays] = await Promise.all([this.loadCriticalData(), this.initRelays()]) this.addUnsubscriber(this.syncEvents()) this.addUnsubscriber(this.syncTracker()) @@ -224,25 +224,57 @@ class Storage { } } - private loadCriticalEvents = async () => { - const table = this.db.table("events") - const initialEvents = await table.getAll() - const keep: TrustedEvent[] = [] - const drop: string[] = [] + /** + * Events load together with the relays they 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. + */ + private loadCriticalData = async () => { + const eventsTable = this.db.table("events") + const trackerTable = this.db.table("tracker") + const [storedEvents, storedItems] = await Promise.all([ + eventsTable.getAll(), + trackerTable.getAll(), + ]) - for (const event of initialEvents) { - if (shouldPersistEvent(event)) { + const relaysById = new Map(storedItems.map(({id, relays}) => [id, new Set(relays)])) + const events: TrustedEvent[] = [] + const staleEvents: string[] = [] + + for (const event of storedEvents) { + if (shouldPersistEvent(event) && (!isRelayScoped(event) || relaysById.has(event.id))) { event[verifiedSymbol] = true - keep.push(event) + events.push(event) } else { - drop.push(event.id) + staleEvents.push(event.id) } } - this.app.repository.load(keep) + for (const [id, relays] of this.app.tracker.relaysById) { + for (const relay of relays) { + addToMapKey(relaysById, id, relay) + } + } - if (drop.length > 0) { - void table.bulkDelete(drop) + this.app.repository.load([...this.app.repository.dump(), ...events]) + + const staleItems = storedItems + .map(item => item.id) + .filter(id => !this.app.repository.getEvent(id)) + + for (const id of staleItems) { + relaysById.delete(id) + } + + this.app.tracker.load(relaysById) + + if (staleEvents.length > 0) { + void eventsTable.bulkDelete(staleEvents) + } + + if (staleItems.length > 0) { + void trackerTable.bulkDelete(staleItems) } } @@ -296,26 +328,6 @@ class Storage { ) } - private loadCriticalTracker = async () => { - const table = this.db.table("tracker") - const relaysById = new Map>() - const stale: string[] = [] - - for (const {id, relays} of await table.getAll()) { - if (this.app.repository.getEvent(id)) { - relaysById.set(id, new Set(relays)) - } else { - stale.push(id) - } - } - - this.app.tracker.load(relaysById) - - if (stale.length > 0) { - void table.bulkDelete(stale) - } - } - private syncTracker = () => { const table = this.db.table("tracker") diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index ce1f50fe..b96dd6c9 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -14,14 +14,13 @@ import {context as pomadeContext} from "@pomade/core" import {sync, throttled} from "@welshman/store" import {setNip55Plugin} from "@welshman/signer" - import * as domain from "@welshman/domain" import * as util from "@welshman/util" import * as lib from "@welshman/lib" import {Logger} from "@welshman/app" import {isMobile, documentActive} from "@lib/html" import AppContainer from "@app/components/AppContainer.svelte" import ModalContainer from "@app/components/ModalContainer.svelte" - import {app} from "@app/core" + import * as core from "@app/core" import {setupHistory} from "@app/routes" import {setupAnalytics} from "@app/analytics" import {setupLogging} from "@app/logger" @@ -65,7 +64,7 @@ ) // Add stuff to window for convenience - Object.assign(window, {get, nip19, theme, app, domain, Logger, ...lib, ...util}) + Object.assign(window, {get, nip19, theme, Logger, ...lib, ...util, ...core}) // Set up context for various modules pomadeContext.setSignerUrls(env.POMADE_SIGNERS) @@ -214,14 +213,21 @@ unsubscribers.push(Push.sync()) // Logging in swaps in a fresh app — its policies rebind themselves, we just have to sync - // application data against the new identity's relays. - let currentApp = app.get() + // application data against the new identity's relays. Loading the new app's storage + // replaces its repository and tracker wholesale, so wait for it the way startup does. + let currentApp = core.app.get() + + const resync = async () => { + await storage.get()?.ready + + syncApplicationData() + } unsubscribers.push( - app.subscribe($app => { + core.app.subscribe($app => { if ($app !== currentApp) { currentApp = $app - syncApplicationData() + resync() } }), ) @@ -282,7 +288,8 @@ }) $effect(() => { - const title = getPageTitle({page: $page, pubkey: $app.user?.pubkey}) + const {user} = core.app.get() + const title = getPageTitle({page: $page, pubkey: user?.pubkey}) // While the tab isn't actively focused the user isn't actually looking at the // active page, so count notifications for it too rather than treating it as read. const paths = $documentActive ? $notificationPaths : $allNotifications