2026-07-28 16:16:26 +00:00
|
|
|
import {derived, writable} from "svelte/store"
|
|
|
|
|
import type {Readable} from "svelte/store"
|
|
|
|
|
import {always} from "@welshman/lib"
|
|
|
|
|
import type {Maybe} from "@welshman/lib"
|
|
|
|
|
import {withGetter} from "@welshman/store"
|
2026-07-30 01:24:57 +00:00
|
|
|
import type {ReadableWithGetter} from "@welshman/store"
|
2026-07-28 16:16:26 +00:00
|
|
|
import {
|
|
|
|
|
App,
|
|
|
|
|
BlockedRelayLists,
|
|
|
|
|
BlossomServerLists,
|
|
|
|
|
Deletes,
|
|
|
|
|
Domain,
|
|
|
|
|
FollowLists,
|
|
|
|
|
Handles,
|
2026-08-13 19:57:42 +00:00
|
|
|
Logger,
|
2026-07-28 16:16:26 +00:00
|
|
|
MessagingRelayLists,
|
|
|
|
|
MuteLists,
|
|
|
|
|
Network,
|
|
|
|
|
Profiles,
|
|
|
|
|
Reactions,
|
|
|
|
|
RelayLists,
|
|
|
|
|
RelayManagement,
|
|
|
|
|
RelayMemberLists,
|
|
|
|
|
RelayRoles,
|
|
|
|
|
RelayStats,
|
|
|
|
|
Relays,
|
|
|
|
|
RoomLists,
|
|
|
|
|
RoomPinLists,
|
|
|
|
|
Rooms,
|
|
|
|
|
Router,
|
|
|
|
|
SearchRelayLists,
|
|
|
|
|
Thunks,
|
|
|
|
|
User,
|
|
|
|
|
Wot,
|
|
|
|
|
Wraps,
|
|
|
|
|
appPolicyCacheDecrypt,
|
|
|
|
|
appPolicyLogSignerMethods,
|
|
|
|
|
appPolicyRelayStats,
|
|
|
|
|
appPolicyWraps,
|
|
|
|
|
} from "@welshman/app"
|
2026-07-30 01:24:57 +00:00
|
|
|
import type {AppPolicy, DerivedPlugin, Plugin, Session} from "@welshman/app"
|
2026-07-28 16:16:26 +00:00
|
|
|
import type {BaseEventReader, EventWriter, KindFactory} from "@welshman/domain"
|
|
|
|
|
import {DEFAULT_RELAYS, DEFAULT_SEARCH_RELAYS, DUFFLEPUD_URL, INDEXER_RELAYS} from "@app/env"
|
|
|
|
|
|
|
|
|
|
// Flotilla's own policies (ingest, sockets, storage) can't be imported here — they depend on
|
|
|
|
|
// this module — so they push themselves in on import, and the first app is built lazily so
|
|
|
|
|
// they're all registered by the time it exists.
|
|
|
|
|
export const appPolicies: AppPolicy[] = [
|
|
|
|
|
appPolicyWraps,
|
|
|
|
|
appPolicyRelayStats,
|
|
|
|
|
appPolicyCacheDecrypt,
|
|
|
|
|
appPolicyLogSignerMethods,
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
const makeApp = (user?: User) => {
|
|
|
|
|
const instance: App = new App({
|
|
|
|
|
user,
|
|
|
|
|
config: {
|
|
|
|
|
dufflepudUrl: DUFFLEPUD_URL,
|
|
|
|
|
getDefaultRelays: always(DEFAULT_RELAYS),
|
|
|
|
|
getIndexerRelays: always(INDEXER_RELAYS),
|
|
|
|
|
getSearchRelays: () => {
|
|
|
|
|
const urls = user ? instance.use(SearchRelayLists).urls(user.pubkey).get() : []
|
|
|
|
|
|
|
|
|
|
return urls.length > 0 ? urls : DEFAULT_SEARCH_RELAYS
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
policies: appPolicies,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return instance
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const appStore = withGetter(writable<Maybe<App>>(undefined))
|
|
|
|
|
|
|
|
|
|
const getApp = () => appStore.get() ?? setApp(makeApp())
|
|
|
|
|
|
|
|
|
|
const setApp = (instance: App) => {
|
|
|
|
|
appStore.set(instance)
|
|
|
|
|
|
|
|
|
|
return instance
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// An app is scoped to a single identity, so logging in replaces it wholesale rather than
|
|
|
|
|
// attaching a user to the anonymous one.
|
|
|
|
|
export const app: ReadableWithGetter<App> = {
|
|
|
|
|
get: getApp,
|
|
|
|
|
subscribe: run => {
|
|
|
|
|
getApp()
|
|
|
|
|
|
|
|
|
|
return appStore.subscribe($app => run($app!))
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const session = withGetter(writable<Maybe<Session>>(undefined))
|
|
|
|
|
|
|
|
|
|
// The signed-in user, for the paths that require one — reading it while signed out throws, so
|
|
|
|
|
// use `$app.user` where absence is a legitimate state.
|
|
|
|
|
export const user = withGetter(derived(app, $app => User.require($app)))
|
|
|
|
|
|
|
|
|
|
// Read a store off the current app, re-subscribing when login swaps in a new one. Anything
|
|
|
|
|
// bound at module load has to go through this or it will keep reading a discarded app.
|
|
|
|
|
export const fromApp = <T>(read: ($app: App) => Readable<T>): Readable<T> =>
|
|
|
|
|
derived(app, ($app, set: (value: T) => void) => read($app).subscribe(set))
|
|
|
|
|
|
2026-07-30 01:24:57 +00:00
|
|
|
// The signed-in user's entry in a keyed collection, e.g. deriveUserItem(Profiles).
|
|
|
|
|
export const deriveUserItem = <T>(Ctor: Plugin<DerivedPlugin<T>>) =>
|
|
|
|
|
derived(app, ($app, set: (item: Maybe<T>) => void) => {
|
|
|
|
|
let previous: Maybe<T>
|
2026-07-28 16:16:26 +00:00
|
|
|
|
2026-07-30 01:24:57 +00:00
|
|
|
return $app.use(Ctor).index.$.subscribe($index => {
|
|
|
|
|
const item = $app.user ? $index.get($app.user.pubkey) : undefined
|
|
|
|
|
|
|
|
|
|
if (item !== previous) {
|
|
|
|
|
previous = item
|
|
|
|
|
set(item)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
})
|
2026-07-28 16:16:26 +00:00
|
|
|
|
|
|
|
|
export const login = async ($session: Session) => {
|
|
|
|
|
const $user = await User.fromSession($session)
|
|
|
|
|
|
|
|
|
|
if (!$user) {
|
|
|
|
|
throw new Error(`Unable to log in using ${$session.method}`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Read the store directly, so restoring a session at startup doesn't build an anonymous app
|
|
|
|
|
// just to tear it down.
|
|
|
|
|
appStore.get()?.cleanup()
|
|
|
|
|
setApp(makeApp($user))
|
|
|
|
|
session.set($session)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Plugins bound to the current app, so `$profiles` in a component and `profiles.get()` in a
|
|
|
|
|
// module both stay pointed at the right one after login swaps it. Flotilla's own plugins expose
|
|
|
|
|
// themselves the same way.
|
2026-07-30 01:24:57 +00:00
|
|
|
export const usePlugin = <T>(Ctor: Plugin<T>) => withGetter(derived(app, $app => $app.use(Ctor)))
|
2026-07-28 16:16:26 +00:00
|
|
|
|
|
|
|
|
export const blockedRelayLists = usePlugin(BlockedRelayLists)
|
|
|
|
|
export const blossomServerLists = usePlugin(BlossomServerLists)
|
|
|
|
|
export const deletes = usePlugin(Deletes)
|
|
|
|
|
export const domain = usePlugin(Domain)
|
|
|
|
|
export const followLists = usePlugin(FollowLists)
|
|
|
|
|
export const handles = usePlugin(Handles)
|
2026-08-13 19:57:42 +00:00
|
|
|
export const logger = usePlugin(Logger)
|
2026-07-28 16:16:26 +00:00
|
|
|
export const messagingRelayLists = usePlugin(MessagingRelayLists)
|
|
|
|
|
export const muteLists = usePlugin(MuteLists)
|
|
|
|
|
export const network = usePlugin(Network)
|
|
|
|
|
export const profiles = usePlugin(Profiles)
|
|
|
|
|
export const reactions = usePlugin(Reactions)
|
|
|
|
|
export const relayLists = usePlugin(RelayLists)
|
|
|
|
|
export const relayManagement = usePlugin(RelayManagement)
|
|
|
|
|
export const relayMemberLists = usePlugin(RelayMemberLists)
|
|
|
|
|
export const relayRoles = usePlugin(RelayRoles)
|
|
|
|
|
export const relayStats = usePlugin(RelayStats)
|
|
|
|
|
export const relays = usePlugin(Relays)
|
|
|
|
|
export const roomLists = usePlugin(RoomLists)
|
|
|
|
|
export const roomPinLists = usePlugin(RoomPinLists)
|
|
|
|
|
export const rooms = usePlugin(Rooms)
|
|
|
|
|
export const router = usePlugin(Router)
|
|
|
|
|
export const searchRelayLists = usePlugin(SearchRelayLists)
|
|
|
|
|
export const thunks = usePlugin(Thunks)
|
|
|
|
|
export const wot = usePlugin(Wot)
|
|
|
|
|
export const wraps = usePlugin(Wraps)
|
|
|
|
|
|
|
|
|
|
// The domain entry points, since almost every read or write goes through one of them.
|
|
|
|
|
export const reader = <R extends BaseEventReader, W extends EventWriter<R>>(
|
|
|
|
|
factory: KindFactory<R, W>,
|
|
|
|
|
) => domain.get().reader(factory)
|
|
|
|
|
|
|
|
|
|
export const writer = <R extends BaseEventReader, W extends EventWriter<R>>(
|
|
|
|
|
factory: KindFactory<R, W>,
|
|
|
|
|
seed?: R,
|
|
|
|
|
) => domain.get().writer(factory, seed)
|
|
|
|
|
|
|
|
|
|
export const command = (eventWriter: EventWriter<any>) => domain.get().command(eventWriter)
|