From 70d907e0de7b6c55ccc4971468956ce75b11ee47 Mon Sep 17 00:00:00 2001 From: hasundue Date: Sun, 17 Mar 2024 11:23:58 +0900 Subject: [PATCH] refactor: thin core --- core/clients.ts | 41 +++------ core/nodes.ts | 137 +++++++++---------------------- core/nodes_test.ts | 23 +----- core/protocol.ts | 53 +++--------- core/relays.ts | 96 +++++++++++++--------- core/relays_test.ts | 17 ---- deno.json | 1 + lib/deno.json | 3 +- lib/types.ts | 4 + {core => lib}/websockets.ts | 0 {core => lib}/websockets_test.ts | 0 nips/01/clients.ts | 8 +- nips/01/clients_test.ts | 7 +- nips/01/protocol.ts | 11 ++- nips/01/relays.ts | 9 +- nips/01/relays_test.ts | 11 ++- nips/02/protocol.ts | 6 +- nips/07/protocol.ts | 3 +- nips/42/protocol.ts | 6 +- nips/42/relays.ts | 5 +- nips/_common.ts | 9 -- nips/clients.ts | 18 +--- nips/deno.json | 4 +- nips/nodes.ts | 69 ++++++++++++++++ nips/protocol.ts | 8 +- nips/relays.ts | 18 +--- 26 files changed, 253 insertions(+), 314 deletions(-) rename {core => lib}/websockets.ts (100%) rename {core => lib}/websockets_test.ts (100%) delete mode 100644 nips/_common.ts create mode 100644 nips/nodes.ts diff --git a/core/clients.ts b/core/clients.ts index 2a04ce0..e84fe0d 100644 --- a/core/clients.ts +++ b/core/clients.ts @@ -4,30 +4,24 @@ import type { RelayToClientMessage, SubscriptionId, } from "./protocol.ts"; -import { ClientEventTypeRecord, NostrNode, NostrNodeConfig } from "./nodes.ts"; -import { NIP } from "@lophus/core/protocol"; +import { Node, NodeConfig } from "./nodes.ts"; -//------------- -// Interfaces -//------------- -export interface ClientConfig - extends NostrNodeConfig { - modules: ClientModule[]; -} +export type ClientConfig = NodeConfig; +export type ClientOptions = Partial; -export type ClientOptions = Partial>; +export interface ClientEventTypeRecord { + message: ClientToRelayMessage; +} /** * A class that represents a remote Nostr client. */ -export class Client extends NostrNode< - N, +export class Client extends Node< RelayToClientMessage, ClientEventTypeRecord > { declare ws: WebSocket; - declare config: ClientConfig; - #ready: Promise; + declare config: ClientConfig; /** Writable interface for the subscriptions. */ readonly subscriptions: Map< @@ -35,31 +29,16 @@ export class Client extends NostrNode< WritableStream > = new Map(); - constructor(ws: WebSocket, options?: ClientOptions) { + constructor(ws: WebSocket, options?: ClientOptions) { super(ws, options); this.config = { ...this.config, - modules: [], ...options, }; - this.ws.addEventListener("message", async (ev: MessageEvent) => { + this.ws.addEventListener("message", (ev: MessageEvent) => { const message = JSON.parse(ev.data) as ClientToRelayMessage; // TODO: Validate the message. - await this.#ready; this.dispatch("message", message); }); - this.#ready = Promise.all( - this.config.modules.map((mod) => mod(this)), - ); } } - -//---------- -// Modules -//---------- -export interface ClientModule< - P extends NIP = never, - D extends NIP = never, -> { - (client: Client

): void | Promise; -} diff --git a/core/nodes.ts b/core/nodes.ts index 732550c..c01b2f6 100644 --- a/core/nodes.ts +++ b/core/nodes.ts @@ -1,49 +1,32 @@ -import type { PromiseCallbackRecord } from "@lophus/lib/types"; -import type { - ClientToRelayMessage, - EventId, - NIP, - NostrEvent, - NostrMessage, - RelayToClientMessage, - RelayToClientMessageType, - SubscriptionFilter, - SubscriptionId, -} from "./protocol.ts"; -import type { WebSocketLike } from "./websockets.ts"; - -export interface NostrNodeConfig< - N extends NIP = never, -> { +import type { WebSocketLike } from "@lophus/lib/websockets"; +import type { InterNodeMessage } from "./protocol.ts"; + +export interface NodeConfig { nbuffer: number; - nips: N[]; } -export type NostrNodeOptions< - N extends NIP = never, -> = Partial>; +export type NodeOptions = Partial; /** * Common base class for relays and clients. */ -export class NostrNode< - N extends NIP = never, - M extends NostrMessage = NostrMessage, - R extends EventTypeRecord = EventTypeRecord, +export class Node< + M extends InterNodeMessage = InterNodeMessage, + R = AnyEventTypeRecord, > extends EventTarget { readonly writable: WritableStream; - readonly config: Readonly>; + readonly config: Readonly; constructor( readonly ws: WebSocketLike, - opts: NostrNodeOptions = {}, + options: NodeOptions = {}, ) { super(); this.writable = new WritableStream({ write: (msg) => this.ws.send(JSON.stringify(msg)), close: () => this.ws.close(), }); - this.config = { nbuffer: 10, nips: [], ...opts }; + this.config = { nbuffer: 10, ...options }; } send(msg: M): void | Promise { @@ -67,7 +50,7 @@ export class NostrNode< declare addEventListener: >( type: T, listener: - | NostrNodeEventListenerOrEventListenerObject + | NodeEventListenerOrEventListenerObject | null, options?: AddEventListenerOptions, ) => void; @@ -75,20 +58,20 @@ export class NostrNode< declare removeEventListener: >( type: T, listener: - | NostrNodeEventListenerOrEventListenerObject + | NodeEventListenerOrEventListenerObject | null, options?: boolean | EventListenerOptions, ) => void; declare dispatchEvent: >( - event: NostrNodeEvent, + event: NodeEvent, ) => boolean; dispatch>( type: T, data: R[T], ) { - this.dispatchEvent(new NostrNodeEvent(type, data)); + this.dispatchEvent(new NodeEvent(type, data)); } on>( @@ -100,59 +83,18 @@ export class NostrNode< } } -//------------------------- -// Messages and contexts -//------------------------- - -type SubscriptionMessage = { - [T in RelayToClientMessageType]: RelayToClientMessage[1] extends - SubscriptionId ? RelayToClientMessage - : never; -}[RelayToClientMessageType]; - -type PublicationMessage = { - [T in RelayToClientMessageType]: RelayToClientMessage[1] extends EventId - ? RelayToClientMessage - : never; -}[RelayToClientMessageType]; - -export interface SubscriptionContext { - id: SubscriptionId; - filters: SubscriptionFilter[]; - realtime: boolean; -} - -export interface PublicationContext extends PromiseCallbackRecord { - event: NostrEvent; -} - // ------------------------------ // Events // ------------------------------ -type EventTypeRecord = RelayEventTypeRecord | ClientEventTypeRecord; - -export interface RelayEventTypeRecord { - message: RelayToClientMessage; - subscribe: SubscriptionContext & { - controller: ReadableStreamDefaultController; - }; - resubscribe: SubscriptionContext; - unsubscribe: SubscriptionContext & { reason: unknown }; - publish: PublicationContext; - [id: SubscriptionId]: SubscriptionMessage; - [id: EventId]: PublicationMessage; -} - -export interface ClientEventTypeRecord { - message: ClientToRelayMessage; -} +// deno-lint-ignore no-explicit-any +type AnyEventTypeRecord = any; -type EventType = keyof R & string; +type EventType = keyof R & string; -export class NostrNodeEvent< - R extends EventTypeRecord, - T extends EventType, +export class NodeEvent< + R = AnyEventTypeRecord, + T extends EventType = EventType, > extends MessageEvent { declare type: T; constructor(type: T, data: R[T]) { @@ -160,29 +102,26 @@ export class NostrNodeEvent< } } -type NostrNodeEventListenerOrEventListenerObject< - N extends NIP, - M extends NostrMessage, - R extends EventTypeRecord, - T extends EventType, +type NodeEventListenerOrEventListenerObject< + M extends InterNodeMessage, + R = AnyEventTypeRecord, + T extends EventType = EventType, > = - | NostrNodeEventListener - | NostrNodeEventListenerObject; - -type NostrNodeEventListener< - N extends NIP, - W extends NostrMessage, - R extends EventTypeRecord, - T extends EventType, + | NodeEventListener + | NodeEventListenerObject; + +type NodeEventListener< + W extends InterNodeMessage, + R = AnyEventTypeRecord, + T extends EventType = EventType, > // deno-lint-ignore no-explicit-any - = (this: NostrNode, ev: MessageEvent) => any; + = (this: Node, ev: MessageEvent) => any; -type NostrNodeEventListenerObject< - N extends NIP, - W extends NostrMessage, - R extends EventTypeRecord, - T extends EventType, +type NodeEventListenerObject< + W extends InterNodeMessage, + R = AnyEventTypeRecord, + T extends EventType = EventType, > = { // deno-lint-ignore no-explicit-any - handleEvent(this: NostrNode, ev: MessageEvent): any; + handleEvent(this: Node, ev: MessageEvent): any; }; diff --git a/core/nodes_test.ts b/core/nodes_test.ts index 86aa0d5..f1997a4 100644 --- a/core/nodes_test.ts +++ b/core/nodes_test.ts @@ -1,28 +1,11 @@ import { assertEquals } from "@std/assert"; import { describe, it } from "@std/testing/bdd"; -import { assertType, Has } from "@std/testing/types"; import { MockWebSocket } from "@lophus/lib/testing"; -import "@lophus/nips"; -import { NIP } from "./protocol.ts"; -import { NostrNode } from "./nodes.ts"; - -type HasNIP = T extends NostrNode ? Has : false; +import { Node } from "./nodes.ts"; describe("NostrNode", () => { - describe("constructor", () => { - it("should not have any NIPs configured by default", () => { - const node = new NostrNode(new MockWebSocket()); - assertType>(false); - }); - - it("should inherit the type of `nips`", () => { - const node = new NostrNode(new MockWebSocket(), { nips: [1] }); - assertType>(true); - }); - }); - describe("writable", () => { - const node = new NostrNode(new MockWebSocket()); + const node = new Node(new MockWebSocket()); let writer: WritableStreamDefaultWriter; it("should open the WebSocket connection", async () => { @@ -34,7 +17,7 @@ describe("NostrNode", () => { }); describe("close", () => { - const node = new NostrNode(new MockWebSocket()); + const node = new Node(new MockWebSocket()); it("should close the WebSocket connection", async () => { await node.close(); diff --git a/core/protocol.ts b/core/protocol.ts index 622e591..50ce9e5 100644 --- a/core/protocol.ts +++ b/core/protocol.ts @@ -10,24 +10,6 @@ import type { AlphabetLetter, Brand, Stringified } from "@lophus/lib/types"; -// ---------------------- -// Extendable interfaces -// ---------------------- - -export interface NipRecord {} - -export interface EventKindRecord {} - -export interface TagRecord {} - -export interface ClientToRelayMessageRecord< - K extends EventKind = EventKind, -> {} - -export interface RelayToClientMessageRecord< - K extends EventKind = EventKind, -> {} - // ---------------------- // Events and signatures // ---------------------- @@ -75,6 +57,8 @@ export interface EventInit { // Tags // ---------------------- +export interface TagRecord {} + export type TagType = keyof TagRecord & string; export type TagParam = string | undefined; @@ -105,7 +89,9 @@ export type OptionalTag = EventKindRecord[K] extends export type RelayUrl = `wss://${string}` | `ws://${string}`; export type SubscriptionId = Brand; -export type NostrMessage = ClientToRelayMessage | RelayToClientMessage; +export interface ClientToRelayMessageRecord< + K extends EventKind = EventKind, +> {} export type ClientToRelayMessage< T extends ClientToRelayMessageType = ClientToRelayMessageType, @@ -115,6 +101,10 @@ export type ClientToRelayMessage< }[T]; export type ClientToRelayMessageType = keyof ClientToRelayMessageRecord; +export interface RelayToClientMessageRecord< + K extends EventKind = EventKind, +> {} + export type RelayToClientMessage< T extends RelayToClientMessageType = RelayToClientMessageType, K extends EventKind = EventKind, @@ -152,10 +142,14 @@ export type SubscriptionFilter< limit?: number; }; +export type InterNodeMessage = ClientToRelayMessage | RelayToClientMessage; + // ---------------------- // Events // ---------------------- +export interface EventKindRecord {} + export type EventKind = keyof EventKindRecord & number; export interface EventKindRecordEntry { @@ -173,24 +167,3 @@ export type EventContent = EventKindRecord[K] extends export type ResponsePrefix = EventKindRecord[K] extends { ResponsePrefix: infer P extends string } ? P : DefaultResponsePrefix; - -export type RegularEventKind = Brand; -export type ReplaceableEventKind = Brand; -export type EphemeralEventKind = Brand; -export type ParameterizedReplaceableEventKind = Brand< - EventKind, - "ParameterizedReplaceable" ->; - -// ---------------------- -// NIPs -// ---------------------- - -export type NIP = keyof NipRecord & number; - -export interface NipRecordEntry { - ClientToRelayMessage: ClientToRelayMessageType; - RelayToClientMessage: RelayToClientMessageType; - EventKind: EventKind; - Tag: TagType; -} diff --git a/core/relays.ts b/core/relays.ts index 733ba70..2556f70 100644 --- a/core/relays.ts +++ b/core/relays.ts @@ -1,19 +1,22 @@ +import type { PromiseCallbackRecord } from "@lophus/lib/types"; +import { LazyWebSocket } from "@lophus/lib/websockets"; import type { ClientToRelayMessage, + EventId, EventKind, - NIP, NostrEvent, RelayToClientMessage, + RelayToClientMessageType, RelayUrl, SubscriptionFilter, SubscriptionId, } from "./protocol.ts"; -import { LazyWebSocket } from "./websockets.ts"; -import { NostrNode, NostrNodeConfig, RelayEventTypeRecord } from "./nodes.ts"; +import { Node, NodeConfig } from "./nodes.ts"; //--------- // Errors //--------- + export class ConnectionClosed extends Error {} export class EventRejected extends Error {} export class SubscriptionClosed extends Error {} @@ -21,19 +24,15 @@ export class SubscriptionClosed extends Error {} //------------- // Interfaces //------------- -export interface RelayConfig extends NostrNodeConfig { + +export interface RelayConfig extends NodeConfig { name: string; - modules: RelayModule[]; read: boolean; url: RelayUrl; write: boolean; } -export type RelayOptions = Partial>; - -export interface RelayInit extends RelayOptions { - url: RelayUrl; -} +export type RelayOptions = Partial; export interface SubscriptionOptions { id?: string; @@ -41,42 +40,76 @@ export interface SubscriptionOptions { realtime?: boolean; } +//------------------------ +// Messages and contexts +//------------------------ + +export interface SubscriptionContext { + id: SubscriptionId; + filters: SubscriptionFilter[]; + realtime: boolean; +} + +export interface PublicationContext extends PromiseCallbackRecord { + event: NostrEvent; +} + +type SubscriptionMessage = { + [T in RelayToClientMessageType]: RelayToClientMessage[1] extends + SubscriptionId ? RelayToClientMessage + : never; +}[RelayToClientMessageType]; + +type PublicationMessage = { + [T in RelayToClientMessageType]: RelayToClientMessage[1] extends EventId + ? RelayToClientMessage + : never; +}[RelayToClientMessageType]; + +//--------- +// Events +//--------- + +export interface RelayEventTypeRecord { + message: RelayToClientMessage; + subscribe: SubscriptionContext & { + controller: ReadableStreamDefaultController; + }; + resubscribe: SubscriptionContext; + unsubscribe: SubscriptionContext & { reason: unknown }; + publish: PublicationContext; + [id: SubscriptionId]: SubscriptionMessage; + [id: EventId]: PublicationMessage; +} + /** * A class that represents a remote Nostr Relay. */ -export class Relay extends NostrNode< - N, +export class Relay extends Node< ClientToRelayMessage, RelayEventTypeRecord > { declare ws: LazyWebSocket; - declare config: RelayConfig; - #ready: Promise; + declare config: RelayConfig; constructor( - init: RelayUrl | RelayInit, - options?: RelayOptions, + url: RelayUrl, + options?: RelayOptions, ) { - const url = typeof init === "string" ? init : init.url; super(new LazyWebSocket(url), options); this.config = { ...this.config, - modules: [], name: new URL(url).hostname, read: true, url, write: true, ...options, }; - this.ws.addEventListener("message", async (ev: MessageEvent) => { + this.ws.addEventListener("message", (ev: MessageEvent) => { const message = JSON.parse(ev.data) as RelayToClientMessage; // TODO: Validate the message. - await this.#ready; this.dispatch("message", message); }); - this.#ready = Promise.all( - this.config.modules.map((mod) => mod(this)), - ); } subscribe( @@ -91,8 +124,7 @@ export class Relay extends NostrNode< const resubscribe = () => this.dispatch("resubscribe", context); return new ReadableStream>( { - start: async (controller) => { - await this.#ready; + start: (controller) => { this.addEventListener( context.id, () => this.ws.removeEventListener("close", resubscribe), @@ -117,8 +149,7 @@ export class Relay extends NostrNode< * @throws {EventRejected} If the event is rejected by the relay * @throws {ConnectionClosed} If the WebSocket connection to the relay is closed */ - async publish(event: NostrEvent): Promise { - await this.#ready; + publish(event: NostrEvent): Promise { return new Promise((resolve, reject) => { this.dispatch("publish", { event, resolve, reject }); this.ws.addEventListener( @@ -133,6 +164,7 @@ export class Relay extends NostrNode< //------------- // RelayLikes //------------- + export interface RelayLike extends Pick { readonly config: RelayLikeConfig; @@ -140,13 +172,3 @@ export interface RelayLike export type RelayLikeConfig = Pick; export type RelayLikeOptions = Partial; - -//---------- -// Modules -//---------- -export interface RelayModule< - P extends NIP = never, // The NIP that the module provides - D extends NIP = never, // The NIPs that the module depends on -> { - (relay: Relay

): void | Promise; -} diff --git a/core/relays_test.ts b/core/relays_test.ts index ba01444..a7d334d 100644 --- a/core/relays_test.ts +++ b/core/relays_test.ts @@ -1,27 +1,10 @@ import { assert, assertEquals, assertObjectMatch } from "@std/assert"; import { describe, it } from "@std/testing/bdd"; -import { assertType, Has } from "@std/testing/types"; -import "@lophus/nips"; -import { NIP } from "./protocol.ts"; import { Relay } from "./relays.ts"; -type HasNIP = T extends Relay ? Has : false; - describe("Relay", () => { const url = "wss://localhost:8080"; - describe("constructor", () => { - it("should not configure NIPs by default", () => { - const relay = new Relay(url); - assertType>(false); - }); - - it("should inherit the type of `nips` option", () => { - const relay = new Relay(url, { nips: [1] }); - assertType>(true); - }); - }); - describe("constructed with url only", () => { const relay = new Relay(url); diff --git a/deno.json b/deno.json index 8d4dad2..332f7eb 100644 --- a/deno.json +++ b/deno.json @@ -8,6 +8,7 @@ "@lophus/lib/types": "./lib/types.ts", "@lophus/lib/testing": "./lib/testing.ts", "@lophus/lib/streams": "./lib/streams.ts", + "@lophus/lib/websockets": "./lib/websockets.ts", "@lophus/core": "./core/mod.ts", "@lophus/core/clients": "./core/clients.ts", "@lophus/core/nodes": "./core/nodes.ts", diff --git a/lib/deno.json b/lib/deno.json index 039bc04..7fa4468 100644 --- a/lib/deno.json +++ b/lib/deno.json @@ -4,7 +4,8 @@ "exports": { "./streams": "./streams.ts", "./testing": "./testing.ts", - "./types": "./types.ts" + "./types": "./types.ts", + "./websockets": "./websockets.ts" }, "publish": { "exclude": ["*_test.ts"] diff --git a/lib/types.ts b/lib/types.ts index 0936c4a..66da97f 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,11 +1,13 @@ // ---------------------- // Branded types // ---------------------- + export type Brand = T & { __brand: B }; // ---------------------- // Promises // ---------------------- + export interface PromiseCallbackRecord { resolve: (value: T | PromiseLike) => void; reject: (reason?: unknown) => void; @@ -14,6 +16,7 @@ export interface PromiseCallbackRecord { // ---------------------- // Strings // ---------------------- + export type Url = `https://${string}` | `http://${string}`; export type Stringified = string & { __content: T }; @@ -31,6 +34,7 @@ export type AlphabetLetter = // ---------------------- // Records and maps // ---------------------- + export type Expand = T extends infer O ? { [K in keyof O]: O[K] } : never; export type Require = Expand< diff --git a/core/websockets.ts b/lib/websockets.ts similarity index 100% rename from core/websockets.ts rename to lib/websockets.ts diff --git a/core/websockets_test.ts b/lib/websockets_test.ts similarity index 100% rename from core/websockets_test.ts rename to lib/websockets_test.ts diff --git a/nips/01/clients.ts b/nips/01/clients.ts index 4117ed7..a4684a2 100644 --- a/nips/01/clients.ts +++ b/nips/01/clients.ts @@ -1,11 +1,11 @@ import type { NostrEvent } from "@lophus/core/protocol"; -import { ClientModule } from "@lophus/core/clients"; +import { Client } from "@lophus/core/clients"; +import { NIPModule } from "../nodes.ts"; interface EventValidationContext { data: NostrEvent; resolve: (value: unknown) => void; - // deno-lint-ignore no-explicit-any - reject: (reason?: any) => void; + reject: (reason?: unknown) => void; } declare module "@lophus/core/clients" { @@ -14,7 +14,7 @@ declare module "@lophus/core/clients" { } } -const M: ClientModule<1> = (client) => { +const M: NIPModule = (client) => { client.on("message", (message) => { switch (message[0]) { case "EVENT": { diff --git a/nips/01/clients_test.ts b/nips/01/clients_test.ts index 0758f90..f57de7a 100644 --- a/nips/01/clients_test.ts +++ b/nips/01/clients_test.ts @@ -7,18 +7,17 @@ import { RelayToClientMessage, SubscriptionId, } from "@lophus/core/protocol"; -import { Client } from "@lophus/core/clients"; -import mod from "./clients.ts"; +import { Client } from "../clients.ts"; describe("Client (NIP-01)", () => { let ws: MockWebSocket; - let client: Client<1>; + let client: Client; let subid: SubscriptionId; let sub: WritableStream; beforeAll(() => { ws = new MockWebSocket(); - client = new Client(ws, { modules: [mod] }); + client = new Client(ws); }); afterAll(() => { client.close(); diff --git a/nips/01/protocol.ts b/nips/01/protocol.ts index ee3a687..f91aaf5 100644 --- a/nips/01/protocol.ts +++ b/nips/01/protocol.ts @@ -7,10 +7,11 @@ * @module */ -import "@lophus/core/protocol"; import type { Url } from "@lophus/lib/types"; +import "@lophus/core/protocol"; +import "../protocol.ts"; -declare module "@lophus/core/protocol" { +declare module "../protocol.ts" { interface NipRecord { 1: { ClientToRelayMessage: "EVENT" | "REQ" | "CLOSE"; @@ -19,6 +20,9 @@ declare module "@lophus/core/protocol" { Tag: "e" | "p" | "a" | "d"; }; } +} + +declare module "@lophus/core/protocol" { interface EventKindRecord { 0: { OptionalTag: Tag; @@ -33,6 +37,7 @@ declare module "@lophus/core/protocol" { Content: string; }; } + interface TagRecord { /** Event ID */ "e": [EventId, RelayUrl?]; @@ -45,6 +50,7 @@ declare module "@lophus/core/protocol" { /** Identifier */ "d": [string]; } + interface ClientToRelayMessageRecord< K extends EventKind = EventKind, > { @@ -52,6 +58,7 @@ declare module "@lophus/core/protocol" { REQ: [SubscriptionId, ...SubscriptionFilter[]]; CLOSE: [SubscriptionId]; } + interface RelayToClientMessageRecord< K extends EventKind = EventKind, > { diff --git a/nips/01/relays.ts b/nips/01/relays.ts index 74e6a06..16eddbb 100644 --- a/nips/01/relays.ts +++ b/nips/01/relays.ts @@ -1,10 +1,7 @@ -import { - EventRejected, - RelayModule, - SubscriptionClosed, -} from "@lophus/core/relays"; +import { EventRejected, Relay, SubscriptionClosed } from "@lophus/core/relays"; +import { NIPModule } from "../nodes.ts"; -export const M: RelayModule<1> = (relay) => { +export const M: NIPModule = (relay) => { relay.on("message", (message) => { switch (message[0]) { case "EVENT": diff --git a/nips/01/relays_test.ts b/nips/01/relays_test.ts index 6843715..f905961 100644 --- a/nips/01/relays_test.ts +++ b/nips/01/relays_test.ts @@ -11,10 +11,9 @@ import type { import { ConnectionClosed, EventRejected, - Relay, SubscriptionClosed, } from "@lophus/core/relays"; -import mod from "./relays.ts"; +import { Relay } from "../relays.ts"; function getRemoteSocket() { return MockWebSocket.instances[0].remote; @@ -22,13 +21,13 @@ function getRemoteSocket() { describe("Relay (NIP-01)", () => { const url = "wss://localhost:8080"; - let relay: Relay<1>; + let relay: Relay; let sub_0: ReadableStream>; let sub_1: ReadableStream>; beforeAll(() => { globalThis.WebSocket = MockWebSocket; - relay = new Relay(url, { modules: [mod] }); + relay = new Relay(url); }); afterAll(() => { @@ -37,8 +36,8 @@ describe("Relay (NIP-01)", () => { } }); - it("should have loaded NIP-01 module", () => { - assertEquals(relay.config.modules.length, 1); + it("should not have any nips configured", () => { + assertEquals(relay.config.nips.length, 0); }); it("should create a subscription", () => { diff --git a/nips/02/protocol.ts b/nips/02/protocol.ts index 63abd2d..4861dec 100644 --- a/nips/02/protocol.ts +++ b/nips/02/protocol.ts @@ -1,11 +1,15 @@ import "@lophus/core/protocol"; +import "../protocol.ts"; -declare module "@lophus/core/protocol" { +declare module "../protocol.ts" { interface NipRecord { 2: { Tag: "p"; }; } +} + +declare module "@lophus/core/protocol" { interface EventKindRecord { 3: { Tags: ContactTag[]; diff --git a/nips/07/protocol.ts b/nips/07/protocol.ts index 80f99fe..2063dda 100644 --- a/nips/07/protocol.ts +++ b/nips/07/protocol.ts @@ -4,8 +4,9 @@ import { PublicKey, UnsignedEvent, } from "@lophus/core/protocol"; +import "../protocol.ts"; -declare module "@lophus/core/protocol" { +declare module "../protocol.ts" { interface NipRecord { 7: Record; } diff --git a/nips/42/protocol.ts b/nips/42/protocol.ts index a6d8b41..56653b9 100644 --- a/nips/42/protocol.ts +++ b/nips/42/protocol.ts @@ -1,6 +1,7 @@ import "@lophus/core/protocol"; +import "../protocol.ts"; -declare module "@lophus/core/protocol" { +declare module "../protocol.ts" { interface NipRecord { 42: { ClientToRelayMessage: "AUTH"; @@ -9,6 +10,9 @@ declare module "@lophus/core/protocol" { Tag: "relay" | "challenge"; }; } +} + +declare module "@lophus/core/protocol" { interface RelayToClientMessageRecord { AUTH: [challenge: string]; } diff --git a/nips/42/relays.ts b/nips/42/relays.ts index 270fb5f..9f4da5f 100644 --- a/nips/42/relays.ts +++ b/nips/42/relays.ts @@ -1,7 +1,8 @@ import type { Stringified } from "@lophus/lib/types"; import type { EventInit } from "@lophus/core/protocol"; -import type { RelayModule } from "@lophus/core/relays"; import type { Signer } from "@lophus/std/signs"; +import type { NIPModule } from "../nodes.ts"; +import { Relay } from "@lophus/core/relays"; declare module "@lophus/core/relays" { interface RelayConfig { @@ -9,7 +10,7 @@ declare module "@lophus/core/relays" { } } -const M: RelayModule<42> = (relay) => { +const M: NIPModule = (relay) => { relay.on("message", (message) => { if (message[0] !== "AUTH") { // This NIP only handles AUTH messages diff --git a/nips/_common.ts b/nips/_common.ts deleted file mode 100644 index 3aa04ca..0000000 --- a/nips/_common.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { NIP } from "@lophus/core/protocol"; - -/** - * Convert a NIP to a string. If the NIP is less than 10, a leading zero is - * added. - */ -export function nipToString(nip: NIP | number) { - return nip > 9 ? nip.toString() : "0" + nip.toString(); -} diff --git a/nips/clients.ts b/nips/clients.ts index f68166c..d0f09f8 100644 --- a/nips/clients.ts +++ b/nips/clients.ts @@ -1,15 +1,5 @@ -import { NIP } from "@lophus/core/protocol"; -import { ClientModule } from "@lophus/core/clients"; -import "./protocol.ts"; -import { nipToString } from "./_common.ts"; +import { NIPsEnabled } from "./nodes.ts"; +import { Client as CoreClient } from "@lophus/core/clients"; +import base from "./01/clients.ts"; -const M: ClientModule = async (node) => { - await Promise.all(node.config.nips.map(async (nip) => { - const mod = await import( - new URL(`./${nipToString(nip)}/clients.ts`, import.meta.url).href - ) as ClientModule; - await mod(node); - })); -}; - -export default M; +export class Client extends NIPsEnabled(CoreClient, base, "clients.ts", []) {} diff --git a/nips/deno.json b/nips/deno.json index a890f95..5a2d21d 100644 --- a/nips/deno.json +++ b/nips/deno.json @@ -3,13 +3,15 @@ "version": "0.0.14", "exports": { ".": "./protocol.ts", + "./clients": "./clients.ts", + "./relays": "./relays.ts", "./01": "./01/protocol.ts", "./01/clients": "./01/clients.ts", "./01/relays": "./01/relays.ts", "./02": "./02/protocol.ts", "./07": "./07/protocol.ts", "./42": "./42/protocol.ts", - "./42/clients": "./42/relays.ts", + "./42/clients": "./42/clients.ts", "./42/relays": "./42/relays.ts" }, "imports": { diff --git a/nips/nodes.ts b/nips/nodes.ts new file mode 100644 index 0000000..2fc2d9e --- /dev/null +++ b/nips/nodes.ts @@ -0,0 +1,69 @@ +import { Node, NodeOptions } from "@lophus/core/nodes"; +import { NIP } from "./protocol.ts"; + +interface NodeConstructor { + // deno-lint-ignore no-explicit-any + new (init: any, options: NodeOptions): Node; +} + +// @ts-ignore 2430 FIXME +interface WithNIPs extends T { + config: T["config"] & { nips: N[] }; + readonly ready: Promise; +} + +interface NIPsEnabled { + new ( + init: ConstructorParameters[0], + options?: ConstructorParameters[1] & { nips?: N[] }, + ): WithNIPs, N>; +} + +export interface NIPModule { + (node: WithNIPs, N>): void; +} + +/** Convert a NIP to a string. If the NIP is less than 10, a leading zero is added. */ +function nipToString(nip: NIP | number) { + return nip > 9 ? nip.toString() : "0" + nip.toString(); +} + +export function NIPsEnabled( + Node: T, + base: NIPModule, + moduleBaseName: string, + moduleNIPs: Readonly, +): NIPsEnabled { + // @ts-ignore allow concrete arguments + return class extends Node { + declare config: InstanceType["config"] & { nips: N[] }; + readonly ready: Promise; + + constructor( + readonly init: ConstructorParameters[0], + options: ConstructorParameters[1] & { nips?: N[] } = {}, + ) { + super(init, options); + this.config.nips = options.nips ?? []; + this.ready = Promise.all( + this.config.nips.filter((nip) => moduleNIPs.includes(nip)).map( + async (nip) => { + const mod = await import( + `./${nipToString(nip)}/${moduleBaseName}` + ) as { default: NIPModule }; + mod.default(this); + }, + ), + ).then(); + base(this); + } + + override async dispatch( + type: Parameters["dispatch"]>[0], + data: Parameters["dispatch"]>[1], + ): Promise { + await this.ready; + super.dispatch(type, data); + } + }; +} diff --git a/nips/protocol.ts b/nips/protocol.ts index bbdbf3c..9a71fb1 100644 --- a/nips/protocol.ts +++ b/nips/protocol.ts @@ -1,4 +1,4 @@ -import "./01/protocol.ts"; -import "./02/protocol.ts"; -import "./07/protocol.ts"; -import "./42/protocol.ts"; +// deno-lint-ignore no-empty-interface +export interface NipRecord {} + +export type NIP = keyof NipRecord & number; diff --git a/nips/relays.ts b/nips/relays.ts index 3a0bf53..d847d27 100644 --- a/nips/relays.ts +++ b/nips/relays.ts @@ -1,15 +1,5 @@ -import { NIP } from "@lophus/core/protocol"; -import { RelayModule } from "@lophus/core/relays"; -import "./protocol.ts"; -import { nipToString } from "./_common.ts"; +import { NIPsEnabled } from "./nodes.ts"; +import { Relay as CoreRelay } from "@lophus/core/relays"; +import base from "./01/relays.ts"; -const M: RelayModule = async (node) => { - await Promise.all(node.config.nips.map(async (nip) => { - const mod = await import( - new URL(`./${nipToString(nip)}/relays.ts`, import.meta.url).href - ) as RelayModule; - await mod(node); - })); -}; - -export default M; +export class Relay extends NIPsEnabled(CoreRelay, base, "relays.ts", [42]) {}