Skip to content

Commit

Permalink
build: create nips module
Browse files Browse the repository at this point in the history
WIP: refactor: nip module

refactor: thin core
  • Loading branch information
hasundue committed Mar 18, 2024
1 parent db1ba3b commit 23c7344
Show file tree
Hide file tree
Showing 40 changed files with 509 additions and 588 deletions.
60 changes: 16 additions & 44 deletions core/clients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,69 +4,41 @@ import type {
RelayToClientMessage,
SubscriptionId,
} from "./protocol.ts";
import {
NostrNode,
NostrNodeBase,
NostrNodeConfig,
NostrNodeModule,
} from "./nodes.ts";
import { Node, NodeConfig } from "./nodes.ts";

// ----------------------
// Interfaces
// ----------------------

export type ClientConfig = NostrNodeConfig<
RelayToClientMessage,
ClientEventTypeRecord
>;
export type ClientConfig = NodeConfig;
export type ClientOptions = Partial<ClientConfig>;

export interface ClientEventTypeRecord {
message: ClientToRelayMessage;
}

/**
* A class that represents a remote Nostr client.
*/
export class Client extends NostrNodeBase<
export class Client extends Node<
RelayToClientMessage,
ClientEventTypeRecord
> implements NostrNode<RelayToClientMessage, ClientEventTypeRecord> {
/**
* The WebSocket connection to the client.
*/
> {
declare ws: WebSocket;
declare config: ClientConfig;

/**
* Writable interface for the subscriptions.
*/
/** Writable interface for the subscriptions. */
readonly subscriptions: Map<
SubscriptionId,
WritableStream<NostrEvent>
> = new Map();

constructor(ws: WebSocket, opts?: ClientOptions) {
super(ws, opts);
constructor(ws: WebSocket, options?: ClientOptions) {
super(ws, options);
this.config = {
...this.config,
...options,
};
this.ws.addEventListener("message", (ev: MessageEvent<string>) => {
const message = JSON.parse(ev.data) as ClientToRelayMessage;
// TODO: Validate the message.
this.dispatch("message", message);
});
}
}

// ------------------------------
// Events
// ------------------------------

export interface ClientEventTypeRecord {
"message": ClientToRelayMessage;
}

export type ClientEventType = keyof ClientEventTypeRecord;

// ------------------------------
// Modules
// ------------------------------

export type ClientModule = NostrNodeModule<
RelayToClientMessage,
ClientEventTypeRecord,
Client
>;
6 changes: 6 additions & 0 deletions core/deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,11 @@
"./clients": "./clients.ts",
"./protocol": "./protocol.ts",
"./relays": "./relays.ts"
},
"imports": {
"@lophus/lib": "jsr:@lophus/[email protected]"
},
"publish": {
"exclude": ["*_test.ts"]
}
}
2 changes: 1 addition & 1 deletion core/mod.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export * from "./clients.ts";
export * from "./protocol.ts";
export * from "./relays.ts";
export * from "./clients.ts";
173 changes: 57 additions & 116 deletions core/nodes.ts
Original file line number Diff line number Diff line change
@@ -1,95 +1,35 @@
import type { NostrMessage } from "./protocol.ts";
import { WebSocketLike } from "./websockets.ts";

