mirror of
https://gitlab.com/soapbox-pub/ditto.git
synced 2025-12-06 11:29:46 +00:00
Remove InternalRelay (pubsub) store
This commit is contained in:
parent
6568dca191
commit
d9a466c0ee
8 changed files with 23 additions and 196 deletions
|
|
@ -123,7 +123,7 @@ async function getToken(
|
||||||
encryption: 'nip44',
|
encryption: 'nip44',
|
||||||
pubkey: bunkerPubkey,
|
pubkey: bunkerPubkey,
|
||||||
signer: new NSecSigner(nip46Seckey),
|
signer: new NSecSigner(nip46Seckey),
|
||||||
relay: await Storages.pubsub(), // TODO: Use the relays from the request.
|
relay: await Storages.db(), // TODO: Use the relays from the request.
|
||||||
timeout: 60_000,
|
timeout: 60_000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -94,8 +94,6 @@ const streamingController: AppController = async (c) => {
|
||||||
const { socket, response } = Deno.upgradeWebSocket(c.req.raw, { protocol: token, idleTimeout: 30 });
|
const { socket, response } = Deno.upgradeWebSocket(c.req.raw, { protocol: token, idleTimeout: 30 });
|
||||||
|
|
||||||
const store = await Storages.db();
|
const store = await Storages.db();
|
||||||
const pubsub = await Storages.pubsub();
|
|
||||||
|
|
||||||
const policy = pubkey ? new MuteListPolicy(pubkey, await Storages.admin()) : undefined;
|
const policy = pubkey ? new MuteListPolicy(pubkey, await Storages.admin()) : undefined;
|
||||||
|
|
||||||
function send(e: StreamingEvent) {
|
function send(e: StreamingEvent) {
|
||||||
|
|
@ -107,7 +105,7 @@ const streamingController: AppController = async (c) => {
|
||||||
|
|
||||||
async function sub(filters: NostrFilter[], render: (event: NostrEvent) => Promise<StreamingEvent | undefined>) {
|
async function sub(filters: NostrFilter[], render: (event: NostrEvent) => Promise<StreamingEvent | undefined>) {
|
||||||
try {
|
try {
|
||||||
for await (const msg of pubsub.req(filters, { signal: controller.signal })) {
|
for await (const msg of store.req(filters, { signal: controller.signal })) {
|
||||||
if (msg[0] === 'EVENT') {
|
if (msg[0] === 'EVENT') {
|
||||||
const event = msg[2];
|
const event = msg[2];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,9 +23,6 @@ import { errorJson } from '@/utils/log.ts';
|
||||||
import { purifyEvent } from '@/utils/purify.ts';
|
import { purifyEvent } from '@/utils/purify.ts';
|
||||||
import { Time } from '@/utils/time.ts';
|
import { Time } from '@/utils/time.ts';
|
||||||
|
|
||||||
/** Limit of initial events returned for a subscription. */
|
|
||||||
const FILTER_LIMIT = 100;
|
|
||||||
|
|
||||||
const limiters = {
|
const limiters = {
|
||||||
msg: new MemoryRateLimiter({ limit: 300, window: Time.minutes(1) }),
|
msg: new MemoryRateLimiter({ limit: 300, window: Time.minutes(1) }),
|
||||||
req: new MultiRateLimiter([
|
req: new MultiRateLimiter([
|
||||||
|
|
@ -126,11 +123,10 @@ function connectStream(socket: WebSocket, ip: string | undefined, conf: DittoCon
|
||||||
controllers.set(subId, controller);
|
controllers.set(subId, controller);
|
||||||
|
|
||||||
const store = await Storages.db();
|
const store = await Storages.db();
|
||||||
const pubsub = await Storages.pubsub();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (const event of await store.query(filters, { limit: FILTER_LIMIT, timeout: conf.db.timeouts.relay })) {
|
for await (const [verb, , ...rest] of store.req(filters, { timeout: conf.db.timeouts.relay })) {
|
||||||
send(['EVENT', subId, purifyEvent(event)]);
|
send([verb, subId, ...rest] as NostrRelayMsg);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof RelayError) {
|
if (e instanceof RelayError) {
|
||||||
|
|
@ -143,18 +139,6 @@ function connectStream(socket: WebSocket, ip: string | undefined, conf: DittoCon
|
||||||
controllers.delete(subId);
|
controllers.delete(subId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
send(['EOSE', subId]);
|
|
||||||
|
|
||||||
try {
|
|
||||||
for await (const msg of pubsub.req(filters, { signal: controller.signal })) {
|
|
||||||
if (msg[0] === 'EVENT') {
|
|
||||||
send(['EVENT', subId, msg[2]]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
controllers.delete(subId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Handle EVENT. Store the event. */
|
/** Handle EVENT. Store the event. */
|
||||||
|
|
|
||||||
|
|
@ -77,42 +77,21 @@ async function handleEvent(event: DittoEvent, opts: PipelineOpts): Promise<void>
|
||||||
// NIP-46 events get special treatment.
|
// NIP-46 events get special treatment.
|
||||||
// They are exempt from policies and other side-effects, and should be streamed out immediately.
|
// They are exempt from policies and other side-effects, and should be streamed out immediately.
|
||||||
// If streaming fails, an error should be returned.
|
// If streaming fails, an error should be returned.
|
||||||
if (event.kind === 24133) {
|
if (event.kind !== 24133) {
|
||||||
await streamOut(event);
|
// Ensure the event doesn't violate the policy.
|
||||||
return;
|
if (event.pubkey !== Conf.pubkey) {
|
||||||
}
|
await policyFilter(event, opts.signal);
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure the event doesn't violate the policy.
|
// Prepare the event for additional checks.
|
||||||
if (event.pubkey !== Conf.pubkey) {
|
// FIXME: This is kind of hacky. Should be reorganized to fetch only what's needed for each stage.
|
||||||
await policyFilter(event, opts.signal);
|
await hydrateEvent(event, opts.signal);
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare the event for additional checks.
|
// Ensure that the author is not banned.
|
||||||
// FIXME: This is kind of hacky. Should be reorganized to fetch only what's needed for each stage.
|
const n = getTagSet(event.user?.tags ?? [], 'n');
|
||||||
await hydrateEvent(event, opts.signal);
|
if (n.has('disabled')) {
|
||||||
|
throw new RelayError('blocked', 'author is blocked');
|
||||||
// Ensure that the author is not banned.
|
}
|
||||||
const n = getTagSet(event.user?.tags ?? [], 'n');
|
|
||||||
if (n.has('disabled')) {
|
|
||||||
throw new RelayError('blocked', 'author is blocked');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ephemeral events must throw if they are not streamed out.
|
|
||||||
if (NKinds.ephemeral(event.kind)) {
|
|
||||||
await Promise.all([
|
|
||||||
streamOut(event),
|
|
||||||
webPush(event),
|
|
||||||
]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Events received through notify are thought to already be in the database, so they only need to be streamed.
|
|
||||||
if (opts.source === 'notify') {
|
|
||||||
await Promise.all([
|
|
||||||
streamOut(event),
|
|
||||||
webPush(event),
|
|
||||||
]);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const kysely = await Storages.kysely();
|
const kysely = await Storages.kysely();
|
||||||
|
|
@ -127,12 +106,7 @@ async function handleEvent(event: DittoEvent, opts: PipelineOpts): Promise<void>
|
||||||
prewarmLinkPreview(event, opts.signal),
|
prewarmLinkPreview(event, opts.signal),
|
||||||
generateSetEvents(event),
|
generateSetEvents(event),
|
||||||
])
|
])
|
||||||
.then(() =>
|
.then(() => webPush(event));
|
||||||
Promise.allSettled([
|
|
||||||
streamOut(event),
|
|
||||||
webPush(event),
|
|
||||||
])
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -165,12 +139,13 @@ async function hydrateEvent(event: DittoEvent, signal: AbortSignal): Promise<voi
|
||||||
|
|
||||||
/** Maybe store the event, if eligible. */
|
/** Maybe store the event, if eligible. */
|
||||||
async function storeEvent(event: NostrEvent, signal?: AbortSignal): Promise<undefined> {
|
async function storeEvent(event: NostrEvent, signal?: AbortSignal): Promise<undefined> {
|
||||||
if (NKinds.ephemeral(event.kind)) return;
|
|
||||||
const store = await Storages.db();
|
const store = await Storages.db();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await store.transaction(async (store, kysely) => {
|
await store.transaction(async (store, kysely) => {
|
||||||
await updateStats({ event, store, kysely });
|
if (!NKinds.ephemeral(event.kind)) {
|
||||||
|
await updateStats({ event, store, kysely });
|
||||||
|
}
|
||||||
await store.event(event, { signal });
|
await store.event(event, { signal });
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -274,16 +249,6 @@ function isFresh(event: NostrEvent): boolean {
|
||||||
return eventAge(event) < Time.minutes(1);
|
return eventAge(event) < Time.minutes(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Distribute the event through active subscriptions. */
|
|
||||||
async function streamOut(event: NostrEvent): Promise<void> {
|
|
||||||
if (!isFresh(event)) {
|
|
||||||
throw new RelayError('invalid', 'event too old');
|
|
||||||
}
|
|
||||||
|
|
||||||
const pubsub = await Storages.pubsub();
|
|
||||||
await pubsub.event(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function webPush(event: NostrEvent): Promise<void> {
|
async function webPush(event: NostrEvent): Promise<void> {
|
||||||
if (!isFresh(event)) {
|
if (!isFresh(event)) {
|
||||||
throw new RelayError('invalid', 'event too old');
|
throw new RelayError('invalid', 'event too old');
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ export class ConnectSigner implements NostrSigner {
|
||||||
encryption: 'nip44',
|
encryption: 'nip44',
|
||||||
pubkey: this.opts.bunkerPubkey,
|
pubkey: this.opts.bunkerPubkey,
|
||||||
// TODO: use a remote relay for `nprofile` signing (if present and `Conf.relay` isn't already in the list)
|
// TODO: use a remote relay for `nprofile` signing (if present and `Conf.relay` isn't already in the list)
|
||||||
relay: await Storages.pubsub(),
|
relay: await Storages.db(),
|
||||||
signer,
|
signer,
|
||||||
timeout: 60_000,
|
timeout: 60_000,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,12 @@
|
||||||
// deno-lint-ignore-file require-await
|
// deno-lint-ignore-file require-await
|
||||||
import { type DittoDatabase, DittoDB } from '@ditto/db';
|
import { type DittoDatabase, DittoDB } from '@ditto/db';
|
||||||
import { internalSubscriptionsSizeGauge } from '@ditto/metrics';
|
import { NPool, NRelay1 } from '@nostrify/nostrify';
|
||||||
import { logi } from '@soapbox/logi';
|
import { logi } from '@soapbox/logi';
|
||||||
|
|
||||||
import { Conf } from '@/config.ts';
|
import { Conf } from '@/config.ts';
|
||||||
import { wsUrlSchema } from '@/schema.ts';
|
import { wsUrlSchema } from '@/schema.ts';
|
||||||
import { AdminStore } from '@/storages/AdminStore.ts';
|
import { AdminStore } from '@/storages/AdminStore.ts';
|
||||||
import { DittoPgStore } from '@/storages/DittoPgStore.ts';
|
import { DittoPgStore } from '@/storages/DittoPgStore.ts';
|
||||||
import { InternalRelay } from '@/storages/InternalRelay.ts';
|
|
||||||
import { NPool, NRelay1 } from '@nostrify/nostrify';
|
|
||||||
import { getRelays } from '@/utils/outbox.ts';
|
import { getRelays } from '@/utils/outbox.ts';
|
||||||
import { seedZapSplits } from '@/utils/zap-split.ts';
|
import { seedZapSplits } from '@/utils/zap-split.ts';
|
||||||
|
|
||||||
|
|
@ -17,7 +15,6 @@ export class Storages {
|
||||||
private static _database: Promise<DittoDatabase> | undefined;
|
private static _database: Promise<DittoDatabase> | undefined;
|
||||||
private static _admin: Promise<AdminStore> | undefined;
|
private static _admin: Promise<AdminStore> | undefined;
|
||||||
private static _client: Promise<NPool<NRelay1>> | undefined;
|
private static _client: Promise<NPool<NRelay1>> | undefined;
|
||||||
private static _pubsub: Promise<InternalRelay> | undefined;
|
|
||||||
|
|
||||||
public static async database(): Promise<DittoDatabase> {
|
public static async database(): Promise<DittoDatabase> {
|
||||||
if (!this._database) {
|
if (!this._database) {
|
||||||
|
|
@ -59,14 +56,6 @@ export class Storages {
|
||||||
return this._admin;
|
return this._admin;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Internal pubsub relay between controllers and the pipeline. */
|
|
||||||
public static async pubsub(): Promise<InternalRelay> {
|
|
||||||
if (!this._pubsub) {
|
|
||||||
this._pubsub = Promise.resolve(new InternalRelay({ gauge: internalSubscriptionsSizeGauge }));
|
|
||||||
}
|
|
||||||
return this._pubsub;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Relay pool storage. */
|
/** Relay pool storage. */
|
||||||
public static async client(): Promise<NPool<NRelay1>> {
|
public static async client(): Promise<NPool<NRelay1>> {
|
||||||
if (!this._client) {
|
if (!this._client) {
|
||||||
|
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
import { assertEquals } from '@std/assert';
|
|
||||||
|
|
||||||
import { eventFixture } from '@/test.ts';
|
|
||||||
|
|
||||||
import { InternalRelay } from './InternalRelay.ts';
|
|
||||||
|
|
||||||
Deno.test('InternalRelay', async () => {
|
|
||||||
const relay = new InternalRelay();
|
|
||||||
const event1 = await eventFixture('event-1');
|
|
||||||
|
|
||||||
const promise = new Promise((resolve) => setTimeout(() => resolve(relay.event(event1)), 0));
|
|
||||||
|
|
||||||
for await (const msg of relay.req([{}])) {
|
|
||||||
if (msg[0] === 'EVENT') {
|
|
||||||
assertEquals(relay.subs.size, 1);
|
|
||||||
assertEquals(msg[2], event1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await promise;
|
|
||||||
assertEquals(relay.subs.size, 0); // cleanup
|
|
||||||
});
|
|
||||||
|
|
@ -1,86 +0,0 @@
|
||||||
// deno-lint-ignore-file require-await
|
|
||||||
import {
|
|
||||||
NIP50,
|
|
||||||
NostrEvent,
|
|
||||||
NostrFilter,
|
|
||||||
NostrRelayCLOSED,
|
|
||||||
NostrRelayEOSE,
|
|
||||||
NostrRelayEVENT,
|
|
||||||
NRelay,
|
|
||||||
} from '@nostrify/nostrify';
|
|
||||||
import { Machina } from '@nostrify/nostrify/utils';
|
|
||||||
import { matchFilter } from 'nostr-tools';
|
|
||||||
import { Gauge } from 'prom-client';
|
|
||||||
|
|
||||||
import { DittoEvent } from '@/interfaces/DittoEvent.ts';
|
|
||||||
import { purifyEvent } from '@/utils/purify.ts';
|
|
||||||
|
|
||||||
interface InternalRelayOpts {
|
|
||||||
gauge?: Gauge;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PubSub event store for streaming events within the application.
|
|
||||||
* The pipeline should push events to it, then anything in the application can subscribe to it.
|
|
||||||
*/
|
|
||||||
export class InternalRelay implements NRelay {
|
|
||||||
readonly subs = new Map<string, { filters: NostrFilter[]; machina: Machina<NostrEvent> }>();
|
|
||||||
|
|
||||||
constructor(private opts: InternalRelayOpts = {}) {}
|
|
||||||
|
|
||||||
async *req(
|
|
||||||
filters: NostrFilter[],
|
|
||||||
opts?: { signal?: AbortSignal },
|
|
||||||
): AsyncGenerator<NostrRelayEVENT | NostrRelayEOSE | NostrRelayCLOSED> {
|
|
||||||
const id = crypto.randomUUID();
|
|
||||||
const machina = new Machina<NostrEvent>(opts?.signal);
|
|
||||||
|
|
||||||
yield ['EOSE', id];
|
|
||||||
|
|
||||||
this.subs.set(id, { filters, machina });
|
|
||||||
this.opts.gauge?.set(this.subs.size);
|
|
||||||
|
|
||||||
try {
|
|
||||||
for await (const event of machina) {
|
|
||||||
yield ['EVENT', id, event];
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
this.subs.delete(id);
|
|
||||||
this.opts.gauge?.set(this.subs.size);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async event(event: DittoEvent): Promise<void> {
|
|
||||||
for (const { filters, machina } of this.subs.values()) {
|
|
||||||
for (const filter of filters) {
|
|
||||||
if (matchFilter(filter, event)) {
|
|
||||||
if (filter.search) {
|
|
||||||
const tokens = NIP50.parseInput(filter.search);
|
|
||||||
|
|
||||||
const domain = (tokens.find((t) =>
|
|
||||||
typeof t === 'object' && t.key === 'domain'
|
|
||||||
) as { key: 'domain'; value: string } | undefined)?.value;
|
|
||||||
|
|
||||||
if (domain === event.author_stats?.nip05_hostname) {
|
|
||||||
machina.push(purifyEvent(event));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
machina.push(purifyEvent(event));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
async query(): Promise<NostrEvent[]> {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
async close(): Promise<void> {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Add table
Reference in a new issue