start using applesauce

This commit is contained in:
hzrd149
2025-02-07 12:09:52 -06:00
parent 9efd5019e3
commit f71c7aa96a
29 changed files with 420 additions and 325 deletions

22
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,22 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch",
"skipFiles": ["<node_internals>/**", "data/**"],
"args": ["--loader", "@swc-node/register/esm", "src/index.ts"],
"outFiles": ["${workspaceFolder}/**/*.js"],
"env": {
"NODE_ENV": "development",
"DEBUG": "bakery,bakery:*,applesauce,applesauce:*",
"DEBUG_HIDE_DATE": "true",
"DEBUG_COLORS": "true"
}
}
]
}

View File

@@ -28,8 +28,8 @@
"@noble/hashes": "^1.7.1",
"@satellite-earth/core": "^0.5.0",
"applesauce-core": "next",
"applesauce-factory": "0.0.0-next-20250206174509",
"applesauce-loaders": "^0.10.0",
"applesauce-factory": "next",
"applesauce-loaders": "next",
"applesauce-signers": "next",
"better-sqlite3": "^11.8.1",
"blossom-client-sdk": "^2.1.1",

77
pnpm-lock.yaml generated
View File

@@ -19,16 +19,16 @@ importers:
version: 0.5.0(typescript@5.7.3)
applesauce-core:
specifier: next
version: 0.0.0-next-20250206174509(typescript@5.7.3)
version: 0.0.0-next-20250207175032(typescript@5.7.3)
applesauce-factory:
specifier: 0.0.0-next-20250206174509
version: 0.0.0-next-20250206174509(typescript@5.7.3)
specifier: next
version: 0.0.0-next-20250207175032(typescript@5.7.3)
applesauce-loaders:
specifier: ^0.10.0
version: 0.10.0(typescript@5.7.3)
specifier: next
version: 0.0.0-next-20250207175032(typescript@5.7.3)
applesauce-signers:
specifier: next
version: 0.0.0-next-20250206174509(typescript@5.7.3)
version: 0.0.0-next-20250207175032(typescript@5.7.3)
better-sqlite3:
specifier: ^11.8.1
version: 11.8.1
@@ -560,23 +560,20 @@ packages:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
applesauce-content@0.0.0-next-20250206174509:
resolution: {integrity: sha512-+LXzDZdANYwZGKnpc0CFoEgF0KgKuOcYTEUrf0X79ezKejCEGwvn1UvNce2c8MIWfwhaHBTJq8kQO2gKaODQ9A==}
applesauce-content@0.0.0-next-20250207175032:
resolution: {integrity: sha512-n6fA+OGWipHXEQI8rt42/GRAYUTHvUScpXiBei9KvCs2rhIIA1wJ4dlHEyOTL12slcVMi3q6PfCxdv0uWNuHxw==}
applesauce-core@0.0.0-next-20250206174509:
resolution: {integrity: sha512-+5G8y4WwjJVvL2dfn56w0mYhm4GjL0hoMk4ZJBM7FUBcpuq40UoBsslheIFaLYuzzZHvd28EtJycbyhBKM86pQ==}
applesauce-core@0.0.0-next-20250207175032:
resolution: {integrity: sha512-jTM62hmA7BRjO3EGZePMTrl77pA1uXaWOWMbdwyDKFcil9RZUGMKV2cL73SXtuxFQJRrFmgrTWSLzOEWEfVdVg==}
applesauce-core@0.10.0:
resolution: {integrity: sha512-QMhUh4FIARcqY5soCB4Z8DIu+py0rYb28IgWT4gP9DLBGpDrY8lStXk7W1/46TLjEH97y0hbiXFK7kMCZ31oOQ==}
applesauce-factory@0.0.0-next-20250207175032:
resolution: {integrity: sha512-NMGrHpD/BxFNvm0AzK2fYzXGBjzqvoA/o6ZWZUvo8Y52qvETjz1qS7asMndMXBWszDHwo8e0buDMHjlAA5/prA==}
applesauce-factory@0.0.0-next-20250206174509:
resolution: {integrity: sha512-E5OTJLYSh5vejQmuHZOqxLajJwPiQOFnlkhKQSBMeA2RyxClOuuDIm8Qj3PXlC1T9kAqIqL/CsieUcJ1jhUWdQ==}
applesauce-loaders@0.0.0-next-20250207175032:
resolution: {integrity: sha512-2gQF3O4jS7hFjFkI2lI0gMJVn1IB4iWUSR9FjWYICQfQXZ0ewpOhhi2KzX6oXdlMeZx/y2bh8aNUO6unvagGFA==}
applesauce-loaders@0.10.0:
resolution: {integrity: sha512-cEPV8nSJKfmvVVRaSDc/3TRaDE8s0CSFJGzEvSYBTjokZBvQz2QZrlQXbwTvYD90XrWFH1LJ8dLDDnEUehaOow==}
applesauce-signers@0.0.0-next-20250206174509:
resolution: {integrity: sha512-SfV1nG9YhF9aKS1c8uUg2ciNmetBbUgmV58l8/nnulHatGDDNtX+d34XGKxXXGRHUHk7bYtFKeeYSBNw4Wm94A==}
applesauce-signers@0.0.0-next-20250207175032:
resolution: {integrity: sha512-rJQvFpeIk9s0eHdNjl/DvMzD8pXTtx+Hr0Rcadsq0w2t+vAA0Z0B7G4xtG2Y+eCnIdSeJ/TdkebEcRLDV5eZHw==}
argparse@1.0.10:
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
@@ -1606,8 +1603,8 @@ packages:
resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==}
engines: {node: '>= 6'}
possible-typed-array-names@1.0.0:
resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==}
possible-typed-array-names@1.1.0:
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
engines: {node: '>= 0.4'}
prebuild-install@7.1.3:
@@ -2605,13 +2602,13 @@ snapshots:
normalize-path: 3.0.0
picomatch: 2.3.1
applesauce-content@0.0.0-next-20250206174509(typescript@5.7.3):
applesauce-content@0.0.0-next-20250207175032(typescript@5.7.3):
dependencies:
'@cashu/cashu-ts': 2.0.0-rc1
'@types/hast': 3.0.4
'@types/mdast': 4.0.4
'@types/unist': 3.0.3
applesauce-core: 0.0.0-next-20250206174509(typescript@5.7.3)
applesauce-core: 0.0.0-next-20250207175032(typescript@5.7.3)
mdast-util-find-and-replace: 3.0.2
nostr-tools: 2.10.4(typescript@5.7.3)
remark: 15.0.1
@@ -2622,7 +2619,7 @@ snapshots:
- supports-color
- typescript
applesauce-core@0.0.0-next-20250206174509(typescript@5.7.3):
applesauce-core@0.0.0-next-20250207175032(typescript@5.7.3):
dependencies:
'@scure/base': 1.2.4
debug: 4.4.0(supports-color@5.5.0)
@@ -2636,33 +2633,19 @@ snapshots:
- supports-color
- typescript
applesauce-core@0.10.0(typescript@5.7.3):
applesauce-factory@0.0.0-next-20250207175032(typescript@5.7.3):
dependencies:
'@scure/base': 1.2.4
debug: 4.4.0(supports-color@5.5.0)
fast-deep-equal: 3.1.3
hash-sum: 2.0.0
light-bolt11-decoder: 3.2.0
nanoid: 5.0.9
nostr-tools: 2.10.4(typescript@5.7.3)
rxjs: 7.8.1
transitivePeerDependencies:
- supports-color
- typescript
applesauce-factory@0.0.0-next-20250206174509(typescript@5.7.3):
dependencies:
applesauce-content: 0.0.0-next-20250206174509(typescript@5.7.3)
applesauce-core: 0.0.0-next-20250206174509(typescript@5.7.3)
applesauce-content: 0.0.0-next-20250207175032(typescript@5.7.3)
applesauce-core: 0.0.0-next-20250207175032(typescript@5.7.3)
nanoid: 5.0.9
nostr-tools: 2.10.4(typescript@5.7.3)
transitivePeerDependencies:
- supports-color
- typescript
applesauce-loaders@0.10.0(typescript@5.7.3):
applesauce-loaders@0.0.0-next-20250207175032(typescript@5.7.3):
dependencies:
applesauce-core: 0.10.0(typescript@5.7.3)
applesauce-core: 0.0.0-next-20250207175032(typescript@5.7.3)
nanoid: 5.0.9
nostr-tools: 2.10.4(typescript@5.7.3)
rx-nostr: 3.5.0
@@ -2671,12 +2654,12 @@ snapshots:
- supports-color
- typescript
applesauce-signers@0.0.0-next-20250206174509(typescript@5.7.3):
applesauce-signers@0.0.0-next-20250207175032(typescript@5.7.3):
dependencies:
'@noble/hashes': 1.7.1
'@noble/secp256k1': 1.7.1
'@scure/base': 1.2.4
applesauce-core: 0.0.0-next-20250206174509(typescript@5.7.3)
applesauce-core: 0.0.0-next-20250207175032(typescript@5.7.3)
debug: 4.4.0(supports-color@5.5.0)
nanoid: 5.0.9
nostr-tools: 2.10.4(typescript@5.7.3)
@@ -2707,7 +2690,7 @@ snapshots:
available-typed-arrays@1.0.7:
dependencies:
possible-typed-array-names: 1.0.0
possible-typed-array-names: 1.1.0
b4a@1.6.7: {}
@@ -3918,7 +3901,7 @@ snapshots:
pirates@4.0.6: {}
possible-typed-array-names@1.0.0: {}
possible-typed-array-names@1.1.0: {}
prebuild-install@7.1.3:
dependencies:

View File

@@ -1,11 +1,8 @@
import EventEmitter from "events";
import Database, { type Database as SQLDatabase } from "better-sqlite3";
import { fileURLToPath } from "url";
import path from "path";
import fs from "fs";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export type LocalDatabaseConfig = {
directory: string;
name: string;

View File

@@ -1,13 +1,12 @@
import path from "path";
import { WebSocketServer } from "ws";
import { createServer, Server } from "http";
import { SimpleSigner } from "applesauce-signers/signers/simple-signer";
import { IEventStore, NostrRelay, SQLiteEventStore } from "@satellite-earth/core";
import { getDMRecipient } from "@satellite-earth/core/helpers/nostr";
import { kinds } from "nostr-tools";
import { AbstractRelay } from "nostr-tools/abstract-relay";
import express, { Express } from "express";
import { EventEmitter } from "events";
import { SimpleSigner } from "applesauce-signers/signers/simple-signer";
import cors from "cors";
import { logger } from "../logger.js";
@@ -44,6 +43,12 @@ import SecretsManager from "../modules/secrets-manager.js";
import outboundNetwork, { OutboundNetworkManager } from "../modules/network/outbound/index.js";
import Switchboard from "../modules/switchboard/switchboard.js";
import Gossip from "../modules/gossip.js";
import database from "../services/database.js";
import secrets from "../services/secrets.js";
import config from "../services/config.js";
import logStore from "../services/log-store.js";
import stateManager from "../services/state.js";
import eventCache from "../services/event-cache.js";
type EventMap = {
listening: [];
@@ -81,14 +86,12 @@ export default class App extends EventEmitter<EventMap> {
switchboard: Switchboard;
gossip: Gossip;
constructor(dataPath: string) {
constructor() {
super();
this.config = new ConfigManager(path.join(dataPath, "node.json"));
this.config.read();
this.config = config;
this.secrets = new SecretsManager(path.join(dataPath, "secrets.json"));
this.secrets.read();
this.secrets = secrets;
this.signer = new SimpleSigner(this.secrets.get("nostrKey"));
@@ -131,15 +134,13 @@ export default class App extends EventEmitter<EventMap> {
headers.push("Access-Control-Allow-Origin: *");
});
// Init embedded sqlite database
this.database = new Database({ directory: dataPath });
// Init sqlite database
this.database = database;
// create log managers
this.logStore = new LogStore(this.database.db);
this.logStore.setup();
this.logStore = logStore;
this.state = new ApplicationStateManager(this.database.db);
this.state.setup();
this.state = stateManager;
// Recognize local relay by matching auth string
this.pool = new CautiousPool((relay: AbstractRelay, challenge: string) => {
@@ -150,17 +151,16 @@ export default class App extends EventEmitter<EventMap> {
});
// Initialize the event store
this.eventStore = new SQLiteEventStore(this.database.db);
this.eventStore.setup();
this.eventStore = eventCache;
// setup decryption cache
this.decryptionCache = new DecryptionCache(this.database.db);
this.decryptionCache.setup();
// Setup managers user contacts and profiles
this.addressBook = new AddressBook(this);
this.profileBook = new ProfileBook(this);
this.contactBook = new ContactBook(this);
this.addressBook = new AddressBook();
this.profileBook = new ProfileBook();
this.contactBook = new ContactBook();
// Setup the notifications manager
this.notifications = new NotificationsManager(this /*this.eventStore, this.state*/);
@@ -291,8 +291,8 @@ export default class App extends EventEmitter<EventMap> {
// send direct message
const results = await this.directMessageManager.forwardMessage(ctx.event);
if (!results || !results.some((p) => p.status === "fulfilled")) throw new Error("Failed to forward message");
return `Forwarded message to ${results.filter((p) => p.status === "fulfilled").length}/${results.length} relays`;
if (!results || !results.some((p) => p.ok)) throw new Error("Failed to forward message");
return `Forwarded message to ${results.filter((p) => p.ok).length}/${results.length} relays`;
} else return next();
});
@@ -320,8 +320,8 @@ export default class App extends EventEmitter<EventMap> {
const profile = this.profileBook.getProfile(pubkey);
if (!profile) {
this.profileBook.loadProfile(pubkey, this.addressBook.getOutboxes(pubkey));
this.addressBook.loadOutboxes(pubkey).then((outboxes) => {
this.profileBook.loadProfile(pubkey, outboxes ?? undefined);
this.addressBook.loadMailboxes(pubkey).then((mailboxes) => {
this.profileBook.loadProfile(pubkey, mailboxes?.outboxes);
});
}
};

View File

@@ -1,23 +0,0 @@
import { createRxNostr, noopVerifier } from "rx-nostr";
import { verifyEvent } from "nostr-tools/wasm";
import { EventStore, QueryStore } from "applesauce-core";
import { logger } from "./logger.js";
const log = logger.extend("rx-nostr");
export const rxNostr = createRxNostr({
verifier: async (event) => {
try {
return verifyEvent(event);
} catch (error) {
return false;
}
},
});
rxNostr.createConnectionStateObservable().subscribe((packet) => {
log(`${packet.state} ${packet.from}`);
});
export const eventStore = new EventStore();
export const queryStore = new QueryStore(eventStore);

View File

@@ -1,4 +1,5 @@
import "dotenv/config.js";
import "dotenv/config";
import { mkdirp } from "mkdirp";
import { OUTBOUND_PROXY_TYPES } from "./const.js";
import { safeRelayUrls } from "applesauce-core/helpers";
@@ -7,6 +8,8 @@ import { normalizeToHexPubkey } from "./helpers/nip19.js";
export const OWNER_PUBKEY = process.env.OWNER_PUBKEY ? normalizeToHexPubkey(process.env.OWNER_PUBKEY) : undefined;
export const PUBLIC_ADDRESS = process.env.PUBLIC_ADDRESS;
export const DATA_PATH = process.env.DATA_PATH || "./data";
await mkdirp(DATA_PATH);
export const PORT = parseInt(process.env.PORT ?? "") || 3000;
// I2P config
@@ -30,4 +33,6 @@ export const BOOTSTRAP_RELAYS = process.env.BOOTSTRAP_RELAYS
export const COMMON_CONTACT_RELAYS = process.env.COMMON_CONTACT_RELAYS
? safeRelayUrls(process.env.COMMON_CONTACT_RELAYS.split(","))
: safeRelayUrls(["wss://purplepag.es", "wss://user.kindpag.es", "wss://relay.nos.social"]);
: safeRelayUrls(["wss://purplepag.es", "wss://user.kindpag.es"]);
export const IS_DEV = process.env.NODE_ENV === "development";

View File

@@ -1,4 +1,4 @@
export function arrayFallback<T>(arr: T[], fallback: T[]): T[] {
if (arr.length === 0) return fallback;
export function arrayFallback<T>(arr: T[] | undefined, fallback: T[]): T[] {
if (arr === undefined || arr.length === 0) return fallback;
else return arr;
}

16
src/helpers/rxjs.ts Normal file
View File

@@ -0,0 +1,16 @@
import { bufferTime, MonoTypeOperatorFunction, Subject, tap } from "rxjs";
export function bufferAudit<T>(buffer = 10_000, audit: (messages: T[]) => void): MonoTypeOperatorFunction<T> {
return (source) => {
const logBuffer = new Subject<T>();
logBuffer
.pipe(
bufferTime(buffer),
tap((values) => audit(values)),
)
.subscribe();
return source.pipe(tap((value) => logBuffer.next(value)));
};
}

View File

@@ -4,13 +4,12 @@ import process from "node:process";
import path from "node:path";
import express, { Request } from "express";
import { mkdirp } from "mkdirp";
import dayjs from "dayjs";
import duration from "dayjs/plugin/duration.js";
import localizedFormat from "dayjs/plugin/localizedFormat.js";
import App from "./app/index.js";
import { DATA_PATH, PUBLIC_ADDRESS } from "./env.js";
import { PUBLIC_ADDRESS } from "./env.js";
import { addListener, logger } from "./logger.js";
import { pathExists } from "./helpers/fs.js";
@@ -19,8 +18,7 @@ dayjs.extend(duration);
dayjs.extend(localizedFormat);
// create app
await mkdirp(DATA_PATH);
const app = new App(DATA_PATH);
const app = new App();
// connect logger to app LogStore
addListener(({ namespace }, ...args) => {

View File

@@ -20,6 +20,4 @@ debug.log = function (this: Debugger, ...args: any[]) {
console.log.apply(this, args);
};
const logger = debug("bakery");
export { logger };
export const logger = debug("bakery");

View File

@@ -1,66 +1,44 @@
import { NostrEvent, kinds } from "nostr-tools";
import _throttle from "lodash.throttle";
import { tap } from "rxjs";
import { kinds } from "nostr-tools";
import { MailboxesQuery } from "applesauce-core/queries";
import { getObservableValue } from "applesauce-core/observable";
import { getInboxes, getOutboxes } from "applesauce-core/helpers";
import { getInboxes, getOutboxes } from "@satellite-earth/core/helpers/nostr/mailboxes.js";
import { logger } from "../logger.js";
import App from "../app/index.js";
import PubkeyBatchLoader from "./pubkey-batch-loader.js";
import { COMMON_CONTACT_RELAYS } from "../env.js";
import { replaceableLoader } from "../services/loaders.js";
import { eventStore, queryStore } from "../services/stores.js";
import { simpleTimeout } from "../operators/simple-timeout.js";
import { arrayFallback } from "../helpers/array.js";
const DEFAULT_REQUEST_TIMEOUT = 10_000;
/** Loads 10002 events for pubkeys */
export default class AddressBook {
log = logger.extend("AddressBook");
app: App;
loader: PubkeyBatchLoader;
get extraRelays() {
return this.loader.extraRelays;
}
set extraRelays(v: string[]) {
this.loader.extraRelays = v;
}
constructor(app: App) {
this.app = app;
this.loader = new PubkeyBatchLoader(kinds.RelayList, this.app.pool, (pubkey) => {
return this.app.eventStore.getEventsForFilters([{ kinds: [kinds.RelayList], authors: [pubkey] }])?.[0];
});
this.loader.on("event", (event) => this.app.eventStore.addEvent(event));
this.loader.on("batch", (found, failed) => {
this.log(`Found ${found}, failed ${failed}, pending ${this.loader.queue}`);
});
}
getMailboxes(pubkey: string) {
return this.loader.getEvent(pubkey);
return eventStore.getReplaceable(kinds.RelayList, pubkey);
}
getOutboxes(pubkey: string) {
const mailboxes = this.getMailboxes(pubkey);
return mailboxes && getOutboxes(mailboxes);
}
getInboxes(pubkey: string) {
const mailboxes = this.getMailboxes(pubkey);
return mailboxes && getInboxes(mailboxes);
}
handleEvent(event: NostrEvent) {
this.loader.handleEvent(event);
}
async loadMailboxes(pubkey: string, relays?: string[], force?: boolean) {
relays = arrayFallback(relays, COMMON_CONTACT_RELAYS);
this.log(`Requesting mailboxes from ${relays.length} relays for ${pubkey}`);
replaceableLoader.next({ kind: kinds.RelayList, pubkey, relays, force });
async loadMailboxes(pubkey: string, relays?: string[]) {
return this.loader.getOrLoadEvent(pubkey, relays);
}
async loadOutboxes(pubkey: string, relays?: string[]) {
const mailboxes = await this.loadMailboxes(pubkey, relays);
return mailboxes && getOutboxes(mailboxes);
}
async loadInboxes(pubkey: string, relays?: string[]) {
const mailboxes = await this.loadMailboxes(pubkey, relays);
return mailboxes && getInboxes(mailboxes);
return getObservableValue(
queryStore.createQuery(MailboxesQuery, pubkey).pipe(
simpleTimeout(DEFAULT_REQUEST_TIMEOUT, `Failed to load mailboxes for ${pubkey}`),
tap((m) => m && this.log(`Found mailboxes for ${pubkey}`, m)),
),
);
}
}

View File

@@ -1,33 +1,24 @@
import { NostrEvent, kinds } from "nostr-tools";
import _throttle from "lodash.throttle";
import { tap } from "rxjs";
import { kinds } from "nostr-tools";
import { ReplaceableQuery, UserContactsQuery } from "applesauce-core/queries";
import { getObservableValue } from "applesauce-core/observable";
import { COMMON_CONTACT_RELAYS } from "../env.js";
import { logger } from "../logger.js";
import App from "../app/index.js";
import PubkeyBatchLoader from "./pubkey-batch-loader.js";
import { COMMON_CONTACT_RELAYS } from "../env.js";
import { replaceableLoader } from "../services/loaders.js";
import { eventStore, queryStore } from "../services/stores.js";
import { simpleTimeout } from "../operators/simple-timeout.js";
import { arrayFallback } from "../helpers/array.js";
const DEFAULT_REQUEST_TIMEOUT = 10_000;
/** Loads 3 contact lists for pubkeys */
export default class ContactBook {
log = logger.extend("ContactsBook");
app: App;
loader: PubkeyBatchLoader;
extraRelays = COMMON_CONTACT_RELAYS;
constructor(app: App) {
this.app = app;
this.loader = new PubkeyBatchLoader(kinds.Contacts, this.app.pool, (pubkey) => {
return this.app.eventStore.getEventsForFilters([{ kinds: [kinds.Contacts], authors: [pubkey] }])?.[0];
});
this.loader.on("event", (event) => this.app.eventStore.addEvent(event));
this.loader.on("batch", (found, failed) => {
this.log(`Found ${found}, failed ${failed}, pending ${this.loader.queue}`);
});
}
/** @deprecated use loadContacts instead */
getContacts(pubkey: string) {
return this.loader.getEvent(pubkey);
return eventStore.getReplaceable(kinds.Contacts, pubkey);
}
getFollowedPubkeys(pubkey: string): string[] {
@@ -44,11 +35,29 @@ export default class ContactBook {
return [];
}
handleEvent(event: NostrEvent) {
this.loader.handleEvent(event);
async loadContacts(pubkey: string, relays?: string[], force?: boolean) {
relays = arrayFallback(relays, COMMON_CONTACT_RELAYS);
this.log(`Requesting contacts from ${relays.length} relays for ${pubkey}`);
replaceableLoader.next({ kind: kinds.Contacts, pubkey, relays, force });
return getObservableValue(
queryStore
.createQuery(UserContactsQuery, pubkey)
.pipe(simpleTimeout(DEFAULT_REQUEST_TIMEOUT, `Failed to load contacts for ${pubkey}`)),
);
}
async loadContacts(pubkey: string, relays: string[] = []) {
return this.loader.getOrLoadEvent(pubkey, relays);
/** @deprecated */
async loadContactsEvent(pubkey: string, relays?: string[]) {
relays = arrayFallback(relays, COMMON_CONTACT_RELAYS);
this.log(`Requesting contacts from ${relays.length} relays for ${pubkey}`);
replaceableLoader.next({ kind: kinds.Contacts, pubkey, relays });
return getObservableValue(
queryStore.createQuery(ReplaceableQuery, kinds.Contacts, pubkey).pipe(
simpleTimeout(DEFAULT_REQUEST_TIMEOUT, `Failed to load contacts for ${pubkey}`),
tap((c) => c && this.log(`Found contacts for ${pubkey}`, c)),
),
);
}
}

View File

@@ -1,13 +1,17 @@
import { filter, lastValueFrom, mergeMap, Subscription, tap, toArray } from "rxjs";
import { NostrEvent, kinds } from "nostr-tools";
import { SubCloser } from "nostr-tools/abstract-pool";
import { Subscription } from "nostr-tools/abstract-relay";
import { getInboxes } from "applesauce-core/helpers";
import { createRxForwardReq } from "rx-nostr";
import { getRelaysFromContactList } from "@satellite-earth/core/helpers/nostr/contacts.js";
import { MailboxesQuery } from "applesauce-core/queries";
import { EventEmitter } from "events";
import { getRelaysFromContactList } from "@satellite-earth/core/helpers/nostr/contacts.js";
import { logger } from "../logger.js";
import type App from "../app/index.js";
import { arrayFallback } from "../helpers/array.js";
import { rxNostr } from "../services/rx-nostr.js";
import { eventStore, queryStore } from "../services/stores.js";
import { COMMON_CONTACT_RELAYS } from "../env.js";
import { bufferAudit } from "../helpers/rxjs.js";
type EventMap = {
open: [string, string];
@@ -47,11 +51,11 @@ export default class DirectMessageManager extends EventEmitter<EventMap> {
if (!addressedTo) return;
// get users inboxes
let relays = await this.app.addressBook.loadInboxes(addressedTo);
let relays = (await this.app.addressBook.loadMailboxes(addressedTo))?.inboxes;
if (!relays || relays.length === 0) {
// try to send the DM to the users legacy app relays
const contacts = await this.app.contactBook.loadContacts(addressedTo);
const contacts = await this.app.contactBook.loadContactsEvent(addressedTo);
if (contacts) {
const appRelays = getRelaysFromContactList(contacts);
@@ -65,7 +69,7 @@ export default class DirectMessageManager extends EventEmitter<EventMap> {
}
this.log(`Forwarding message to ${relays.length} relays`);
const results = await Promise.allSettled(this.app.pool.publish(relays, event));
const results = await lastValueFrom(rxNostr.send(event, { on: { relays } }).pipe(toArray()));
return results;
}
@@ -75,95 +79,93 @@ export default class DirectMessageManager extends EventEmitter<EventMap> {
else return b + ":" + a;
}
watching = new Map<string, Map<string, Subscription>>();
watching = new Map<string, Subscription>();
async watchInbox(pubkey: string) {
if (this.watching.has(pubkey)) return;
this.app.addressBook.loadMailboxes(pubkey, COMMON_CONTACT_RELAYS, true);
this.log(`Watching ${pubkey} inboxes for mail`);
const mailboxes = await this.app.addressBook.loadMailboxes(pubkey);
if (!mailboxes) {
this.log(`Failed to get ${pubkey} mailboxes`);
return;
}
const subscription = queryStore
.createQuery(MailboxesQuery, pubkey)
.pipe(
// ignore undefined
filter((m) => !!m),
// start a new subscription for each update
mergeMap((mailboxes) => {
const relays = arrayFallback(mailboxes.inboxes, this.explicitRelays);
this.log(`Subscribing to ${relays.length} relays for ${pubkey}`);
const relays = arrayFallback(getInboxes(mailboxes), this.explicitRelays);
const subscriptions = new Map<string, Subscription>();
const req = createRxForwardReq();
const sub = rxNostr.use(req, { on: { relays } }).pipe(
filter((packet) => this.app.eventStore.addEvent(packet.event)),
// also pass to event store
tap((packet) => eventStore.add(packet.event, packet.from)),
// log how many events where found every 10s
bufferAudit(10_000, (events) => {
if (events.length > 0) this.log(`Found ${events.length} new events for ${pubkey}`);
}),
);
for (const url of relays) {
const subscribe = async () => {
const relay = await this.app.pool.ensureRelay(url);
const sub = relay.subscribe([{ kinds: [kinds.EncryptedDirectMessage], "#p": [pubkey] }], {
onevent: (event) => {
this.app.eventStore.addEvent(event);
},
onclose: () => {
// reconnect if we are still watching this pubkey
if (this.watching.has(pubkey)) {
this.log(`Reconnecting to ${relay.url} for ${pubkey} inbox DMs`);
setTimeout(() => subscribe(), 30_000);
}
},
});
req.emit({ kinds: [kinds.EncryptedDirectMessage], "#p": [pubkey] });
subscriptions.set(relay.url, sub);
};
return sub;
}),
)
.subscribe();
subscribe();
}
this.watching.set(pubkey, subscriptions);
this.watching.set(pubkey, subscription);
}
stopWatchInbox(pubkey: string) {
const subs = this.watching.get(pubkey);
if (subs) {
const sub = this.watching.get(pubkey);
if (sub) {
sub.unsubscribe();
this.watching.delete(pubkey);
for (const [_, sub] of subs) {
sub.close();
}
}
}
subscriptions = new Map<string, SubCloser>();
openConversations = new Map<string, Subscription>();
async openConversation(a: string, b: string) {
const key = this.getConversationKey(a, b);
if (this.subscriptions.has(key)) return;
if (this.openConversations.has(key)) return;
const aMailboxes = await this.app.addressBook.loadMailboxes(a);
const bMailboxes = await this.app.addressBook.loadMailboxes(b);
// If inboxes for either user cannot be determined, either because nip65
// was not found, or nip65 had no listed read relays, fall back to explicit
const aInboxes = aMailboxes ? arrayFallback(getInboxes(aMailboxes), this.explicitRelays) : this.explicitRelays;
const bInboxes = bMailboxes ? arrayFallback(getInboxes(bMailboxes), this.explicitRelays) : this.explicitRelays;
// was not found, or nip65 had no listed read relays, fallback to explicit relays
const aInboxes = aMailboxes ? arrayFallback(aMailboxes.inboxes, this.explicitRelays) : this.explicitRelays;
const bInboxes = bMailboxes ? arrayFallback(bMailboxes.inboxes, this.explicitRelays) : this.explicitRelays;
const relays = new Set([...aInboxes, ...bInboxes]);
let events = 0;
const sub = this.app.pool.subscribeMany(
Array.from(relays),
[{ kinds: [kinds.EncryptedDirectMessage], authors: [a, b], "#p": [a, b] }],
{
onevent: (event) => {
events += +this.app.eventStore.addEvent(event);
},
oneose: () => {
if (events) this.log(`Found ${events} new messages`);
},
},
);
const req = createRxForwardReq();
const sub = rxNostr
.use(req)
.pipe(filter((packet) => this.app.eventStore.addEvent(packet.event)))
.subscribe();
req.emit([{ kinds: [kinds.EncryptedDirectMessage], authors: [a, b], "#p": [a, b] }]);
this.log(`Opened conversation ${key} on ${relays.size} relays`);
this.subscriptions.set(key, sub);
this.openConversations.set(key, sub);
this.emit("open", a, b);
}
closeConversation(a: string, b: string) {
const key = this.getConversationKey(a, b);
const sub = this.subscriptions.get(key);
const sub = this.openConversations.get(key);
if (sub) {
sub.close();
this.subscriptions.delete(key);
sub.unsubscribe();
this.openConversations.delete(key);
this.emit("close", a, b);
}
}
[Symbol.dispose]() {
for (const [_, sub] of this.watching) sub.unsubscribe();
for (const [_, sub] of this.openConversations) sub.unsubscribe();
}
}

View File

@@ -1,13 +1,15 @@
import { NotificationChannel, WebPushNotification } from "@satellite-earth/core/types/control-api/notifications.js";
import { getDMRecipient, getDMSender, getUserDisplayName, parseKind0Event } from "@satellite-earth/core/helpers/nostr";
import { getDMRecipient, getDMSender, getUserDisplayName } from "@satellite-earth/core/helpers/nostr";
import { NostrEvent, kinds } from "nostr-tools";
import { npubEncode } from "nostr-tools/nip19";
import { getDisplayName, unixNow } from "applesauce-core/helpers";
import EventEmitter from "events";
import webPush from "web-push";
import dayjs from "dayjs";
import { logger } from "../../logger.js";
import App from "../../app/index.js";
import stateManager from "../../services/state.js";
import config from "../../services/config.js";
export type NotificationsManagerState = {
channels: NotificationChannel[];
@@ -22,7 +24,7 @@ type EventMap = {
export default class NotificationsManager extends EventEmitter<EventMap> {
log = logger.extend("Notifications");
app: App;
lastRead: number = dayjs().unix();
lastRead: number = unixNow();
webPushKeys: webPush.VapidKeys = webPush.generateVAPIDKeys();
@@ -39,7 +41,7 @@ export default class NotificationsManager extends EventEmitter<EventMap> {
async setup() {
this.state = (
await this.app.state.getMutableState<NotificationsManagerState>("notification-manager", { channels: [] })
await stateManager.getMutableState<NotificationsManagerState>("notification-manager", { channels: [] })
).proxy;
}
@@ -71,7 +73,7 @@ export default class NotificationsManager extends EventEmitter<EventMap> {
/** Whether a notification should be sent */
shouldNotify(event: NostrEvent) {
if (event.kind !== kinds.EncryptedDirectMessage) return;
if (getDMRecipient(event) !== this.app.config.data.owner) return;
if (getDMRecipient(event) !== config.data.owner) return;
if (event.created_at > this.lastRead) return true;
}
@@ -82,9 +84,8 @@ export default class NotificationsManager extends EventEmitter<EventMap> {
switch (event.kind) {
case kinds.EncryptedDirectMessage:
const sender = getDMSender(event);
const senderProfileEvent = await this.app.profileBook.loadProfile(sender);
const senderProfile = senderProfileEvent ? parseKind0Event(senderProfileEvent) : undefined;
const senderName = getUserDisplayName(senderProfile, sender);
const senderProfile = await this.app.profileBook.loadProfile(sender);
const senderName = senderProfile ? (getDisplayName(senderProfile) ?? "Unknown") : "Unknown";
return {
kind: event.kind,

View File

@@ -1,40 +1,35 @@
import { NostrEvent, kinds } from "nostr-tools";
import _throttle from "lodash.throttle";
import { tap } from "rxjs";
import { kinds } from "nostr-tools";
import { getObservableValue } from "applesauce-core/observable";
import { ProfileQuery } from "applesauce-core/queries";
import { COMMON_CONTACT_RELAYS } from "../env.js";
import { logger } from "../logger.js";
import App from "../app/index.js";
import PubkeyBatchLoader from "./pubkey-batch-loader.js";
import { replaceableLoader } from "../services/loaders.js";
import { eventStore, queryStore } from "../services/stores.js";
import { simpleTimeout } from "../operators/simple-timeout.js";
import { arrayFallback } from "../helpers/array.js";
const DEFAULT_REQUEST_TIMEOUT = 10_000;
/** loads kind 0 metadata for pubkeys */
export default class ProfileBook {
log = logger.extend("ProfileBook");
app: App;
loader: PubkeyBatchLoader;
extraRelays = COMMON_CONTACT_RELAYS;
constructor(app: App) {
this.app = app;
this.loader = new PubkeyBatchLoader(kinds.Metadata, this.app.pool, (pubkey) => {
return this.app.eventStore.getEventsForFilters([{ kinds: [kinds.Metadata], authors: [pubkey] }])?.[0];
});
this.loader.on("event", (event) => this.app.eventStore.addEvent(event));
this.loader.on("batch", (found, failed) => {
this.log(`Found ${found}, failed ${failed}, pending ${this.loader.queue}`);
});
}
getProfile(pubkey: string) {
return this.loader.getEvent(pubkey);
return eventStore.getReplaceable(kinds.Metadata, pubkey);
}
handleEvent(event: NostrEvent) {
this.loader.handleEvent(event);
}
async loadProfile(pubkey: string, relays?: string[], force?: boolean) {
relays = arrayFallback(relays, COMMON_CONTACT_RELAYS);
this.log(`Requesting profile from ${relays.length} relays for ${pubkey}`);
replaceableLoader.next({ kind: kinds.Metadata, pubkey, relays, force });
async loadProfile(pubkey: string, relays: string[] = []) {
return this.loader.getOrLoadEvent(pubkey, relays);
return getObservableValue(
queryStore.createQuery(ProfileQuery, pubkey).pipe(
simpleTimeout(DEFAULT_REQUEST_TIMEOUT, `Failed to load profile for ${pubkey}`),
tap((p) => p && this.log(`Found profile for ${pubkey}`, p)),
),
);
}
}

View File

@@ -1,36 +1,13 @@
import EventEmitter from "events";
import { NostrEvent, SimplePool, Filter } from "nostr-tools";
import { NostrEvent, SimplePool } from "nostr-tools";
import SuperMap from "@satellite-earth/core/helpers/super-map.js";
import { AbstractRelay, Subscription, SubscriptionParams } from "nostr-tools/abstract-relay";
import { getPubkeysFromList } from "@satellite-earth/core/helpers/nostr/lists.js";
import { getInboxes, getOutboxes } from "@satellite-earth/core/helpers/nostr/mailboxes.js";
import { getRelaysFromContactList } from "@satellite-earth/core/helpers/nostr/contacts.js";
import { Subscription } from "nostr-tools/abstract-relay";
import { getRelaysFromContactsEvent } from "applesauce-core/helpers";
import { BOOTSTRAP_RELAYS } from "../../env.js";
import { BOOTSTRAP_RELAYS, COMMON_CONTACT_RELAYS } from "../../env.js";
import { logger } from "../../logger.js";
import App from "../../app/index.js";
/** creates a new subscription and waits for it to get an event or close */
function asyncSubscription(relay: AbstractRelay, filters: Filter[], opts: SubscriptionParams) {
let resolved = false;
return new Promise<Subscription>((res, rej) => {
const sub = relay.subscribe(filters, {
onevent: (event) => {
if (!resolved) res(sub);
opts.onevent?.(event);
},
oneose: () => {
if (!resolved) res(sub);
opts.oneose?.();
},
onclose: (reason) => {
if (!resolved) rej(new Error(reason));
opts.onclose?.(reason);
},
});
});
}
import { arrayFallback } from "../../helpers/array.js";
type EventMap = {
started: [Receiver];
@@ -83,24 +60,26 @@ export default class Receiver extends EventEmitter<EventMap> {
const owner = this.app.config.data.owner;
if (!owner) throw new Error("Missing owner");
const ownerMailboxes = await this.app.addressBook.loadMailboxes(owner);
const ownerInboxes = getInboxes(ownerMailboxes);
const ownerOutboxes = getOutboxes(ownerMailboxes);
const commonMailboxesRelays = [...BOOTSTRAP_RELAYS, ...COMMON_CONTACT_RELAYS];
const ownerMailboxes = await this.app.addressBook.loadMailboxes(owner, commonMailboxesRelays, true);
this.log("Searching for owner kind:3 contacts");
const contacts = await this.app.contactBook.loadContacts(owner);
const contacts = await this.app.contactBook.loadContacts(
owner,
arrayFallback(ownerMailboxes?.outboxes, BOOTSTRAP_RELAYS),
true,
);
if (!contacts) throw new Error("Cant find contacts");
this.pubkeyRelays.clear();
this.relayPubkeys.clear();
// add the owners details
this.pubkeyRelays.set(owner, new Set(ownerOutboxes));
for (const url of ownerOutboxes) this.relayPubkeys.get(url).add(owner);
this.pubkeyRelays.set(owner, new Set(ownerMailboxes?.outboxes));
if (ownerMailboxes?.outboxes) for (const url of ownerMailboxes?.outboxes) this.relayPubkeys.get(url).add(owner);
const people = getPubkeysFromList(contacts);
this.log(`Found ${people.length} contacts`);
this.log(`Found ${contacts.length} contacts`);
let usersWithMailboxes = 0;
let usersWithContactRelays = 0;
@@ -108,20 +87,28 @@ export default class Receiver extends EventEmitter<EventMap> {
// fetch all addresses in parallel
await Promise.all(
people.map(async (person) => {
const mailboxes = await this.app.addressBook.loadMailboxes(person.pubkey, ownerInboxes ?? BOOTSTRAP_RELAYS);
contacts.map(async (person) => {
const mailboxes = await this.app.addressBook.loadMailboxes(
person.pubkey,
arrayFallback(ownerMailboxes?.inboxes, commonMailboxesRelays),
);
let relays = getOutboxes(mailboxes);
let relays = mailboxes?.outboxes ?? [];
// if the user does not have any mailboxes try to get the relays stored in the contact list
if (relays.length === 0) {
this.log(`Failed to find mailboxes for ${person.pubkey}`);
const contacts = await this.app.contactBook.loadContacts(person.pubkey, ownerInboxes ?? BOOTSTRAP_RELAYS);
const contacts = await this.app.contactBook.loadContactsEvent(
person.pubkey,
arrayFallback(ownerMailboxes?.inboxes, BOOTSTRAP_RELAYS),
);
if (contacts && contacts.content.startsWith("{")) {
const parsed = getRelaysFromContactList(contacts);
const parsed = getRelaysFromContactsEvent(contacts);
if (parsed) {
relays = parsed.filter((r) => r.write).map((r) => r.url);
relays = Array.from(parsed.entries())
.filter(([r, mode]) => mode === "all" || mode === "outbox")
.map(([r]) => r);
usersWithContactRelays++;
} else {
relays = BOOTSTRAP_RELAYS;

View File

@@ -5,7 +5,6 @@ import { Deferred, createDefer } from "applesauce-core/promise";
import App from "../../app/index.js";
import { logger } from "../../logger.js";
import { getPubkeysFromList } from "@satellite-earth/core/helpers/nostr/lists.js";
import PubkeyScrapper from "./pubkey-scrapper.js";
const MAX_TASKS = 10;
@@ -49,7 +48,7 @@ export default class Scrapper extends EventEmitter<EventMap> {
if (!contacts) throw new Error("Missing contact list");
return { contacts: getPubkeysFromList(contacts), mailboxes };
return { contacts, mailboxes };
}
private async scrapeOwner() {
@@ -104,7 +103,7 @@ export default class Scrapper extends EventEmitter<EventMap> {
// check running again since this is resuming
if (!this.running) return;
const promise = this.scrapeForPubkey(person.pubkey, person.relay);
const promise = this.scrapeForPubkey(person.pubkey, person.relays?.[0]);
// add it to the tasks array
this.tasks.add(promise);

View File

@@ -3,7 +3,6 @@ import { NostrEvent } from "nostr-tools";
import { EventEmitter } from "events";
import { Debugger } from "debug";
import { getOutboxes } from "@satellite-earth/core/helpers/nostr/mailboxes.js";
import PubkeyRelayScrapper, { PubkeyRelayScrapperState } from "./pubkey-relay-scrapper.js";
import { logger } from "../../logger.js";
@@ -39,9 +38,7 @@ export default class PubkeyScrapper extends EventEmitter<EventMap> {
async loadNext() {
const { mailboxes } = await this.ensureData();
const outboxes = getOutboxes(mailboxes);
const relays = [...outboxes, ...this.additionalRelays];
const relays = [...(mailboxes?.outboxes ?? []), ...this.additionalRelays];
const scrappers: PubkeyRelayScrapper[] = [];
for (const url of relays) {
if (this.failed.has(url)) continue;

View File

@@ -0,0 +1,5 @@
import { MonoTypeOperatorFunction, throwError, timeout } from "rxjs";
export function simpleTimeout<T extends unknown>(first: number, message: string): MonoTypeOperatorFunction<T> {
return timeout({ first, with: () => throwError(() => new Error(message)) });
}

10
src/services/config.ts Normal file
View File

@@ -0,0 +1,10 @@
import path from "node:path";
import ConfigManager from "../modules/config-manager.js";
import { DATA_PATH } from "../env.js";
const config = new ConfigManager(path.join(DATA_PATH, "node.json"));
config.read();
export default config;

7
src/services/database.ts Normal file
View File

@@ -0,0 +1,7 @@
import LocalDatabase from "../app/database.js";
import { DATA_PATH } from "../env.js";
// setup database
const database = new LocalDatabase({ directory: DATA_PATH });
export default database;

View File

@@ -0,0 +1,7 @@
import { SQLiteEventStore } from "@satellite-earth/core";
import database from "./database.js";
const eventCache = new SQLiteEventStore(database.db);
await eventCache.setup();
export default eventCache;

23
src/services/loaders.ts Normal file
View File

@@ -0,0 +1,23 @@
import { from, tap } from "rxjs";
import { Filter } from "nostr-tools";
import { isFromCache, markFromCache } from "applesauce-core/helpers";
import { ReplaceableLoader } from "applesauce-loaders/loaders";
import { rxNostr } from "./rx-nostr.js";
import eventCache from "./event-cache.js";
import { eventStore } from "./stores.js";
function cacheRequest(filters: Filter[]) {
const events = eventCache.getEventsForFilters(filters);
return from(events).pipe(tap(markFromCache));
}
export const replaceableLoader = new ReplaceableLoader(rxNostr, { cacheRequest });
replaceableLoader.subscribe((packet) => {
// add it to event store
const event = eventStore.add(packet.event, packet.from);
// save it to the cache if its new
if (!isFromCache(event)) eventCache.addEvent(event);
});

View File

@@ -0,0 +1,7 @@
import LogStore from "../modules/log-store/log-store.js";
import database from "./database.js";
const logStore = new LogStore(database.db);
await logStore.setup();
export default logStore;

36
src/services/rx-nostr.ts Normal file
View File

@@ -0,0 +1,36 @@
import { ConnectionState, createRxNostr } from "rx-nostr";
import { unixNow } from "applesauce-core/helpers";
import { verifyEvent } from "nostr-tools/wasm";
import { BehaviorSubject } from "rxjs";
import { nanoid } from "nanoid";
// import { logger } from "../logger.js";
// const log = logger.extend("rx-nostr");
export const rxNostr = createRxNostr({
verifier: async (event) => {
try {
return verifyEvent(event);
} catch (error) {
return false;
}
},
});
// keep track of all relay connection states
export const connections$ = new BehaviorSubject<Record<string, ConnectionState>>({});
rxNostr.createConnectionStateObservable().subscribe((packet) => {
const url = new URL(packet.from).toString();
connections$.next({ ...connections$.value, [url]: packet.state });
});
// capture all notices sent from relays
export const notices$ = new BehaviorSubject<{ id: string; from: string; message: string; timestamp: number }[]>([]);
rxNostr.createAllMessageObservable().subscribe((packet) => {
if (packet.type === "NOTICE") {
const from = new URL(packet.from).toString();
const notice = { id: nanoid(), from, message: packet.notice, timestamp: unixNow() };
notices$.next([...notices$.value, notice]);
}
});

9
src/services/secrets.ts Normal file
View File

@@ -0,0 +1,9 @@
import path from "node:path";
import SecretsManager from "../modules/secrets-manager.js";
import { DATA_PATH } from "../env.js";
const secrets = new SecretsManager(path.join(DATA_PATH, "secrets.json"));
secrets.read();
export default secrets;

7
src/services/state.ts Normal file
View File

@@ -0,0 +1,7 @@
import ApplicationStateManager from "../modules/state/application-state-manager.js";
import database from "./database.js";
const stateManager = new ApplicationStateManager(database.db);
await stateManager.setup();
export default stateManager;

20
src/services/stores.ts Normal file
View File

@@ -0,0 +1,20 @@
import { EventStore, QueryStore } from "applesauce-core";
import { IS_DEV } from "../env.js";
import { rxNostr } from "./rx-nostr.js";
export const eventStore = new EventStore();
export const queryStore = new QueryStore(eventStore);
// add all new events to event store
rxNostr.createAllEventObservable().subscribe((packet) => {
eventStore.add(packet.event, packet.from);
});
setTimeout(() => {
eventStore.database.prune();
}, 10 * 60_000);
if (IS_DEV) {
// @ts-ignore
global.eventStore = eventStore;
}