export interface NostrNodeConfig<
W extends NostrMessage = NostrMessage,
R extends EventTypeRecord = EventTypeRecord,
> {
modules: NostrNodeModule<W, R>[];
import type { WebSocketLike } from "@lophus/lib/websockets";
import type { InterNodeMessage } from "./protocol.ts";

export interface NodeConfig {
nbuffer: number;
}

export type NostrNodeOptions<
W extends NostrMessage = NostrMessage,
R extends EventTypeRecord = EventTypeRecord,
> = Partial<NostrNodeConfig<W, R>>;

/**
* Common interface for relays and clients, which extends `EventTarget`.
*/
export interface NostrNode<
W extends NostrMessage = NostrMessage,
R extends EventTypeRecord = EventTypeRecord,
> {
readonly config: Readonly<NostrNodeConfig<W, R>>;
readonly ws: WebSocketLike;
readonly writable: WritableStream<W>;

status: WebSocketLike["readyState"];
send(msg: W): void | Promise<void>;
close(): Promise<void>;

install(mod: NostrNodeModule<W, R>): void;

addEventListener<T extends EventType<R>>(
type: T,
listener:
| NostrNodeEventListenerOrEventListenerObject<W, R, T>
| null,
options?: AddEventListenerOptions,
): void;

removeEventListener<T extends EventType<R>>(
type: T,
listener:
| NostrNodeEventListenerOrEventListenerObject<W, R, T>
| null,
options?: boolean | EventListenerOptions,
): void;

dispatchEvent<T extends EventType<R>>(event: NostrNodeEvent<R, T>): boolean;

/**
* A convenience method to dispatch a `NostrNodeEvent` with the given `type`
* and `data`.
*/
dispatch<T extends EventType<R>>(type: T, data: R[T]): void;

/**
* A convenience method to add an event listener for the given `type` that
* calls the given `listener` when the event is dispatched.
*/
on<T extends EventType<R>>(
type: T,
// deno-lint-ignore no-explicit-any
listener: (data: R[T]) => any,
): void;
}
export type NodeOptions = Partial<NodeConfig>;

/**
* Common base class for relays and clients.
*/
export class NostrNodeBase<
W extends NostrMessage = NostrMessage,
R extends EventTypeRecord = EventTypeRecord,
> extends EventTarget implements NostrNode<W, R> {
readonly writable: WritableStream<W>;
readonly config: Readonly<NostrNodeConfig<W, R>>;
export class Node<
M extends InterNodeMessage = InterNodeMessage,
R = AnyEventTypeRecord,
> extends EventTarget {
readonly writable: WritableStream<M>;
readonly config: Readonly<NodeConfig>;

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 = { modules: [], nbuffer: 10, ...opts };
this.config.modules.forEach((m) => this.install(m));
this.config = { nbuffer: 10, ...options };
}

send(msg: W): void | Promise<void> {
send(msg: M): void | Promise<void> {
return this.ws.send(JSON.stringify(msg));
}

Expand All @@ -107,19 +47,31 @@ export class NostrNodeBase<
}
}

install(mod: NostrNodeModule<W, R>): void {
mod.install(this);
}
declare addEventListener: <T extends EventType<R>>(
type: T,
listener:
| NodeEventListenerOrEventListenerObject<M, R, T>
| null,
options?: AddEventListenerOptions,
) => void;

declare removeEventListener: <T extends EventType<R>>(
type: T,
listener:
| NodeEventListenerOrEventListenerObject<M, R, T>
| null,
options?: boolean | EventListenerOptions,
) => void;

declare addEventListener: NostrNode<W, R>["addEventListener"];
declare removeEventListener: NostrNode<W, R>["removeEventListener"];
declare dispatchEvent: NostrNode<W, R>["dispatchEvent"];
declare dispatchEvent: <T extends EventType<R>>(
event: NodeEvent<R, T>,
) => boolean;

dispatch<T extends EventType<R>>(
type: T,
data: R[T],
) {
this.dispatchEvent(new NostrNodeEvent<R, T>(type, data));
this.dispatchEvent(new NodeEvent<R, T>(type, data));
}

on<T extends EventType<R>>(
Expand All @@ -131,56 +83,45 @@ export class NostrNodeBase<
}
}

// ------------------------------
// Extensions
// ------------------------------

export interface NostrNodeModule<
W extends NostrMessage = NostrMessage,
R extends EventTypeRecord = EventTypeRecord,
N extends NostrNode<W, R> = NostrNode<W, R>,
> {
// deno-lint-ignore no-explicit-any
install(node: N): any;
}

// ------------------------------
// Events
// ------------------------------

