Skip to content

Commit

Permalink
refactor: thin core
Browse files Browse the repository at this point in the history
  • Loading branch information
hasundue committed Mar 17, 2024
1 parent 8b86963 commit 70d907e
Show file tree
Hide file tree
Showing 26 changed files with 253 additions and 314 deletions.
41 changes: 10 additions & 31 deletions core/clients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,62 +4,41 @@ 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<N extends NIP = never>
extends NostrNodeConfig<N> {
modules: ClientModule<N, N>[];
}
export type ClientConfig = NodeConfig;
export type ClientOptions = Partial<ClientConfig>;

export type ClientOptions<N extends NIP = never> = Partial<ClientConfig<N>>;
export interface ClientEventTypeRecord {
message: ClientToRelayMessage;
}

/**
* A class that represents a remote Nostr client.
*/
export class Client<N extends NIP = never> extends NostrNode<
N,
export class Client extends Node<
RelayToClientMessage,
ClientEventTypeRecord
> {
declare ws: WebSocket;
declare config: ClientConfig<N>;
#ready: Promise<void[]>;
declare config: ClientConfig;

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

constructor(ws: WebSocket, options?: ClientOptions<N>) {
constructor(ws: WebSocket, options?: ClientOptions) {
super(ws, options);
this.config = {
...this.config,
modules: [],
...options,
};
this.ws.addEventListener("message", async (ev: MessageEvent<string>) => {
this.ws.addEventListener("message", (ev: MessageEvent<string>) => {
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<P | D>): void | Promise<void>;
}
137 changes: 38 additions & 99 deletions core/nodes.ts
Original file line number Diff line number Diff line change
@@ -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<NostrNodeConfig<N>>;
export type NodeOptions = Partial<NodeConfig>;

/**
* 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<M>;
readonly config: Readonly<NostrNodeConfig<N>>;
readonly config: Readonly<NodeConfig>;

constructor(
readonly ws: WebSocketLike,
opts: NostrNodeOptions<N> = {},
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<void> {
Expand All @@ -67,28 +50,28 @@ export class NostrNode<
declare addEventListener: <T extends EventType<R>>(
type: T,
listener:
| NostrNodeEventListenerOrEventListenerObject<N, M, R, T>
| NodeEventListenerOrEventListenerObject<M, R, T>
| null,
options?: AddEventListenerOptions,
) => void;

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

declare dispatchEvent: <T extends EventType<R>>(
event: NostrNodeEvent<R, T>,
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 @@ -100,89 +83,45 @@ export class NostrNode<
}
}

//-------------------------
// Messages and contexts
//-------------------------

type SubscriptionMessage = {
[T in RelayToClientMessageType]: RelayToClientMessage<T>[1] extends
SubscriptionId ? RelayToClientMessage<T>
: never;
}[RelayToClientMessageType];

type PublicationMessage = {
[T in RelayToClientMessageType]: RelayToClientMessage<T>[1] extends EventId
? RelayToClientMessage<T>
: never;
}[RelayToClientMessageType];

export interface SubscriptionContext {
id: SubscriptionId;
filters: SubscriptionFilter[];
realtime: boolean;
}

export interface PublicationContext extends PromiseCallbackRecord<void> {
event: NostrEvent;
}

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

type EventTypeRecord = RelayEventTypeRecord | ClientEventTypeRecord;

export interface RelayEventTypeRecord {
message: RelayToClientMessage;
subscribe: SubscriptionContext & {
controller: ReadableStreamDefaultController<NostrEvent>;
};
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<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<
N extends NIP,
M extends NostrMessage,
R extends EventTypeRecord,
T extends EventType<R>,
type NodeEventListenerOrEventListenerObject<
M extends InterNodeMessage,
R = AnyEventTypeRecord,
T extends EventType<R> = EventType<R>,
> =
| NostrNodeEventListener<N, M, R, T>
| NostrNodeEventListenerObject<N, M, R, T>;

type NostrNodeEventListener<
N extends NIP,
W extends NostrMessage,
R extends EventTypeRecord,
T extends 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<N, W, R>, ev: MessageEvent<R[T]>) => any;
= (this: Node<W, R>, ev: MessageEvent<R[T]>) => any;

type NostrNodeEventListenerObject<
N extends NIP,
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<N, W, R>, ev: MessageEvent<R[T]>): any;
handleEvent(this: Node<W, R>, ev: MessageEvent<R[T]>): any;
};
23 changes: 3 additions & 20 deletions core/nodes_test.ts
Original file line number Diff line number Diff line change
@@ -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, N> = T extends NostrNode<infer M> ? Has<M, N> : 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<HasNIP<typeof node, NIP>>(false);
});

it("should inherit the type of `nips`", () => {
const node = new NostrNode(new MockWebSocket(), { nips: [1] });
assertType<HasNIP<typeof node, 1>>(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 () => {
Expand All @@ -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();
Expand Down
Loading

0 comments on commit 70d907e

Please sign in to comment.