diff --git a/packages/common/src/client-context.ts b/packages/common/src/client-context.ts index 4c4be04a..f3d68dc9 100644 --- a/packages/common/src/client-context.ts +++ b/packages/common/src/client-context.ts @@ -40,7 +40,7 @@ export class ClientContext { * @param options Client context options */ constructor( - private readonly client: Client, + readonly client: Client, options: ClientContextOptions = {}, ) { this.id = randomUUID(); diff --git a/packages/common/src/interfaces/common.interface.ts b/packages/common/src/interfaces/common.interface.ts index 9cdd77f1..daf788fb 100644 --- a/packages/common/src/interfaces/common.interface.ts +++ b/packages/common/src/interfaces/common.interface.ts @@ -4,3 +4,5 @@ export type Signature = string; export type Tag = string[]; export type SubscriptionId = string; + +export type KeysOfUnion = T extends T ? keyof T : never; diff --git a/packages/common/src/interfaces/event.interface.ts b/packages/common/src/interfaces/event.interface.ts index 32cab81f..2eb92274 100644 --- a/packages/common/src/interfaces/event.interface.ts +++ b/packages/common/src/interfaces/event.interface.ts @@ -10,7 +10,7 @@ export interface Event { sig: Signature; } -export type EventHandleResult = { +export type HandleEventResult = { success: boolean; message?: string; noReplyNeeded?: boolean; diff --git a/packages/core/src/interfaces/handle-result.interface.ts b/packages/common/src/interfaces/handle-result.interface.ts similarity index 52% rename from packages/core/src/interfaces/handle-result.interface.ts rename to packages/common/src/interfaces/handle-result.interface.ts index 76974529..2c2dddee 100644 --- a/packages/core/src/interfaces/handle-result.interface.ts +++ b/packages/common/src/interfaces/handle-result.interface.ts @@ -1,41 +1,5 @@ -import { Event, LogLevel, Logger, MessageType } from '@nostr-relay/common'; - -/** - * Options for NostrRelay - */ -export type NostrRelayOptions = { - /** - * Domain name of the Nostr Relay server. If not set, NIP-42 is not enabled. - * More info: https://github.com/nostr-protocol/nips/blob/master/42.md - */ - domain?: string; - /** - * Logger to use. `Default: ConsoleLoggerService` - */ - logger?: Logger; - /** - * The minimum log level to log. `Default: LogLevel.INFO` - */ - logLevel?: LogLevel; - createdAtUpperLimit?: number; - createdAtLowerLimit?: number; - /** - * Allowed minimum PoW difficulty for events.` Default: 0` - */ - minPowDifficulty?: number; - /** - * Maximum number of subscriptions per client. `Default: 20` - */ - maxSubscriptionsPerClient?: number; - /** - * TTL for filter result cache in milliseconds. `Default: 1000` - */ - filterResultCacheTtl?: number; - /** - * TTL for event handling result cache in milliseconds. `Default: 600000` - */ - eventHandlingResultCacheTtl?: number; -}; +import { MessageType } from '../constants'; +import { Event } from './event.interface'; /** * Result of handling REQ message diff --git a/packages/common/src/interfaces/index.ts b/packages/common/src/interfaces/index.ts index bef5ba94..a1998409 100644 --- a/packages/common/src/interfaces/index.ts +++ b/packages/common/src/interfaces/index.ts @@ -3,6 +3,8 @@ export * from './common.interface'; export * from './event-repository.interface'; export * from './event.interface'; export * from './filter.interface'; +export * from './handle-result.interface'; export * from './logger.interface'; export * from './message.interface'; +export * from './nostr-relay-options.interface'; export * from './plugin.interface'; diff --git a/packages/common/src/interfaces/nostr-relay-options.interface.ts b/packages/common/src/interfaces/nostr-relay-options.interface.ts new file mode 100644 index 00000000..79a334aa --- /dev/null +++ b/packages/common/src/interfaces/nostr-relay-options.interface.ts @@ -0,0 +1,39 @@ +import { LogLevel } from '../constants'; +import { Logger } from './logger.interface'; + +/** + * Options for NostrRelay + */ +export type NostrRelayOptions = { + /** + * Domain name of the Nostr Relay server. If not set, NIP-42 is not enabled. + * More info: https://github.com/nostr-protocol/nips/blob/master/42.md + */ + domain?: string; + /** + * Logger to use. `Default: ConsoleLoggerService` + */ + logger?: Logger; + /** + * The minimum log level to log. `Default: LogLevel.INFO` + */ + logLevel?: LogLevel; + createdAtUpperLimit?: number; + createdAtLowerLimit?: number; + /** + * Allowed minimum PoW difficulty for events.` Default: 0` + */ + minPowDifficulty?: number; + /** + * Maximum number of subscriptions per client. `Default: 20` + */ + maxSubscriptionsPerClient?: number; + /** + * TTL for filter result cache in milliseconds. `Default: 1000` + */ + filterResultCacheTtl?: number; + /** + * TTL for event handling result cache in milliseconds. `Default: 600000` + */ + eventHandlingResultCacheTtl?: number; +}; diff --git a/packages/common/src/interfaces/plugin.interface.ts b/packages/common/src/interfaces/plugin.interface.ts index d74842e1..0e5e0cd7 100644 --- a/packages/common/src/interfaces/plugin.interface.ts +++ b/packages/common/src/interfaces/plugin.interface.ts @@ -1,42 +1,22 @@ import { ClientContext } from '../client-context'; -import { Event, EventHandleResult } from './event.interface'; +import { Event } from './event.interface'; +import { HandleMessageResult } from './handle-result.interface'; +import { IncomingMessage } from './message.interface'; -export type NostrRelayPlugin = - | BeforeEventHandle - | AfterEventHandle - | BeforeEventBroadcast - | AfterEventBroadcast; +export type NostrRelayPlugin = HandleMessageMiddleware | BroadcastMiddleware; -export type BeforeHookResult = - | { canContinue: true } - | ({ canContinue: false } & T); - -export type BeforeEventHandleResult = BeforeHookResult<{ - result: EventHandleResult; -}>; - -export interface BeforeEventHandle { - beforeEventHandle( +export interface HandleMessageMiddleware { + handleMessage( ctx: ClientContext, - event: Event, - ): Promise | BeforeEventHandleResult; + message: IncomingMessage, + next: () => Promise, + ): Promise | HandleMessageResult; } -export interface AfterEventHandle { - afterEventHandle( +export interface BroadcastMiddleware { + broadcast( ctx: ClientContext, event: Event, - handleResult: EventHandleResult, + next: () => Promise, ): Promise | void; } - -export interface BeforeEventBroadcast { - beforeEventBroadcast( - ctx: ClientContext, - event: Event, - ): Promise | BeforeHookResult; -} - -export interface AfterEventBroadcast { - afterEventBroadcast(ctx: ClientContext, event: Event): Promise | void; -} diff --git a/packages/core/__test__/nostr-relay.spec.ts b/packages/core/__test__/nostr-relay.spec.ts index 3f0ad17c..da92a82b 100644 --- a/packages/core/__test__/nostr-relay.spec.ts +++ b/packages/core/__test__/nostr-relay.spec.ts @@ -81,11 +81,6 @@ describe('NostrRelay', () => { '', ]; - const mockPlugin = { - beforeEventHandle: jest.fn().mockReturnValue({ canContinue: true }), - afterEventHandle: jest.fn().mockReturnValue(handleResult), - }; - nostrRelay.register(mockPlugin); const mockHandleEvent = jest .spyOn(nostrRelay['eventService'], 'handleEvent') .mockResolvedValue(handleResult); @@ -93,12 +88,6 @@ describe('NostrRelay', () => { await nostrRelay.handleEventMessage(client, event); - expect(mockPlugin.beforeEventHandle).toHaveBeenCalledWith(ctx, event); - expect(mockPlugin.afterEventHandle).toHaveBeenCalledWith( - ctx, - event, - handleResult, - ); expect(mockHandleEvent).toHaveBeenCalledWith(ctx, event); expect(client.send).toHaveBeenCalledWith(JSON.stringify(outgoingMessage)); }); @@ -156,19 +145,6 @@ describe('NostrRelay', () => { expect(client.send).toHaveBeenNthCalledWith(1, outgoingMessageStr); expect(client.send).toHaveBeenNthCalledWith(2, outgoingMessageStr); }); - - it('should not handle event due to plugin prevention', async () => { - jest - .spyOn(nostrRelay['pluginManagerService'], 'callBeforeEventHandleHooks') - .mockResolvedValue({ canContinue: false, result: {} as any }); - const mockHandleEvent = jest - .spyOn(nostrRelay['eventService'], 'handleEvent') - .mockResolvedValue({ success: true }); - - await nostrRelay.handleEventMessage(client, { id: 'eventId' } as Event); - - expect(mockHandleEvent).not.toHaveBeenCalled(); - }); }); describe('req', () => { diff --git a/packages/core/__test__/services/event.service.spec.ts b/packages/core/__test__/services/event.service.spec.ts index 17fafb8e..aaa78631 100644 --- a/packages/core/__test__/services/event.service.spec.ts +++ b/packages/core/__test__/services/event.service.spec.ts @@ -131,45 +131,14 @@ describe('eventService', () => { }); it('should handle ephemeral event successfully', async () => { - const mockBeforeEventBroadcast = jest - .spyOn( - eventService['pluginManagerService'], - 'callBeforeEventBroadcastHooks', - ) - .mockResolvedValue({ canContinue: true }); - const mockAfterEventBroadcast = jest - .spyOn( - eventService['pluginManagerService'], - 'callAfterEventBroadcastHooks', - ) - .mockImplementation(); - jest.spyOn(EventUtils, 'validate').mockReturnValue(undefined); - const event = { id: 'a', kind: EventKind.EPHEMERAL_FIRST } as Event; - expect(await eventService.handleEvent(ctx, event)).toEqual({ - noReplyNeeded: true, - success: true, - }); - expect(mockBeforeEventBroadcast).toHaveBeenCalledWith(ctx, event); - expect(mockAfterEventBroadcast).toHaveBeenCalledWith(ctx, event); - expect(subscriptionService.broadcast).toHaveBeenCalledWith(event); - }); - - it('should not broadcast due to plugin prevention', async () => { - jest - .spyOn( - eventService['pluginManagerService'], - 'callBeforeEventBroadcastHooks', - ) - .mockResolvedValue({ canContinue: false }); jest.spyOn(EventUtils, 'validate').mockReturnValue(undefined); - const event = { id: 'a', kind: EventKind.EPHEMERAL_FIRST } as Event; expect(await eventService.handleEvent(ctx, event)).toEqual({ noReplyNeeded: true, success: true, }); - expect(subscriptionService.broadcast).not.toHaveBeenCalled(); + expect(subscriptionService.broadcast).toHaveBeenCalledWith(event); }); it('should handle regular event successfully', async () => { diff --git a/packages/core/__test__/services/plugin-manager.service.spec.ts b/packages/core/__test__/services/plugin-manager.service.spec.ts index bb1fc898..42f86255 100644 --- a/packages/core/__test__/services/plugin-manager.service.spec.ts +++ b/packages/core/__test__/services/plugin-manager.service.spec.ts @@ -1,4 +1,9 @@ -import { ClientContext, ClientReadyState, Event } from '../../../common'; +import { + ClientContext, + ClientReadyState, + Event, + IncomingMessage, +} from '../../../common'; import { PluginManagerService } from '../../src/services/plugin-manager.service'; describe('PluginManagerService', () => { @@ -14,246 +19,143 @@ describe('PluginManagerService', () => { }); describe('register', () => { - it('should register before event handler plugin', () => { + it('should register plugin', () => { const plugin = { - beforeEventHandle: jest.fn(), + handleMessage: jest.fn(), + broadcast: jest.fn(), }; pluginManagerService.register(plugin); - expect(pluginManagerService['beforeEventHandlePlugins']).toEqual([ + expect(pluginManagerService['handleMessageMiddlewares']).toEqual([ plugin, ]); + expect(pluginManagerService['broadcastMiddlewares']).toEqual([plugin]); }); - it('should register after event handler plugin', () => { - const plugin = { - afterEventHandle: jest.fn(), + it('should register plugins', () => { + const plugin1 = { + handleMessage: jest.fn(), }; - - pluginManagerService.register(plugin); - - expect(pluginManagerService['afterEventHandlePlugins']).toEqual([plugin]); - }); - - it('should register before event broadcast plugin', () => { - const plugin = { - beforeEventBroadcast: jest.fn(), + const plugin2 = { + broadcast: jest.fn(), }; - - pluginManagerService.register(plugin); - - expect(pluginManagerService['beforeEventBroadcastPlugins']).toEqual([ - plugin, - ]); - }); - - it('should register after event broadcast plugin', () => { - const plugin = { - afterEventBroadcast: jest.fn(), + const plugin3 = { + handleMessage: jest.fn(), + broadcast: jest.fn(), }; - pluginManagerService.register(plugin); + pluginManagerService.register(plugin1, plugin2).register(plugin3); - expect(pluginManagerService['afterEventBroadcastPlugins']).toEqual([ - plugin, + expect(pluginManagerService['handleMessageMiddlewares']).toEqual([ + plugin1, + plugin3, ]); - }); - - it('should register multiple plugins', () => { - const pluginA = { - beforeEventHandle: jest.fn(), - beforeEventBroadcast: jest.fn(), - }; - const pluginB = { - afterEventHandle: jest.fn(), - afterEventBroadcast: jest.fn(), - }; - - pluginManagerService.register(pluginA); - pluginManagerService.register(pluginB); - - expect(pluginManagerService['beforeEventHandlePlugins']).toEqual([ - pluginA, + expect(pluginManagerService['broadcastMiddlewares']).toEqual([ + plugin2, + plugin3, ]); - expect(pluginManagerService['afterEventHandlePlugins']).toEqual([ - pluginB, - ]); - expect(pluginManagerService['beforeEventBroadcastPlugins']).toEqual([ - pluginA, - ]); - expect(pluginManagerService['afterEventBroadcastPlugins']).toEqual([ - pluginB, - ]); - }); - }); - - describe('callBeforeEventHandleHooks', () => { - it('should run before event handle plugins', async () => { - const pluginA = { - beforeEventHandle: jest.fn().mockReturnValue({ canContinue: true }), - }; - const pluginB = { - beforeEventHandle: jest.fn().mockReturnValue({ canContinue: true }), - }; - const event = {} as Event; - - pluginManagerService.register(pluginA); - pluginManagerService.register(pluginB); - - const result = await pluginManagerService.callBeforeEventHandleHooks( - ctx, - event, - ); - - expect(result).toEqual({ canContinue: true }); - expect(pluginA.beforeEventHandle).toHaveBeenCalledWith(ctx, event); - expect(pluginB.beforeEventHandle).toHaveBeenCalledWith(ctx, event); - - const pluginACallOrder = - pluginA.beforeEventHandle.mock.invocationCallOrder[0]; - const pluginBCallOrder = - pluginB.beforeEventHandle.mock.invocationCallOrder[0]; - expect(pluginACallOrder).toBeLessThan(pluginBCallOrder); - }); - - it('should return false if any plugin returns false', async () => { - const pluginA = { - beforeEventHandle: jest.fn().mockReturnValue(false), - }; - const pluginB = { - beforeEventHandle: jest.fn().mockReturnValue(true), - }; - const event = {} as Event; - - pluginManagerService.register(pluginA); - pluginManagerService.register(pluginB); - - const result = await pluginManagerService.callBeforeEventHandleHooks( - ctx, - event, - ); - - expect(result).toBe(false); - expect(pluginA.beforeEventHandle).toHaveBeenCalledWith(ctx, event); - expect(pluginB.beforeEventHandle).not.toHaveBeenCalled(); }); }); - describe('callAfterEventHandleHooks', () => { - it('should run after event handle plugins', async () => { - const handleResult = { needResponse: true, success: true }; - const pluginA = { - afterEventHandle: jest.fn().mockImplementation(), - }; - const pluginB = { - afterEventHandle: jest.fn().mockImplementation(), - }; - const event = {} as Event; - - pluginManagerService.register(pluginA); - pluginManagerService.register(pluginB); - - await pluginManagerService.callAfterEventHandleHooks( - ctx, - event, - handleResult, + describe('handleMessage', () => { + it('should call middlewares in order', async () => { + const arr: number[] = []; + pluginManagerService.register( + { + handleMessage: async (_ctx, _message, next) => { + arr.push(1); + const result = await next(); + arr.push(5); + return result; + }, + }, + { + handleMessage: async (_ctx, _message, next) => { + arr.push(2); + const result = await next(); + arr.push(4); + return result; + }, + }, ); + const mockNext = jest.fn().mockImplementation(async () => { + arr.push(3); + return { messageType: 'EVENT', success: true }; + }); - expect(pluginB.afterEventHandle).toHaveBeenCalledWith( - ctx, - event, - handleResult, - ); - expect(pluginA.afterEventHandle).toHaveBeenCalledWith( + await pluginManagerService.handleMessage( ctx, - event, - handleResult, + {} as IncomingMessage, + mockNext, ); - const pluginBCallOrder = - pluginB.afterEventHandle.mock.invocationCallOrder[0]; - const pluginACallOrder = - pluginA.afterEventHandle.mock.invocationCallOrder[0]; - expect(pluginBCallOrder).toBeLessThan(pluginACallOrder); + expect(arr).toEqual([1, 2, 3, 4, 5]); + expect(mockNext).toHaveBeenCalledTimes(1); + expect(mockNext).toHaveBeenCalledWith(ctx, {}); }); - }); - describe('callBeforeEventBroadcastHooks', () => { - it('should run before event broadcast plugins', async () => { - const pluginA = { - beforeEventBroadcast: jest.fn().mockReturnValue({ canContinue: true }), - }; - const pluginB = { - beforeEventBroadcast: jest.fn().mockReturnValue({ canContinue: true }), - }; - const event = {} as Event; + it('should directly return if middleware does not call next', async () => { + pluginManagerService.register({ + handleMessage: async () => { + return { messageType: 'EVENT', success: false }; + }, + }); - pluginManagerService.register(pluginA); - pluginManagerService.register(pluginB); - - const result = await pluginManagerService.callBeforeEventBroadcastHooks( + const result = await pluginManagerService.handleMessage( ctx, - event, + {} as IncomingMessage, + async () => { + return { messageType: 'EVENT', success: true }; + }, ); - expect(result).toEqual({ canContinue: true }); - expect(pluginA.beforeEventBroadcast).toHaveBeenCalledWith(ctx, event); - expect(pluginB.beforeEventBroadcast).toHaveBeenCalledWith(ctx, event); - - const pluginACallOrder = - pluginA.beforeEventBroadcast.mock.invocationCallOrder[0]; - const pluginBCallOrder = - pluginB.beforeEventBroadcast.mock.invocationCallOrder[0]; - expect(pluginACallOrder).toBeLessThan(pluginBCallOrder); + expect(result).toEqual({ messageType: 'EVENT', success: false }); }); - it('should return false if any plugin returns false', async () => { - const pluginA = { - beforeEventBroadcast: jest.fn().mockReturnValue(false), - }; - const pluginB = { - beforeEventBroadcast: jest.fn().mockReturnValue(true), - }; - const event = {} as Event; - - pluginManagerService.register(pluginA); - pluginManagerService.register(pluginB); - - const result = await pluginManagerService.callBeforeEventBroadcastHooks( - ctx, - event, - ); - - expect(result).toBe(false); - expect(pluginA.beforeEventBroadcast).toHaveBeenCalledWith(ctx, event); - expect(pluginB.beforeEventBroadcast).not.toHaveBeenCalled(); + it('should throw error if next() called multiple times', async () => { + pluginManagerService.register({ + handleMessage: async (_ctx, _message, next) => { + await next(); + await next(); + }, + }); + + await expect( + pluginManagerService.handleMessage( + ctx, + {} as IncomingMessage, + async () => {}, + ), + ).rejects.toThrow('next() called multiple times'); }); }); - describe('callAfterEventBroadcastHooks', () => { - it('should run after event broadcast plugins', async () => { - const pluginA = { - afterEventBroadcast: jest.fn(), - }; - const pluginB = { - afterEventBroadcast: jest.fn(), - }; - const event = {} as Event; - - pluginManagerService.register(pluginA); - pluginManagerService.register(pluginB); - - await pluginManagerService.callAfterEventBroadcastHooks(ctx, event); + describe('broadcast', () => { + it('should call middlewares in order', async () => { + const arr: number[] = []; + pluginManagerService.register( + { + broadcast: async (_ctx, _message, next) => { + arr.push(1); + await next(); + arr.push(5); + }, + }, + { + broadcast: async (_ctx, _message, next) => { + arr.push(2); + await next(); + arr.push(4); + }, + }, + ); - expect(pluginB.afterEventBroadcast).toHaveBeenCalledWith(ctx, event); - expect(pluginA.afterEventBroadcast).toHaveBeenCalledWith(ctx, event); + await pluginManagerService.broadcast(ctx, {} as Event, async () => { + arr.push(3); + }); - const pluginBCallOrder = - pluginB.afterEventBroadcast.mock.invocationCallOrder[0]; - const pluginACallOrder = - pluginA.afterEventBroadcast.mock.invocationCallOrder[0]; - expect(pluginBCallOrder).toBeLessThan(pluginACallOrder); + expect(arr).toEqual([1, 2, 3, 4, 5]); }); }); }); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 1491b97e..b4c2f8da 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,3 +1,2 @@ -export * from './interfaces'; export * from './nostr-relay'; export * from './utils/response'; diff --git a/packages/core/src/interfaces/index.ts b/packages/core/src/interfaces/index.ts deleted file mode 100644 index 896a1193..00000000 --- a/packages/core/src/interfaces/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './handle-result.interface'; diff --git a/packages/core/src/nostr-relay.ts b/packages/core/src/nostr-relay.ts index 19939a6a..278c9a24 100644 --- a/packages/core/src/nostr-relay.ts +++ b/packages/core/src/nostr-relay.ts @@ -3,26 +3,24 @@ import { ClientContext, ConsoleLoggerService, Event, - EventHandleResult, EventId, EventRepository, EventUtils, Filter, FilterUtils, - IncomingMessage, - LogLevel, - MessageType, - NostrRelayPlugin, - SubscriptionId, -} from '@nostr-relay/common'; -import { HandleAuthMessageResult, HandleCloseMessageResult, HandleEventMessageResult, + HandleEventResult, HandleMessageResult, HandleReqMessageResult, + IncomingMessage, + LogLevel, + MessageType, NostrRelayOptions, -} from './interfaces'; + NostrRelayPlugin, + SubscriptionId, +} from '@nostr-relay/common'; import { EventService } from './services/event.service'; import { PluginManagerService } from './services/plugin-manager.service'; import { SubscriptionService } from './services/subscription.service'; @@ -41,7 +39,7 @@ export class NostrRelay { private readonly eventService: EventService; private readonly subscriptionService: SubscriptionService; private readonly eventHandlingLazyCache: - | LazyCache> + | LazyCache> | undefined; private readonly domain?: string; private readonly pluginManagerService: PluginManagerService; @@ -140,6 +138,19 @@ export class NostrRelay { client: Client, message: IncomingMessage, ): Promise { + const ctx = this.getClientContext(client); + return await this.pluginManagerService.handleMessage( + ctx, + message, + this._handleMessage.bind(this), + ); + } + + private async _handleMessage( + ctx: ClientContext, + message: IncomingMessage, + ): Promise { + const client = ctx.client; if (message[0] === MessageType.EVENT) { const [, event] = message; const result = await this.handleEventMessage(client, event); @@ -176,7 +187,6 @@ export class NostrRelay { ...result, }; } - const ctx = this.getClientContext(client); ctx.sendMessage( createOutgoingNoticeMessage('invalid: unknown message type'), ); @@ -193,22 +203,8 @@ export class NostrRelay { event: Event, ): Promise { const ctx = this.getClientContext(client); - const callback = async (): Promise => { - const hookResult = - await this.pluginManagerService.callBeforeEventHandleHooks(ctx, event); - if (!hookResult.canContinue) { - return hookResult.result; - } - - const handleResult = await this.eventService.handleEvent(ctx, event); - - await this.pluginManagerService.callAfterEventHandleHooks( - ctx, - event, - handleResult, - ); - - return handleResult; + const callback = (): Promise => { + return this.eventService.handleEvent(ctx, event); }; const handleResult = this.eventHandlingLazyCache diff --git a/packages/core/src/services/event.service.ts b/packages/core/src/services/event.service.ts index 194f8165..8c15b282 100644 --- a/packages/core/src/services/event.service.ts +++ b/packages/core/src/services/event.service.ts @@ -1,12 +1,12 @@ import { ClientContext, Event, - EventHandleResult, EventKind, EventRepository, EventType, EventUtils, Filter, + HandleEventResult, Logger, } from '@nostr-relay/common'; import { LazyCache } from '../utils'; @@ -66,7 +66,7 @@ export class EventService { async handleEvent( ctx: ClientContext, event: Event, - ): Promise { + ): Promise { if (event.kind === EventKind.AUTHENTICATION) { return { success: true, noReplyNeeded: true }; } @@ -138,7 +138,7 @@ export class EventService { private async handleEphemeralEvent( ctx: ClientContext, event: Event, - ): Promise { + ): Promise { await this.broadcast(ctx, event); return { noReplyNeeded: true, success: true }; } @@ -146,7 +146,7 @@ export class EventService { private async handleRegularEvent( ctx: ClientContext, event: Event, - ): Promise { + ): Promise { const { isDuplicate } = await this.eventRepository.upsert(event); if (!isDuplicate) { @@ -166,13 +166,9 @@ export class EventService { } private async broadcast(ctx: ClientContext, event: Event): Promise { - const hookResult = - await this.pluginManagerService.callBeforeEventBroadcastHooks(ctx, event); - if (!hookResult.canContinue) return; - - await this.subscriptionService.broadcast(event); - - await this.pluginManagerService.callAfterEventBroadcastHooks(ctx, event); + return this.pluginManagerService.broadcast(ctx, event, (_, e) => + this.subscriptionService.broadcast(e), + ); } private mergeSortedEventArrays(arrays: Event[][]): Event[] { diff --git a/packages/core/src/services/plugin-manager.service.ts b/packages/core/src/services/plugin-manager.service.ts index 835efa94..1864331b 100644 --- a/packages/core/src/services/plugin-manager.service.ts +++ b/packages/core/src/services/plugin-manager.service.ts @@ -1,106 +1,93 @@ import { - AfterEventBroadcast, - AfterEventHandle, - BeforeEventBroadcast, - BeforeEventHandle, - BeforeEventHandleResult, - BeforeHookResult, + BroadcastMiddleware, ClientContext, Event, - EventHandleResult, + HandleMessageMiddleware, + HandleMessageResult, + IncomingMessage, + KeysOfUnion, NostrRelayPlugin, } from '@nostr-relay/common'; export class PluginManagerService { - private readonly beforeEventHandlePlugins: BeforeEventHandle[] = []; - private readonly afterEventHandlePlugins: AfterEventHandle[] = []; - private readonly beforeEventBroadcastPlugins: BeforeEventBroadcast[] = []; - private readonly afterEventBroadcastPlugins: AfterEventBroadcast[] = []; + private readonly handleMessageMiddlewares: HandleMessageMiddleware[] = []; + private readonly broadcastMiddlewares: BroadcastMiddleware[] = []; - register(plugin: NostrRelayPlugin): void { - if (this.hasBeforeEventHandleHook(plugin)) { - this.beforeEventHandlePlugins.push(plugin); - } - if (this.hasAfterEventHandleHook(plugin)) { - this.afterEventHandlePlugins.unshift(plugin); - } - if (this.hasBeforeEventBroadcastHook(plugin)) { - this.beforeEventBroadcastPlugins.push(plugin); - } - if (this.hasAfterEventBroadcastHook(plugin)) { - this.afterEventBroadcastPlugins.unshift(plugin); - } - } - - async callBeforeEventHandleHooks( - ctx: ClientContext, - event: Event, - ): Promise { - for await (const plugin of this.beforeEventHandlePlugins) { - const result = await plugin.beforeEventHandle(ctx, event); - if (!result.canContinue) return result; - } - return { canContinue: true }; + register(...plugins: NostrRelayPlugin[]): PluginManagerService { + plugins.forEach(plugin => { + if (this.hasHandleMessageMiddleware(plugin)) { + this.handleMessageMiddlewares.push(plugin); + } + if (this.hasBroadcastMiddleware(plugin)) { + this.broadcastMiddlewares.push(plugin); + } + }); + return this; } - async callAfterEventHandleHooks( + async handleMessage( ctx: ClientContext, - event: Event, - handleResult: EventHandleResult, - ): Promise { - for await (const plugin of this.afterEventHandlePlugins) { - await plugin.afterEventHandle(ctx, event, handleResult); - } - } - - async callBeforeEventBroadcastHooks( - ctx: ClientContext, - event: Event, - ): Promise { - for await (const plugin of this.beforeEventBroadcastPlugins) { - const result = await plugin.beforeEventBroadcast(ctx, event); - if (!result.canContinue) return result; - } - return { canContinue: true }; + message: IncomingMessage, + next: ( + ctx: ClientContext, + message: IncomingMessage, + ) => Promise, + ): Promise { + return this.compose( + this.handleMessageMiddlewares, + 'handleMessage', + next, + ctx, + message, + ); } - async callAfterEventBroadcastHooks( + async broadcast( ctx: ClientContext, event: Event, + next: (ctx: ClientContext, event: Event) => Promise, ): Promise { - for await (const plugin of this.afterEventBroadcastPlugins) { - await plugin.afterEventBroadcast(ctx, event); - } - } - - private hasBeforeEventHandleHook( - plugin: NostrRelayPlugin, - ): plugin is BeforeEventHandle { - return ( - typeof (plugin as BeforeEventHandle).beforeEventHandle === 'function' + return this.compose( + this.broadcastMiddlewares, + 'broadcast', + next, + ctx, + event, ); } - private hasAfterEventHandleHook( - plugin: NostrRelayPlugin, - ): plugin is AfterEventHandle { - return typeof (plugin as AfterEventHandle).afterEventHandle === 'function'; + private compose( + plugins: NostrRelayPlugin[], + funcName: KeysOfUnion, + next: (...args: any[]) => Promise, + ...args: any[] + ): Promise { + let index = -1; + async function dispatch(i: number): Promise { + if (i <= index) { + throw new Error('next() called multiple times'); + } + index = i; + const middleware = plugins[i]?.[funcName]; + if (!middleware) { + return next(...args); + } + return middleware(...args, dispatch.bind(null, i + 1)); + } + return dispatch(0); } - private hasBeforeEventBroadcastHook( + private hasHandleMessageMiddleware( plugin: NostrRelayPlugin, - ): plugin is BeforeEventBroadcast { + ): plugin is HandleMessageMiddleware { return ( - typeof (plugin as BeforeEventBroadcast).beforeEventBroadcast === - 'function' + typeof (plugin as HandleMessageMiddleware).handleMessage === 'function' ); } - private hasAfterEventBroadcastHook( + private hasBroadcastMiddleware( plugin: NostrRelayPlugin, - ): plugin is AfterEventBroadcast { - return ( - typeof (plugin as AfterEventBroadcast).afterEventBroadcast === 'function' - ); + ): plugin is BroadcastMiddleware { + return typeof (plugin as BroadcastMiddleware).broadcast === 'function'; } }