// deno-lint-ignore no-empty-interface
export interface EventTypeRecord {}
// deno-lint-ignore no-explicit-any
type AnyEventTypeRecord = any;

type EventType<R extends EventTypeRecord> = keyof R & string;
type EventType<R = AnyEventTypeRecord> = keyof R & string;

export class NostrNodeEvent<
R extends EventTypeRecord,
T extends EventType<R>,
export class NodeEvent<
R = AnyEventTypeRecord,
T extends EventType<R> = EventType<R>,
> extends MessageEvent<R[T]> {
declare type: T;
constructor(type: T, data: R[T]) {
super(type, { data });
}
}

type NostrNodeEventListenerOrEventListenerObject<
W extends NostrMessage,
R extends EventTypeRecord,
T extends EventType<R>,
> = NostrNodeEventListener<W, R, T> | NostrNodeEventListenerObject<W, R, T>;

type NostrNodeEventListener<
W extends NostrMessage,
R extends EventTypeRecord,
T extends EventType<R>,
type NodeEventListenerOrEventListenerObject<
M extends InterNodeMessage,
R = AnyEventTypeRecord,
T extends EventType<R> = EventType<R>,
> =
| NodeEventListener<M, R, T>
| NodeEventListenerObject<M, R, T>;

type NodeEventListener<
W extends InterNodeMessage,
R = AnyEventTypeRecord,
T extends EventType<R> = EventType<R>,
> // deno-lint-ignore no-explicit-any
= (this: NostrNode<W, R>, ev: MessageEvent<R[T]>) => any;
= (this: Node<W, R>, ev: MessageEvent<R[T]>) => any;

type NostrNodeEventListenerObject<
W extends NostrMessage,
R extends EventTypeRecord,
T extends EventType<R>,
type NodeEventListenerObject<
W extends InterNodeMessage,
R = AnyEventTypeRecord,
T extends EventType<R> = EventType<R>,
> = {
// deno-lint-ignore no-explicit-any
handleEvent(this: NostrNode<W, R>, ev: MessageEvent<R[T]>): any;
handleEvent(this: Node<W, R>, ev: MessageEvent<R[T]>): any;
};
40 changes: 17 additions & 23 deletions core/nodes_test.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,27 @@
import { assertEquals } from "@std/assert";
import { afterAll, beforeAll, describe, it } from "@std/testing/bdd";
import { describe, it } from "@std/testing/bdd";
import { MockWebSocket } from "@lophus/lib/testing";
import { NostrNode, NostrNodeBase } from "./nodes.ts";
import { Node } from "./nodes.ts";

describe("NostrNodeBase", () => {
let node: NostrNode;
let writer: WritableStreamDefaultWriter;
describe("NostrNode", () => {
describe("writable", () => {
const node = new Node(new MockWebSocket());
let writer: WritableStreamDefaultWriter;

beforeAll(() => {
node = new NostrNodeBase(new MockWebSocket());
});

afterAll(async () => {
await node.close().catch((err) => {
if (err.message !== "Writable stream is closed or errored.") {
throw err;
}
it("should open the WebSocket connection", async () => {
writer = node.writable.getWriter();
await writer.write(["NOTICE", "test"]);
writer.releaseLock();
assertEquals(node.status, WebSocket.OPEN);
});
});

it("should be connected to the WebSocket after a message is sent", async () => {
writer = node.writable.getWriter();
await writer.write(["NOTICE", "test"]);
writer.releaseLock();
assertEquals(node.status, WebSocket.OPEN);
});
describe("close", () => {
const node = new Node(new MockWebSocket());

it("should close the WebSocket when the node is closed", async () => {
await node.close();
assertEquals(node.status, WebSocket.CLOSED);
it("should close the WebSocket connection", async () => {
await node.close();
assertEquals(node.status, WebSocket.CLOSED);
});
});
});
Loading

0 comments on commit 23c7344

Please sign in to comment.