From c190d2c8ce4034e7b720559edcbaf9641c378e18 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 1 May 2024 14:56:47 -0500 Subject: [PATCH] Refactor Storages to get lazy-loaded only when they are used --- src/controllers/api/accounts.ts | 18 ++-- src/controllers/api/admin.ts | 6 +- src/controllers/api/blocks.ts | 4 +- src/controllers/api/bookmarks.ts | 4 +- src/controllers/api/ditto.ts | 6 +- src/controllers/api/instance.ts | 4 +- src/controllers/api/notifications.ts | 4 +- src/controllers/api/pleroma.ts | 4 +- src/controllers/api/search.ts | 10 +-- src/controllers/api/statuses.ts | 8 +- src/controllers/api/streaming.ts | 5 +- src/controllers/nostr/relay-info.ts | 4 +- src/controllers/nostr/relay.ts | 5 +- src/db/users.ts | 4 +- src/middleware/store.ts | 8 +- src/pipeline.ts | 26 +++--- src/pipeline/DVM.ts | 4 +- src/queries.ts | 18 ++-- src/stats.ts | 8 +- src/storages.ts | 118 ++++++++++++++++++--------- src/storages/adminStore.ts | 7 -- src/utils/api.ts | 6 +- src/utils/nip05.ts | 4 +- src/utils/outbox.ts | 4 +- src/views.ts | 16 ++-- src/views/mastodon/relationships.ts | 4 +- src/views/mastodon/statuses.ts | 6 +- 27 files changed, 175 insertions(+), 140 deletions(-) delete mode 100644 src/storages/adminStore.ts diff --git a/src/controllers/api/accounts.ts b/src/controllers/api/accounts.ts index a3ac3786..96d4afa5 100644 --- a/src/controllers/api/accounts.ts +++ b/src/controllers/api/accounts.ts @@ -7,7 +7,7 @@ import { Conf } from '@/config.ts'; import { getAuthor, getFollowedPubkeys } from '@/queries.ts'; import { booleanParamSchema, fileSchema } from '@/schema.ts'; import { jsonMetaContentSchema } from '@/schemas/nostr.ts'; -import { eventsDB, searchStore } from '@/storages.ts'; +import { Storages } from '@/storages.ts'; import { addTag, deleteTag, findReplyTag, getTagSet } from '@/tags.ts'; import { uploadFile } from '@/upload.ts'; import { nostrNow } from '@/utils.ts'; @@ -92,12 +92,12 @@ const accountSearchController: AppController = async (c) => { const [event, events] = await Promise.all([ lookupAccount(query), - searchStore.query([{ kinds: [0], search: query, limit: 20 }], { signal: c.req.raw.signal }), + Storages.search.query([{ kinds: [0], search: query, limit: 20 }], { signal: c.req.raw.signal }), ]); const results = await hydrateEvents({ events: event ? [event, ...events] : events, - storage: eventsDB, + storage: Storages.db, signal: c.req.raw.signal, }); @@ -143,7 +143,7 @@ const accountStatusesController: AppController = async (c) => { const { signal } = c.req.raw; if (pinned) { - const [pinEvent] = await eventsDB.query([{ kinds: [10001], authors: [pubkey], limit: 1 }], { signal }); + const [pinEvent] = await Storages.db.query([{ kinds: [10001], authors: [pubkey], limit: 1 }], { signal }); if (pinEvent) { const pinnedEventIds = getTagSet(pinEvent.tags, 'e'); return renderStatuses(c, [...pinnedEventIds].reverse()); @@ -164,8 +164,8 @@ const accountStatusesController: AppController = async (c) => { filter['#t'] = [tagged]; } - const events = await eventsDB.query([filter], { signal }) - .then((events) => hydrateEvents({ events, storage: eventsDB, signal })) + const events = await Storages.db.query([filter], { signal }) + .then((events) => hydrateEvents({ events, storage: Storages.db, signal })) .then((events) => { if (exclude_replies) { return events.filter((event) => !findReplyTag(event.tags)); @@ -306,7 +306,7 @@ const favouritesController: AppController = async (c) => { const params = paginationSchema.parse(c.req.query()); const { signal } = c.req.raw; - const events7 = await eventsDB.query( + const events7 = await Storages.db.query( [{ kinds: [7], authors: [pubkey], ...params }], { signal }, ); @@ -315,8 +315,8 @@ const favouritesController: AppController = async (c) => { .map((event) => event.tags.find((tag) => tag[0] === 'e')?.[1]) .filter((id): id is string => !!id); - const events1 = await eventsDB.query([{ kinds: [1], ids }], { signal }) - .then((events) => hydrateEvents({ events, storage: eventsDB, signal })); + const events1 = await Storages.db.query([{ kinds: [1], ids }], { signal }) + .then((events) => hydrateEvents({ events, storage: Storages.db, signal })); const statuses = await Promise.all(events1.map((event) => renderStatus(event, { viewerPubkey: c.get('pubkey') }))); return paginated(c, events1, statuses); diff --git a/src/controllers/api/admin.ts b/src/controllers/api/admin.ts index 990c0fc5..15420564 100644 --- a/src/controllers/api/admin.ts +++ b/src/controllers/api/admin.ts @@ -3,7 +3,7 @@ import { z } from 'zod'; import { type AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; import { booleanParamSchema } from '@/schema.ts'; -import { eventsDB } from '@/storages.ts'; +import { Storages } from '@/storages.ts'; import { renderAdminAccount } from '@/views/mastodon/admin-accounts.ts'; import { paginated, paginationSchema } from '@/utils/api.ts'; @@ -41,9 +41,9 @@ const adminAccountsController: AppController = async (c) => { const { since, until, limit } = paginationSchema.parse(c.req.query()); const { signal } = c.req.raw; - const events = await eventsDB.query([{ kinds: [30361], authors: [Conf.pubkey], since, until, limit }], { signal }); + const events = await Storages.db.query([{ kinds: [30361], authors: [Conf.pubkey], since, until, limit }], { signal }); const pubkeys = events.map((event) => event.tags.find(([name]) => name === 'd')?.[1]!); - const authors = await eventsDB.query([{ kinds: [0], authors: pubkeys }], { signal }); + const authors = await Storages.db.query([{ kinds: [0], authors: pubkeys }], { signal }); for (const event of events) { const d = event.tags.find(([name]) => name === 'd')?.[1]; diff --git a/src/controllers/api/blocks.ts b/src/controllers/api/blocks.ts index d54773ac..16fa5cb1 100644 --- a/src/controllers/api/blocks.ts +++ b/src/controllers/api/blocks.ts @@ -1,5 +1,5 @@ import { type AppController } from '@/app.ts'; -import { eventsDB } from '@/storages.ts'; +import { Storages } from '@/storages.ts'; import { getTagSet } from '@/tags.ts'; import { renderAccounts } from '@/views.ts'; @@ -8,7 +8,7 @@ const blocksController: AppController = async (c) => { const pubkey = c.get('pubkey')!; const { signal } = c.req.raw; - const [event10000] = await eventsDB.query( + const [event10000] = await Storages.db.query( [{ kinds: [10000], authors: [pubkey], limit: 1 }], { signal }, ); diff --git a/src/controllers/api/bookmarks.ts b/src/controllers/api/bookmarks.ts index 16e87e76..8d44f953 100644 --- a/src/controllers/api/bookmarks.ts +++ b/src/controllers/api/bookmarks.ts @@ -1,5 +1,5 @@ import { type AppController } from '@/app.ts'; -import { eventsDB } from '@/storages.ts'; +import { Storages } from '@/storages.ts'; import { getTagSet } from '@/tags.ts'; import { renderStatuses } from '@/views.ts'; @@ -8,7 +8,7 @@ const bookmarksController: AppController = async (c) => { const pubkey = c.get('pubkey')!; const { signal } = c.req.raw; - const [event10003] = await eventsDB.query( + const [event10003] = await Storages.db.query( [{ kinds: [10003], authors: [pubkey], limit: 1 }], { signal }, ); diff --git a/src/controllers/api/ditto.ts b/src/controllers/api/ditto.ts index 425dcfba..e9485932 100644 --- a/src/controllers/api/ditto.ts +++ b/src/controllers/api/ditto.ts @@ -3,7 +3,7 @@ import { z } from 'zod'; import { AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; -import { eventsDB } from '@/storages.ts'; +import { Storages } from '@/storages.ts'; import { AdminSigner } from '@/signers/AdminSigner.ts'; const relaySchema = z.object({ @@ -15,7 +15,7 @@ const relaySchema = z.object({ type RelayEntity = z.infer; export const adminRelaysController: AppController = async (c) => { - const [event] = await eventsDB.query([ + const [event] = await Storages.db.query([ { kinds: [10002], authors: [Conf.pubkey], limit: 1 }, ]); @@ -36,7 +36,7 @@ export const adminSetRelaysController: AppController = async (c) => { created_at: Math.floor(Date.now() / 1000), }); - await eventsDB.event(event); + await Storages.db.event(event); return c.json(renderRelays(event)); }; diff --git a/src/controllers/api/instance.ts b/src/controllers/api/instance.ts index 1355330f..d4239d31 100644 --- a/src/controllers/api/instance.ts +++ b/src/controllers/api/instance.ts @@ -1,13 +1,13 @@ import { type AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; import { jsonServerMetaSchema } from '@/schemas/nostr.ts'; -import { eventsDB } from '@/storages.ts'; +import { Storages } from '@/storages.ts'; const instanceController: AppController = async (c) => { const { host, protocol } = Conf.url; const { signal } = c.req.raw; - const [event] = await eventsDB.query([{ kinds: [0], authors: [Conf.pubkey], limit: 1 }], { signal }); + const [event] = await Storages.db.query([{ kinds: [0], authors: [Conf.pubkey], limit: 1 }], { signal }); const meta = jsonServerMetaSchema.parse(event?.content); /** Protocol to use for WebSocket URLs, depending on the protocol of the `LOCAL_DOMAIN`. */ diff --git a/src/controllers/api/notifications.ts b/src/controllers/api/notifications.ts index 703e79f1..7820dd86 100644 --- a/src/controllers/api/notifications.ts +++ b/src/controllers/api/notifications.ts @@ -1,5 +1,5 @@ import { type AppController } from '@/app.ts'; -import { eventsDB } from '@/storages.ts'; +import { Storages } from '@/storages.ts'; import { paginated, paginationSchema } from '@/utils/api.ts'; import { renderNotification } from '@/views/mastodon/notifications.ts'; @@ -8,7 +8,7 @@ const notificationsController: AppController = async (c) => { const { since, until } = paginationSchema.parse(c.req.query()); const { signal } = c.req.raw; - const events = await eventsDB.query( + const events = await Storages.db.query( [{ kinds: [1], '#p': [pubkey], since, until }], { signal }, ); diff --git a/src/controllers/api/pleroma.ts b/src/controllers/api/pleroma.ts index 64984d7c..51f48dc3 100644 --- a/src/controllers/api/pleroma.ts +++ b/src/controllers/api/pleroma.ts @@ -4,7 +4,7 @@ import { type AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; import { configSchema, elixirTupleSchema, type PleromaConfig } from '@/schemas/pleroma-api.ts'; import { AdminSigner } from '@/signers/AdminSigner.ts'; -import { eventsDB } from '@/storages.ts'; +import { Storages } from '@/storages.ts'; import { createAdminEvent } from '@/utils/api.ts'; import { jsonSchema } from '@/schema.ts'; @@ -66,7 +66,7 @@ const pleromaAdminDeleteStatusController: AppController = async (c) => { async function getConfigs(signal: AbortSignal): Promise { const { pubkey } = Conf; - const [event] = await eventsDB.query([{ + const [event] = await Storages.db.query([{ kinds: [30078], authors: [pubkey], '#d': ['pub.ditto.pleroma.config'], diff --git a/src/controllers/api/search.ts b/src/controllers/api/search.ts index 63561c45..f595e22f 100644 --- a/src/controllers/api/search.ts +++ b/src/controllers/api/search.ts @@ -5,7 +5,7 @@ import { z } from 'zod'; import { AppController } from '@/app.ts'; import { booleanParamSchema } from '@/schema.ts'; import { nostrIdSchema } from '@/schemas/nostr.ts'; -import { searchStore } from '@/storages.ts'; +import { Storages } from '@/storages.ts'; import { dedupeEvents } from '@/utils.ts'; import { nip05Cache } from '@/utils/nip05.ts'; import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts'; @@ -91,8 +91,8 @@ function searchEvents({ q, type, limit, account_id }: SearchQuery, signal: Abort filter.authors = [account_id]; } - return searchStore.query([filter], { signal }) - .then((events) => hydrateEvents({ events, storage: searchStore, signal })); + return Storages.search.query([filter], { signal }) + .then((events) => hydrateEvents({ events, storage: Storages.search, signal })); } /** Get event kinds to search from `type` query param. */ @@ -111,8 +111,8 @@ function typeToKinds(type: SearchQuery['type']): number[] { async function lookupEvent(query: SearchQuery, signal: AbortSignal): Promise { const filters = await getLookupFilters(query, signal); - return searchStore.query(filters, { limit: 1, signal }) - .then((events) => hydrateEvents({ events, storage: searchStore, signal })) + return Storages.search.query(filters, { limit: 1, signal }) + .then((events) => hydrateEvents({ events, storage: Storages.search, signal })) .then(([event]) => event); } diff --git a/src/controllers/api/statuses.ts b/src/controllers/api/statuses.ts index 2c94a719..42923365 100644 --- a/src/controllers/api/statuses.ts +++ b/src/controllers/api/statuses.ts @@ -15,7 +15,7 @@ import { renderReblog, renderStatus } from '@/views/mastodon/statuses.ts'; import { getLnurl } from '@/utils/lnurl.ts'; import { nip05Cache } from '@/utils/nip05.ts'; import { asyncReplaceAll } from '@/utils/text.ts'; -import { eventsDB } from '@/storages.ts'; +import { Storages } from '@/storages.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; const createStatusSchema = z.object({ @@ -137,7 +137,7 @@ const createStatusController: AppController = async (c) => { if (data.quote_id) { await hydrateEvents({ events: [event], - storage: eventsDB, + storage: Storages.db, signal: c.req.raw.signal, }); } @@ -242,7 +242,7 @@ const reblogStatusController: AppController = async (c) => { await hydrateEvents({ events: [reblogEvent], - storage: eventsDB, + storage: Storages.db, signal: signal, }); @@ -262,7 +262,7 @@ const unreblogStatusController: AppController = async (c) => { if (!event) return c.json({ error: 'Event not found.' }, 404); const filters: NostrFilter[] = [{ kinds: [6], authors: [pubkey], '#e': [event.id] }]; - const [repostedEvent] = await eventsDB.query(filters, { limit: 1 }); + const [repostedEvent] = await Storages.db.query(filters, { limit: 1 }); if (!repostedEvent) return c.json({ error: 'Event not found.' }, 404); await createEvent({ diff --git a/src/controllers/api/streaming.ts b/src/controllers/api/streaming.ts index c190c3e3..4bbc627b 100644 --- a/src/controllers/api/streaming.ts +++ b/src/controllers/api/streaming.ts @@ -10,7 +10,6 @@ import { renderReblog, renderStatus } from '@/views/mastodon/statuses.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; import { Storages } from '@/storages.ts'; import { UserStore } from '@/storages/UserStore.ts'; -import { getAdminStore } from '@/storages/adminStore.ts'; const debug = Debug('ditto:streaming'); @@ -69,11 +68,11 @@ const streamingController: AppController = (c) => { const filter = await topicToFilter(stream, c.req.query(), pubkey); if (!filter) return; + const store = pubkey ? new UserStore(pubkey, Storages.admin) : Storages.admin; + try { for await (const msg of Storages.pubsub.req([filter], { signal: controller.signal })) { if (msg[0] === 'EVENT') { - const store = new UserStore(pubkey as string, getAdminStore()); - const [event] = await store.query([{ ids: [msg[2].id] }]); if (!event) continue; diff --git a/src/controllers/nostr/relay-info.ts b/src/controllers/nostr/relay-info.ts index 9d246448..7f1ddf7f 100644 --- a/src/controllers/nostr/relay-info.ts +++ b/src/controllers/nostr/relay-info.ts @@ -1,11 +1,11 @@ import { AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; import { jsonServerMetaSchema } from '@/schemas/nostr.ts'; -import { eventsDB } from '@/storages.ts'; +import { Storages } from '@/storages.ts'; const relayInfoController: AppController = async (c) => { const { signal } = c.req.raw; - const [event] = await eventsDB.query([{ kinds: [0], authors: [Conf.pubkey], limit: 1 }], { signal }); + const [event] = await Storages.db.query([{ kinds: [0], authors: [Conf.pubkey], limit: 1 }], { signal }); const meta = jsonServerMetaSchema.parse(event?.content); return c.json({ diff --git a/src/controllers/nostr/relay.ts b/src/controllers/nostr/relay.ts index 3db72c31..2fe8f921 100644 --- a/src/controllers/nostr/relay.ts +++ b/src/controllers/nostr/relay.ts @@ -1,6 +1,5 @@ import { NostrEvent, NostrFilter, NSchema as n } from '@nostrify/nostrify'; import { relayInfoController } from '@/controllers/nostr/relay-info.ts'; -import { eventsDB } from '@/storages.ts'; import * as pipeline from '@/pipeline.ts'; import { type ClientCLOSE, @@ -71,7 +70,7 @@ function connectStream(socket: WebSocket) { controllers.get(subId)?.abort(); controllers.set(subId, controller); - for (const event of await eventsDB.query(filters, { limit: FILTER_LIMIT })) { + for (const event of await Storages.db.query(filters, { limit: FILTER_LIMIT })) { send(['EVENT', subId, event]); } @@ -115,7 +114,7 @@ function connectStream(socket: WebSocket) { /** Handle COUNT. Return the number of events matching the filters. */ async function handleCount([_, subId, ...rest]: ClientCOUNT): Promise { - const { count } = await eventsDB.count(prepareFilters(rest)); + const { count } = await Storages.db.count(prepareFilters(rest)); send(['COUNT', subId, { count, approximate: false }]); } diff --git a/src/db/users.ts b/src/db/users.ts index 61c7341b..841e981f 100644 --- a/src/db/users.ts +++ b/src/db/users.ts @@ -3,7 +3,7 @@ import { Conf } from '@/config.ts'; import { Debug } from '@/deps.ts'; import * as pipeline from '@/pipeline.ts'; import { AdminSigner } from '@/signers/AdminSigner.ts'; -import { eventsDB } from '@/storages.ts'; +import { Storages } from '@/storages.ts'; const debug = Debug('ditto:users'); @@ -59,7 +59,7 @@ async function findUser(user: Partial, signal?: AbortSignal): Promise { const pubkey = c.get('pubkey'); - const adminStore = getAdminStore(); + if (pubkey) { - const store = new UserStore(pubkey, adminStore); + const store = new UserStore(pubkey, Storages.admin); c.set('store', store); } else { - c.set('store', adminStore); + c.set('store', Storages.admin); } await next(); }; diff --git a/src/pipeline.ts b/src/pipeline.ts index c4b94be6..5fa9f23f 100644 --- a/src/pipeline.ts +++ b/src/pipeline.ts @@ -12,7 +12,7 @@ import { isEphemeralKind } from '@/kinds.ts'; import { DVM } from '@/pipeline/DVM.ts'; import { updateStats } from '@/stats.ts'; import { hydrateEvents, purifyEvent } from '@/storages/hydrate.ts'; -import { cache, eventsDB, reqmeister, Storages } from '@/storages.ts'; +import { Storages } from '@/storages.ts'; import { getTagSet } from '@/tags.ts'; import { eventAge, isRelay, nostrDate, nostrNow, parseNip05, Time } from '@/utils.ts'; import { fetchWorker } from '@/workers/fetch.ts'; @@ -70,15 +70,15 @@ async function handleEvent(event: DittoEvent, signal: AbortSignal): Promise { - const [existing] = await cache.query([{ ids: [event.id], limit: 1 }]); - cache.event(event); - reqmeister.event(event, { signal }); + const [existing] = await Storages.cache.query([{ ids: [event.id], limit: 1 }]); + Storages.cache.event(event); + Storages.reqmeister.event(event, { signal }); return !!existing; } /** Hydrate the event with the user, if applicable. */ async function hydrateEvent(event: DittoEvent, signal: AbortSignal): Promise { - await hydrateEvents({ events: [event], storage: eventsDB, signal }); + await hydrateEvents({ events: [event], storage: Storages.db, signal }); const domain = await db .selectFrom('pubkey_domains') @@ -93,7 +93,7 @@ async function hydrateEvent(event: DittoEvent, signal: AbortSignal): Promise { if (isEphemeralKind(event.kind)) return; - const [deletion] = await eventsDB.query( + const [deletion] = await Storages.db.query( [{ kinds: [5], authors: [Conf.pubkey, event.pubkey], '#e': [event.id], limit: 1 }], { signal }, ); @@ -102,7 +102,7 @@ async function storeEvent(event: DittoEvent, signal?: AbortSignal): Promise id); - await eventsDB.remove([{ ids: deleteIds }], { signal }); + await Storages.db.remove([{ ids: deleteIds }], { signal }); } } } @@ -202,14 +202,14 @@ function trackRelays(event: NostrEvent) { /** Queue related events to fetch. */ async function fetchRelatedEvents(event: DittoEvent, signal: AbortSignal) { if (!event.user) { - reqmeister.req({ kinds: [0], authors: [event.pubkey] }, { signal }).catch(() => {}); + Storages.reqmeister.req({ kinds: [0], authors: [event.pubkey] }, { signal }).catch(() => {}); } for (const [name, id, relay] of event.tags) { if (name === 'e') { - const { count } = await cache.count([{ ids: [id] }]); + const { count } = await Storages.cache.count([{ ids: [id] }]); if (!count) { - reqmeister.req({ ids: [id] }, { relays: [relay] }).catch(() => {}); + Storages.reqmeister.req({ ids: [id] }, { relays: [relay] }).catch(() => {}); } } } diff --git a/src/pipeline/DVM.ts b/src/pipeline/DVM.ts index 96e3c40d..953e9be0 100644 --- a/src/pipeline/DVM.ts +++ b/src/pipeline/DVM.ts @@ -3,7 +3,7 @@ import { NIP05, NostrEvent } from '@nostrify/nostrify'; import { Conf } from '@/config.ts'; import * as pipeline from '@/pipeline.ts'; import { AdminSigner } from '@/signers/AdminSigner.ts'; -import { eventsDB } from '@/storages.ts'; +import { Storages } from '@/storages.ts'; export class DVM { static async event(event: NostrEvent): Promise { @@ -34,7 +34,7 @@ export class DVM { return DVM.feedback(event, 'error', `Forbidden user: ${user}`); } - const [label] = await eventsDB.query([{ + const [label] = await Storages.db.query([{ kinds: [1985], authors: [admin], '#L': ['nip05'], diff --git a/src/queries.ts b/src/queries.ts index 147e96c5..e4fdc21d 100644 --- a/src/queries.ts +++ b/src/queries.ts @@ -1,6 +1,6 @@ import { NostrEvent, NostrFilter } from '@nostrify/nostrify'; import { Conf } from '@/config.ts'; -import { eventsDB, optimizer } from '@/storages.ts'; +import { Storages } from '@/storages.ts'; import { Debug } from '@/deps.ts'; import { type DittoEvent } from '@/interfaces/DittoEvent.ts'; import { type DittoRelation } from '@/interfaces/DittoFilter.ts'; @@ -31,8 +31,8 @@ const getEvent = async ( filter.kinds = [kind]; } - return await optimizer.query([filter], { limit: 1, signal }) - .then((events) => hydrateEvents({ events, storage: optimizer, signal })) + return await Storages.optimizer.query([filter], { limit: 1, signal }) + .then((events) => hydrateEvents({ events, storage: Storages.optimizer, signal })) .then(([event]) => event); }; @@ -40,14 +40,14 @@ const getEvent = async ( const getAuthor = async (pubkey: string, opts: GetEventOpts = {}): Promise => { const { signal = AbortSignal.timeout(1000) } = opts; - return await optimizer.query([{ authors: [pubkey], kinds: [0], limit: 1 }], { limit: 1, signal }) - .then((events) => hydrateEvents({ events, storage: optimizer, signal })) + return await Storages.optimizer.query([{ authors: [pubkey], kinds: [0], limit: 1 }], { limit: 1, signal }) + .then((events) => hydrateEvents({ events, storage: Storages.optimizer, signal })) .then(([event]) => event); }; /** Get users the given pubkey follows. */ const getFollows = async (pubkey: string, signal?: AbortSignal): Promise => { - const [event] = await eventsDB.query([{ authors: [pubkey], kinds: [3], limit: 1 }], { limit: 1, signal }); + const [event] = await Storages.db.query([{ authors: [pubkey], kinds: [3], limit: 1 }], { limit: 1, signal }); return event; }; @@ -83,15 +83,15 @@ async function getAncestors(event: NostrEvent, result: NostrEvent[] = []): Promi } async function getDescendants(eventId: string, signal = AbortSignal.timeout(2000)): Promise { - const events = await eventsDB.query([{ kinds: [1], '#e': [eventId] }], { limit: 200, signal }); - return hydrateEvents({ events, storage: eventsDB, signal }); + const events = await Storages.db.query([{ kinds: [1], '#e': [eventId] }], { limit: 200, signal }); + return hydrateEvents({ events, storage: Storages.db, signal }); } /** Returns whether the pubkey is followed by a local user. */ async function isLocallyFollowed(pubkey: string): Promise { const { host } = Conf.url; - const [event] = await eventsDB.query( + const [event] = await Storages.db.query( [{ kinds: [3], '#p': [pubkey], search: `domain:${host}`, limit: 1 }], { limit: 1 }, ); diff --git a/src/stats.ts b/src/stats.ts index 6bfe568c..e75b57c7 100644 --- a/src/stats.ts +++ b/src/stats.ts @@ -4,7 +4,7 @@ import { InsertQueryBuilder } from 'kysely'; import { db } from '@/db.ts'; import { DittoTables } from '@/db/DittoTables.ts'; import { Debug } from '@/deps.ts'; -import { eventsDB } from '@/storages.ts'; +import { Storages } from '@/storages.ts'; import { findReplyTag } from '@/tags.ts'; type AuthorStat = keyof Omit; @@ -65,7 +65,7 @@ async function getStatsDiff(event: NostrEvent, prev: NostrEvent | undefined): Pr case 5: { if (!firstTaggedId) break; - const [repostedEvent] = await eventsDB.query( + const [repostedEvent] = await Storages.db.query( [{ kinds: [6], ids: [firstTaggedId], authors: [event.pubkey] }], { limit: 1 }, ); @@ -77,7 +77,7 @@ async function getStatsDiff(event: NostrEvent, prev: NostrEvent | undefined): Pr const eventBeingRepostedPubkey = repostedEvent.tags.find(([name]) => name === 'p')?.[1]; if (!eventBeingRepostedId || !eventBeingRepostedPubkey) break; - const [eventBeingReposted] = await eventsDB.query( + const [eventBeingReposted] = await Storages.db.query( [{ kinds: [1], ids: [eventBeingRepostedId], authors: [eventBeingRepostedPubkey] }], { limit: 1 }, ); @@ -154,7 +154,7 @@ function eventStatsQuery(diffs: EventStatDiff[]) { /** Get the last version of the event, if any. */ async function maybeGetPrev(event: NostrEvent): Promise { - const [prev] = await eventsDB.query([ + const [prev] = await Storages.db.query([ { kinds: [event.kind], authors: [event.pubkey], limit: 1 }, ]); diff --git a/src/storages.ts b/src/storages.ts index 6c6a4a5e..21fe9d50 100644 --- a/src/storages.ts +++ b/src/storages.ts @@ -9,51 +9,95 @@ import { PoolStore } from '@/storages/pool-store.ts'; import { Reqmeister } from '@/storages/reqmeister.ts'; import { SearchStore } from '@/storages/search-store.ts'; import { InternalRelay } from '@/storages/InternalRelay.ts'; +import { UserStore } from '@/storages/UserStore.ts'; import { Time } from '@/utils/time.ts'; -/** Relay pool storage. */ -const client = new PoolStore({ - pool, - relays: activeRelays, - publisher: pipeline, -}); - -/** SQLite database to store events this Ditto server cares about. */ -const eventsDB = new EventsDB(db); - -/** In-memory data store for cached events. */ -const cache = new NCache({ max: 3000 }); - -/** Batches requests for single events. */ -const reqmeister = new Reqmeister({ - client, - delay: Time.seconds(1), - timeout: Time.seconds(1), -}); - -/** Main Ditto storage adapter */ -const optimizer = new Optimizer({ - db: eventsDB, - cache, - client: reqmeister, -}); - -/** Storage to use for remote search. */ -const searchStore = new SearchStore({ - relay: Conf.searchRelay, - fallback: optimizer, -}); - export class Storages { + private static _db: EventsDB | undefined; + private static _admin: UserStore | undefined; + private static _cache: NCache | undefined; + private static _client: PoolStore | undefined; + private static _optimizer: Optimizer | undefined; + private static _reqmeister: Reqmeister | undefined; private static _pubsub: InternalRelay | undefined; + private static _search: SearchStore | undefined; - static get pubsub(): InternalRelay { + /** SQLite database to store events this Ditto server cares about. */ + public static get db(): EventsDB { + if (!this._db) { + this._db = new EventsDB(db); + } + return this._db; + } + + /** Admin user storage. */ + public static get admin(): UserStore { + if (!this._admin) { + this._admin = new UserStore(Conf.pubkey, this.db); + } + return this._admin; + } + + /** Internal pubsub relay between controllers and the pipeline. */ + public static get pubsub(): InternalRelay { if (!this._pubsub) { this._pubsub = new InternalRelay(); } - return this._pubsub; } -} -export { cache, client, eventsDB, optimizer, reqmeister, searchStore }; + /** Relay pool storage. */ + public static get client(): PoolStore { + if (!this._client) { + this._client = new PoolStore({ + pool, + relays: activeRelays, + publisher: pipeline, + }); + } + return this._client; + } + + /** In-memory data store for cached events. */ + public static get cache(): NCache { + if (!this._cache) { + this._cache = new NCache({ max: 3000 }); + } + return this._cache; + } + + /** Batches requests for single events. */ + public static get reqmeister(): Reqmeister { + if (!this._reqmeister) { + this._reqmeister = new Reqmeister({ + client: this.client, + delay: Time.seconds(1), + timeout: Time.seconds(1), + }); + } + return this._reqmeister; + } + + /** Main Ditto storage adapter */ + public static get optimizer(): Optimizer { + if (!this._optimizer) { + this._optimizer = new Optimizer({ + db: this.db, + cache: this.cache, + client: this.reqmeister, + }); + } + return this._optimizer; + } + + /** Storage to use for remote search. */ + public static get search(): SearchStore { + if (!this._search) { + this._search = new SearchStore({ + relay: Conf.searchRelay, + fallback: this.optimizer, + }); + } + return this._search; + } +} diff --git a/src/storages/adminStore.ts b/src/storages/adminStore.ts deleted file mode 100644 index 0b5bb384..00000000 --- a/src/storages/adminStore.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { UserStore } from '@/storages/UserStore.ts'; -import { Conf } from '@/config.ts'; -import { eventsDB } from '@/storages.ts'; - -export function getAdminStore() { - return new UserStore(Conf.pubkey, eventsDB); -} diff --git a/src/utils/api.ts b/src/utils/api.ts index 3f89d3f8..7b090ee6 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -9,7 +9,7 @@ import { Debug, parseFormData, type TypeFest } from '@/deps.ts'; import * as pipeline from '@/pipeline.ts'; import { AdminSigner } from '@/signers/AdminSigner.ts'; import { APISigner } from '@/signers/APISigner.ts'; -import { client, eventsDB } from '@/storages.ts'; +import { Storages } from '@/storages.ts'; import { nostrNow } from '@/utils.ts'; const debug = Debug('ditto:api'); @@ -43,7 +43,7 @@ async function updateEvent( fn: (prev: NostrEvent | undefined) => E, c: AppContext, ): Promise { - const [prev] = await eventsDB.query([filter], { limit: 1, signal: c.req.raw.signal }); + const [prev] = await Storages.db.query([filter], { limit: 1, signal: c.req.raw.signal }); return createEvent(fn(prev), c); } @@ -80,7 +80,7 @@ async function publishEvent(event: NostrEvent, c: AppContext): Promise( ); async function localNip05Lookup(name: string): Promise { - const [label] = await eventsDB.query([{ + const [label] = await Storages.db.query([{ kinds: [1985], authors: [Conf.pubkey], '#L': ['nip05'], diff --git a/src/utils/outbox.ts b/src/utils/outbox.ts index 8189fe25..13edaf69 100644 --- a/src/utils/outbox.ts +++ b/src/utils/outbox.ts @@ -1,10 +1,10 @@ import { Conf } from '@/config.ts'; -import { eventsDB } from '@/storages.ts'; +import { Storages } from '@/storages.ts'; export async function getRelays(pubkey: string): Promise> { const relays = new Set<`wss://${string}`>(); - const events = await eventsDB.query([ + const events = await Storages.db.query([ { kinds: [10002], authors: [pubkey, Conf.pubkey], limit: 2 }, ]); diff --git a/src/views.ts b/src/views.ts index 94bb8df3..9c31dfcb 100644 --- a/src/views.ts +++ b/src/views.ts @@ -1,6 +1,6 @@ import { NostrFilter } from '@nostrify/nostrify'; import { AppContext } from '@/app.ts'; -import { eventsDB } from '@/storages.ts'; +import { Storages } from '@/storages.ts'; import { renderAccount } from '@/views/mastodon/accounts.ts'; import { renderStatus } from '@/views/mastodon/statuses.ts'; import { paginated, paginationSchema } from '@/utils/api.ts'; @@ -12,15 +12,15 @@ async function renderEventAccounts(c: AppContext, filters: NostrFilter[], signal return c.json([]); } - const events = await eventsDB.query(filters, { signal }); + const events = await Storages.db.query(filters, { signal }); const pubkeys = new Set(events.map(({ pubkey }) => pubkey)); if (!pubkeys.size) { return c.json([]); } - const authors = await eventsDB.query([{ kinds: [0], authors: [...pubkeys] }], { signal }) - .then((events) => hydrateEvents({ events, storage: eventsDB, signal })); + const authors = await Storages.db.query([{ kinds: [0], authors: [...pubkeys] }], { signal }) + .then((events) => hydrateEvents({ events, storage: Storages.db, signal })); const accounts = await Promise.all( authors.map((event) => renderAccount(event)), @@ -32,8 +32,8 @@ async function renderEventAccounts(c: AppContext, filters: NostrFilter[], signal async function renderAccounts(c: AppContext, authors: string[], signal = AbortSignal.timeout(1000)) { const { since, until, limit } = paginationSchema.parse(c.req.query()); - const events = await eventsDB.query([{ kinds: [0], authors, since, until, limit }], { signal }) - .then((events) => hydrateEvents({ events, storage: eventsDB, signal })); + const events = await Storages.db.query([{ kinds: [0], authors, since, until, limit }], { signal }) + .then((events) => hydrateEvents({ events, storage: Storages.db, signal })); const accounts = await Promise.all( events.map((event) => renderAccount(event)), @@ -50,8 +50,8 @@ async function renderStatuses(c: AppContext, ids: string[], signal = AbortSignal const { limit } = paginationSchema.parse(c.req.query()); - const events = await eventsDB.query([{ kinds: [1], ids, limit }], { signal }) - .then((events) => hydrateEvents({ events, storage: eventsDB, signal })); + const events = await Storages.db.query([{ kinds: [1], ids, limit }], { signal }) + .then((events) => hydrateEvents({ events, storage: Storages.db, signal })); if (!events.length) { return c.json([]); diff --git a/src/views/mastodon/relationships.ts b/src/views/mastodon/relationships.ts index 983b1342..4cfbfc28 100644 --- a/src/views/mastodon/relationships.ts +++ b/src/views/mastodon/relationships.ts @@ -1,8 +1,8 @@ -import { eventsDB } from '@/storages.ts'; +import { Storages } from '@/storages.ts'; import { hasTag } from '@/tags.ts'; async function renderRelationship(sourcePubkey: string, targetPubkey: string) { - const events = await eventsDB.query([ + const events = await Storages.db.query([ { kinds: [3], authors: [sourcePubkey], limit: 1 }, { kinds: [3], authors: [targetPubkey], limit: 1 }, { kinds: [10000], authors: [sourcePubkey], limit: 1 }, diff --git a/src/views/mastodon/statuses.ts b/src/views/mastodon/statuses.ts index 06ef164a..f63c5010 100644 --- a/src/views/mastodon/statuses.ts +++ b/src/views/mastodon/statuses.ts @@ -6,7 +6,7 @@ import { Conf } from '@/config.ts'; import { type DittoEvent } from '@/interfaces/DittoEvent.ts'; import { getMediaLinks, parseNoteContent } from '@/note.ts'; import { jsonMediaDataSchema } from '@/schemas/nostr.ts'; -import { eventsDB, optimizer } from '@/storages.ts'; +import { Storages } from '@/storages.ts'; import { findReplyTag } from '@/tags.ts'; import { nostrDate } from '@/utils.ts'; import { unfurlCardCached } from '@/utils/unfurl.ts'; @@ -40,7 +40,7 @@ async function renderStatus(event: DittoEvent, opts: statusOpts): Promise { ), ]; - const mentionedProfiles = await optimizer.query( + const mentionedProfiles = await Storages.optimizer.query( [{ authors: mentionedPubkeys, limit: mentionedPubkeys.length }], ); @@ -53,7 +53,7 @@ async function renderStatus(event: DittoEvent, opts: statusOpts): Promise { ), firstUrl ? unfurlCardCached(firstUrl) : null, viewerPubkey - ? await eventsDB.query([ + ? await Storages.db.query([ { kinds: [6], '#e': [event.id], authors: [viewerPubkey], limit: 1 }, { kinds: [7], '#e': [event.id], authors: [viewerPubkey], limit: 1 }, { kinds: [9734], '#e': [event.id], authors: [viewerPubkey], limit: 1 },