feat: NDK (WIP)

This commit is contained in:
2024-04-23 13:08:23 +01:00
parent ea54ee2b00
commit eee76e64e5
17 changed files with 1048 additions and 381 deletions

View File

@ -35,6 +35,7 @@
"dependencies": {
"@noble/curves": "^1.2.0",
"@noble/hashes": "^1.3.2",
"@nostr-dev-kit/ndk": "^2.7.1",
"@scure/base": "^1.1.2",
"@snort/shared": "^1.0.14",
"@stablelib/xchacha20": "^1.0.1",

View File

@ -2,11 +2,68 @@ import { removeUndefined, sanitizeRelayUrl, unwrap } from "@snort/shared";
import debug from "debug";
import { EventEmitter } from "eventemitter3";
import { Connection, RelaySettings } from "./connection";
import { NostrEvent, OkResponse, TaggedNostrEvent } from "./nostr";
import { SystemInterface } from ".";
import { Connection, RelaySettings, SyncCommand } from "./connection";
import { NostrEvent, OkResponse, ReqCommand, TaggedNostrEvent } from "./nostr";
import { RelayInfo, SystemInterface } from ".";
export interface NostrConnectionPoolEvents {
/**
* Events which the ConnectionType must emit
*/
export interface ConnectionTypeEvents {
change: () => void;
connected: (wasReconnect: boolean) => void;
event: (sub: string, e: TaggedNostrEvent) => void;
eose: (sub: string) => void;
closed: (sub: string, reason: string) => void;
disconnect: (code: number) => void;
auth: (challenge: string, relay: string, cb: (ev: NostrEvent) => void) => void;
notice: (msg: string) => void;
unknownMessage: (obj: Array<any>) => void;
}
export interface ConnectionSubscription {}
/**
* Basic relay connection
*/
export type ConnectionType = {
readonly id: string;
readonly address: string;
readonly info: RelayInfo | undefined;
readonly isDown: boolean;
settings: RelaySettings;
ephemeral: boolean;
/**
* Connect to relay
*/
connect: () => Promise<void>;
/**
* Disconnect relay
*/
close: () => void;
/**
* Publish an event to this relay
*/
publish: (ev: NostrEvent, timeout?: number) => Promise<OkResponse>;
/**
* Queue request
*/
request: (req: ReqCommand | SyncCommand, cbSent?: () => void) => void;
/**
* Close a request
*/
closeRequest: (id: string) => void;
} & EventEmitter<ConnectionTypeEvents>;
/**
* Events which are emitted by the connection pool
*/
export interface ConnectionPoolEvents {
connected: (address: string, wasReconnect: boolean) => void;
connectFailed: (address: string) => void;
event: (address: string, sub: string, e: TaggedNostrEvent) => void;
@ -16,31 +73,58 @@ export interface NostrConnectionPoolEvents {
notice: (address: string, msg: string) => void;
}
/**
* Base connection pool
*/
export type ConnectionPool = {
getConnection(id: string): Connection | undefined;
connect(address: string, options: RelaySettings, ephemeral: boolean): Promise<Connection | undefined>;
getConnection(id: string): ConnectionType | undefined;
connect(address: string, options: RelaySettings, ephemeral: boolean): Promise<ConnectionType | undefined>;
disconnect(address: string): void;
broadcast(ev: NostrEvent, cb?: (rsp: OkResponse) => void): Promise<OkResponse[]>;
broadcastTo(address: string, ev: NostrEvent): Promise<OkResponse>;
} & EventEmitter<NostrConnectionPoolEvents> &
Iterable<[string, Connection]>;
} & EventEmitter<ConnectionPoolEvents> &
Iterable<[string, ConnectionType]>;
/**
* Function for building new connections
*/
export type ConnectionBuilder<T extends ConnectionType> = (
address: string,
options: RelaySettings,
ephemeral: boolean,
) => Promise<T>;
/**
* Simple connection pool containing connections to multiple nostr relays
*/
export class DefaultConnectionPool extends EventEmitter<NostrConnectionPoolEvents> implements ConnectionPool {
export class DefaultConnectionPool<T extends ConnectionType = Connection>
extends EventEmitter<ConnectionPoolEvents>
implements ConnectionPool
{
#system: SystemInterface;
#log = debug("NostrConnectionPool");
#log = debug("ConnectionPool");
/**
* All currently connected websockets
*/
#sockets = new Map<string, Connection>();
#sockets = new Map<string, T>();
constructor(system: SystemInterface) {
/**
* Builder function to create new sockets
*/
#connectionBuilder: ConnectionBuilder<T>;
constructor(system: SystemInterface, builder?: ConnectionBuilder<T>) {
super();
this.#system = system;
if (builder) {
this.#connectionBuilder = builder;
} else {
this.#connectionBuilder = async (addr, options, ephemeral) => {
const c = new Connection(addr, options, ephemeral);
return c as unknown as T;
};
}
}
/**
@ -59,7 +143,7 @@ export class DefaultConnectionPool extends EventEmitter<NostrConnectionPoolEvent
try {
const existing = this.#sockets.get(addr);
if (!existing) {
const c = new Connection(addr, options, ephemeral);
const c = await this.#connectionBuilder(addr, options, ephemeral);
this.#sockets.set(addr, c);
c.on("event", (s, e) => {
@ -77,14 +161,15 @@ export class DefaultConnectionPool extends EventEmitter<NostrConnectionPoolEvent
return c;
} else {
// update settings if already connected
existing.Settings = options;
existing.settings = options;
// upgrade to non-ephemeral, never downgrade
if (existing.Ephemeral && !ephemeral) {
existing.Ephemeral = ephemeral;
if (existing.ephemeral && !ephemeral) {
existing.ephemeral = ephemeral;
}
return existing;
}
} catch (e) {
console.error(e);
this.#log("%O", e);
this.emit("connectFailed", addr);
this.#sockets.delete(addr);
@ -108,12 +193,12 @@ export class DefaultConnectionPool extends EventEmitter<NostrConnectionPoolEvent
* @remarks Also write event to read relays of those who are `p` tagged in the event (Inbox model)
*/
async broadcast(ev: NostrEvent, cb?: (rsp: OkResponse) => void) {
const writeRelays = [...this.#sockets.values()].filter(a => !a.Ephemeral && a.Settings.write);
const writeRelays = [...this.#sockets.values()].filter(a => !a.ephemeral && a.settings.write);
const replyRelays = (await this.#system.requestRouter?.forReply(ev)) ?? [];
const oks = await Promise.all([
...writeRelays.map(async s => {
try {
const rsp = await s.sendEventAsync(ev);
const rsp = await s.publish(ev);
cb?.(rsp);
return rsp;
} catch (e) {
@ -137,15 +222,15 @@ export class DefaultConnectionPool extends EventEmitter<NostrConnectionPoolEvent
const existing = this.#sockets.get(addrClean);
if (existing) {
return await existing.sendEventAsync(ev);
return await existing.publish(ev);
} else {
return await new Promise<OkResponse>((resolve, reject) => {
const c = new Connection(address, { write: true, read: true }, true);
return await new Promise<OkResponse>(async (resolve, reject) => {
const c = await this.#connectionBuilder(address, { write: true, read: true }, true);
const t = setTimeout(reject, 10_000);
c.once("connected", async () => {
clearTimeout(t);
const rsp = await c.sendEventAsync(ev);
const rsp = await c.publish(ev);
c.close();
resolve(rsp);
});

View File

@ -10,6 +10,7 @@ import { RelayInfo } from "./relay-info";
import EventKind from "./event-kind";
import { EventExt } from "./event-ext";
import { NegentropyFlow } from "./negentropy/negentropy-flow";
import { ConnectionType, ConnectionTypeEvents } from "./connection-pool";
/**
* Relay settings
@ -19,18 +20,6 @@ export interface RelaySettings {
write: boolean;
}
interface ConnectionEvents {
change: () => void;
connected: (wasReconnect: boolean) => void;
event: (sub: string, e: TaggedNostrEvent) => void;
eose: (sub: string) => void;
closed: (sub: string, reason: string) => void;
disconnect: (code: number) => void;
auth: (challenge: string, relay: string, cb: (ev: NostrEvent) => void) => void;
notice: (msg: string) => void;
unknownMessage: (obj: Array<any>) => void;
}
/**
* SYNC command is an internal command that requests the connection to devise a strategy
* to synchronize based on a set of existing cached events and a filter set.
@ -42,10 +31,10 @@ export type SyncCommand = ["SYNC", id: string, fromSet: Array<TaggedNostrEvent>,
*/
interface ConnectionQueueItem {
obj: ReqCommand | SyncCommand;
cb: () => void;
cb?: () => void;
}
export class Connection extends EventEmitter<ConnectionEvents> {
export class Connection extends EventEmitter<ConnectionTypeEvents> implements ConnectionType {
#log: debug.Debugger;
#ephemeralCheck?: ReturnType<typeof setInterval>;
#activity: number = unixNowMs();
@ -55,15 +44,15 @@ export class Connection extends EventEmitter<ConnectionEvents> {
#downCount = 0;
#activeRequests = new Set<string>();
Id: string;
readonly Address: string;
id: string;
readonly address: string;
Socket: WebSocket | null = null;
PendingRaw: Array<object> = [];
PendingRequests: Array<ConnectionQueueItem> = [];
Settings: RelaySettings;
Info?: RelayInfo;
settings: RelaySettings;
info: RelayInfo | undefined;
ConnectTimeout: number = DefaultConnectTimeout;
HasStateChange: boolean = true;
ReconnectTimer?: ReturnType<typeof setTimeout>;
@ -74,20 +63,20 @@ export class Connection extends EventEmitter<ConnectionEvents> {
constructor(addr: string, options: RelaySettings, ephemeral: boolean = false) {
super();
this.Id = uuid();
this.Address = addr;
this.Settings = options;
this.id = uuid();
this.address = addr;
this.settings = options;
this.EventsCallback = new Map();
this.AwaitingAuth = new Map();
this.#ephemeral = ephemeral;
this.#log = debug("Connection").extend(addr);
}
get Ephemeral() {
get ephemeral() {
return this.#ephemeral;
}
set Ephemeral(v: boolean) {
set ephemeral(v: boolean) {
this.#ephemeral = v;
this.#setupEphemeral();
}
@ -107,8 +96,8 @@ export class Connection extends EventEmitter<ConnectionEvents> {
async connect() {
if (this.isOpen) return;
try {
if (this.Info === undefined) {
const u = new URL(this.Address);
if (this.info === undefined) {
const u = new URL(this.address);
const rsp = await fetch(`${u.protocol === "wss:" ? "https:" : "http:"}//${u.host}`, {
headers: {
accept: "application/nostr+json",
@ -121,7 +110,7 @@ export class Connection extends EventEmitter<ConnectionEvents> {
data[k] = undefined;
}
}
this.Info = data;
this.info = data;
}
}
} catch {
@ -130,14 +119,14 @@ export class Connection extends EventEmitter<ConnectionEvents> {
const wasReconnect = this.Socket !== null;
if (this.Socket) {
this.Id = uuid();
this.id = uuid();
this.Socket.onopen = null;
this.Socket.onmessage = null;
this.Socket.onerror = null;
this.Socket.onclose = null;
this.Socket = null;
}
this.Socket = new WebSocket(this.Address);
this.Socket = new WebSocket(this.address);
this.Socket.onopen = () => this.#onOpen(wasReconnect);
this.Socket.onmessage = e => this.#onMessage(e);
this.Socket.onerror = e => this.#onError(e);
@ -215,7 +204,7 @@ export class Connection extends EventEmitter<ConnectionEvents> {
case "EVENT": {
const ev = {
...(msg[2] as NostrEvent),
relays: [this.Address],
relays: [this.address],
} as TaggedNostrEvent;
if (!EventExt.isValid(ev)) {
@ -268,10 +257,10 @@ export class Connection extends EventEmitter<ConnectionEvents> {
* Send event on this connection
*/
sendEvent(e: NostrEvent) {
if (!this.Settings.write) {
if (!this.settings.write) {
return;
}
this.send(["EVENT", e]);
this.#send(["EVENT", e]);
// todo: stats events send
this.emit("change");
}
@ -279,9 +268,9 @@ export class Connection extends EventEmitter<ConnectionEvents> {
/**
* Send event on this connection and wait for OK response
*/
async sendEventAsync(e: NostrEvent, timeout = 5000) {
async publish(e: NostrEvent, timeout = 5000) {
return await new Promise<OkResponse>((resolve, reject) => {
if (!this.Settings.write) {
if (!this.settings.write) {
reject(new Error("Not a write relay"));
return;
}
@ -290,7 +279,7 @@ export class Connection extends EventEmitter<ConnectionEvents> {
resolve({
ok: false,
id: e.id,
relay: this.Address,
relay: this.address,
message: "Duplicate request",
event: e,
});
@ -301,7 +290,7 @@ export class Connection extends EventEmitter<ConnectionEvents> {
resolve({
ok: false,
id: e.id,
relay: this.Address,
relay: this.address,
message: "Timeout waiting for OK response",
event: e,
});
@ -313,13 +302,13 @@ export class Connection extends EventEmitter<ConnectionEvents> {
resolve({
ok: accepted as boolean,
id: id as string,
relay: this.Address,
relay: this.address,
message: message as string | undefined,
event: e,
});
});
this.send(["EVENT", e]);
this.#send(["EVENT", e]);
// todo: stats events send
this.emit("change");
});
@ -329,14 +318,14 @@ export class Connection extends EventEmitter<ConnectionEvents> {
* Using relay document to determine if this relay supports a feature
*/
supportsNip(n: number) {
return this.Info?.supported_nips?.some(a => a === n) ?? false;
return this.info?.supported_nips?.some(a => a === n) ?? false;
}
/**
* Queue or send command to the relay
* @param cmd The REQ to send to the server
*/
queueReq(cmd: ReqCommand | SyncCommand, cbSent: () => void) {
request(cmd: ReqCommand | SyncCommand, cbSent?: () => void) {
const requestKinds = dedupe(
cmd
.slice(2)
@ -359,14 +348,14 @@ export class Connection extends EventEmitter<ConnectionEvents> {
obj: cmd,
cb: cbSent,
});
cbSent();
cbSent?.();
}
this.emit("change");
}
closeReq(id: string) {
closeRequest(id: string) {
if (this.#activeRequests.delete(id)) {
this.send(["CLOSE", id]);
this.#send(["CLOSE", id]);
this.emit("eose", id);
this.#sendQueuedRequests();
this.emit("change");
@ -389,29 +378,29 @@ export class Connection extends EventEmitter<ConnectionEvents> {
#sendRequestCommand(item: ConnectionQueueItem) {
try {
const cmd = item.obj;
if (cmd[0] === "REQ" || cmd[0] === "GET") {
if (cmd[0] === "REQ") {
this.#activeRequests.add(cmd[1]);
this.send(cmd);
this.#send(cmd);
} else if (cmd[0] === "SYNC") {
const [_, id, eventSet, ...filters] = cmd;
const lastResortSync = () => {
if (filters.some(a => a.since || a.until || a.ids)) {
this.queueReq(["REQ", id, ...filters], item.cb);
this.request(["REQ", id, ...filters], item.cb);
} else {
const latest = eventSet.reduce((acc, v) => (acc = v.created_at > acc ? v.created_at : acc), 0);
const newFilters = filters.map(a => ({
...a,
since: latest + 1,
}));
this.queueReq(["REQ", id, ...newFilters], item.cb);
this.request(["REQ", id, ...newFilters], item.cb);
}
};
if (this.Info?.negentropy === "v1") {
if (this.info?.negentropy === "v1") {
const newFilters = filters;
const neg = new NegentropyFlow(id, this, eventSet, newFilters);
neg.once("finish", filters => {
if (filters.length > 0) {
this.queueReq(["REQ", cmd[1], ...filters], item.cb);
this.request(["REQ", cmd[1], ...filters], item.cb);
} else {
// no results to query, emulate closed
this.emit("closed", id, "Nothing to sync");
@ -432,7 +421,7 @@ export class Connection extends EventEmitter<ConnectionEvents> {
#reset() {
// reset connection Id on disconnect, for query-tracking
this.Id = uuid();
this.id = uuid();
this.#expectAuth = false;
this.#log(
"Reset active=%O, pending=%O, raw=%O",
@ -458,8 +447,15 @@ export class Connection extends EventEmitter<ConnectionEvents> {
this.emit("change");
}
send(obj: object) {
const authPending = !this.Authed && (this.AwaitingAuth.size > 0 || this.Info?.limitation?.auth_required === true);
/**
* Send raw json object on wire
*/
sendRaw(obj: object) {
this.#send(obj);
}
#send(obj: object) {
const authPending = !this.Authed && (this.AwaitingAuth.size > 0 || this.info?.limitation?.auth_required === true);
if (!this.isOpen || authPending) {
this.PendingRaw.push(obj);
return false;
@ -494,7 +490,7 @@ export class Connection extends EventEmitter<ConnectionEvents> {
};
this.AwaitingAuth.set(challenge, true);
const authEvent = await new Promise<NostrEvent>((resolve, reject) =>
this.emit("auth", challenge, this.Address, resolve),
this.emit("auth", challenge, this.address, resolve),
);
this.#log("Auth result: %o", authEvent);
if (!authEvent) {
@ -522,7 +518,7 @@ export class Connection extends EventEmitter<ConnectionEvents> {
}
get #maxSubscriptions() {
return this.Info?.limitation?.max_subscriptions ?? 25;
return this.info?.limitation?.max_subscriptions ?? 25;
}
#setupEphemeral() {
@ -530,7 +526,7 @@ export class Connection extends EventEmitter<ConnectionEvents> {
clearInterval(this.#ephemeralCheck);
this.#ephemeralCheck = undefined;
}
if (this.Ephemeral) {
if (this.ephemeral) {
this.#ephemeralCheck = setInterval(() => {
const lastActivity = unixNowMs() - this.#activity;
if (lastActivity > 30_000 && !this.#closing) {

View File

@ -109,19 +109,16 @@ export class Nip46Signer extends EventEmitter<Nip46Events> implements EventSigne
await this.#onReply(e);
});
this.#conn.on("connected", async () => {
this.#conn!.queueReq(
[
"REQ",
"reply",
{
kinds: [NIP46_KIND],
"#p": [this.#localPubkey],
// strfry doesn't always delete ephemeral events
since: Math.floor(Date.now() / 1000 - 10),
},
],
() => {},
);
this.#conn!.request([
"REQ",
"reply",
{
kinds: [NIP46_KIND],
"#p": [this.#localPubkey],
// strfry doesn't always delete ephemeral events
since: Math.floor(Date.now() / 1000 - 10),
},
]);
if (autoConnect) {
if (isBunker) {
@ -151,7 +148,7 @@ export class Nip46Signer extends EventEmitter<Nip46Events> implements EventSigne
async close() {
if (this.#conn) {
await this.#disconnect();
this.#conn.closeReq("reply");
this.#conn.closeRequest("reply");
this.#conn.close();
this.#conn = undefined;
this.#didInit = false;
@ -290,6 +287,6 @@ export class Nip46Signer extends EventEmitter<Nip46Events> implements EventSigne
this.#log("Send: %O", payload);
const evCommand = await eb.buildAndSign(this.#insideSigner);
await this.#conn.sendEventAsync(evCommand);
await this.#conn.publish(evCommand);
}
}

View File

@ -4,22 +4,6 @@ import { EventSigner, HexKey, NostrEvent } from "..";
const Nip7Queue: Array<WorkQueueItem> = [];
processWorkQueue(Nip7Queue);
declare global {
interface Window {
nostr?: {
getPublicKey: () => Promise<HexKey>;
signEvent: <T extends NostrEvent>(event: T) => Promise<T>;
getRelays?: () => Promise<Record<string, { read: boolean; write: boolean }>>;
nip04?: {
encrypt?: (pubkey: HexKey, plaintext: string) => Promise<string>;
decrypt?: (pubkey: HexKey, ciphertext: string) => Promise<string>;
};
};
}
}
export class Nip7Signer implements EventSigner {
get supports(): string[] {
return ["nip04"];
@ -66,6 +50,12 @@ export class Nip7Signer implements EventSigner {
if (!window.nostr) {
throw new Error("Cannot use NIP-07 signer, not found!");
}
return await barrierQueue(Nip7Queue, () => unwrap(window.nostr).signEvent(ev));
return await barrierQueue(Nip7Queue, async () => {
const signed = await unwrap(window.nostr).signEvent(ev);
return {
...ev,
sig: signed.sig,
};
});
}
}

View File

@ -1,22 +1,10 @@
import { RelaySettings } from "./connection";
import { RequestBuilder } from "./request-builder";
import { NostrEvent, OkResponse, ReqFilter, TaggedNostrEvent } from "./nostr";
import { ProfileLoaderService } from "./profile-cache";
import { AuthorsRelaysCache } from "./outbox";
import { RelayMetadataLoader } from "outbox/relay-loader";
import { Optimizer } from "./query-optimizer";
import { base64 } from "@scure/base";
import { CachedTable } from "@snort/shared";
import { ConnectionPool } from "./connection-pool";
import { EventEmitter } from "eventemitter3";
import { QueryEvents } from "./query";
import { CacheRelay } from "./cache-relay";
import { RequestRouter } from "./request-router";
import { UsersFollows } from "./cache/index";
export { NostrSystem } from "./nostr-system";
export { NDKSystem } from "./ndk-system";
export { default as EventKind } from "./event-kind";
export { default as SocialGraph, socialGraphInstance } from "./SocialGraph/SocialGraph";
export * from "./system";
export * from "./SocialGraph/UniqueIds";
export * from "./nostr";
export * from "./links";
@ -53,134 +41,6 @@ export * from "./cache/user-relays";
export * from "./cache/user-metadata";
export * from "./cache/relay-metric";
export type QueryLike = {
get progress(): number;
feed: {
add: (evs: Array<TaggedNostrEvent>) => void;
clear: () => void;
};
cancel: () => void;
uncancel: () => void;
get snapshot(): Array<TaggedNostrEvent>;
} & EventEmitter<QueryEvents>;
export interface SystemInterface {
/**
* Check event signatures (reccomended)
*/
checkSigs: boolean;
/**
* Do some initialization
* @param follows A follower list to preload content for
*/
Init(follows?: Array<string>): Promise<void>;
/**
* Get an active query by ID
* @param id Query ID
*/
GetQuery(id: string): QueryLike | undefined;
/**
* Open a new query to relays
* @param req Request to send to relays
*/
Query(req: RequestBuilder): QueryLike;
/**
* Fetch data from nostr relays asynchronously
* @param req Request to send to relays
* @param cb A callback which will fire every 100ms when new data is received
*/
Fetch(req: RequestBuilder, cb?: (evs: Array<TaggedNostrEvent>) => void): Promise<Array<TaggedNostrEvent>>;
/**
* Create a new permanent connection to a relay
* @param address Relay URL
* @param options Read/Write settings
*/
ConnectToRelay(address: string, options: RelaySettings): Promise<void>;
/**
* Disconnect permanent relay connection
* @param address Relay URL
*/
DisconnectRelay(address: string): void;
/**
* Push an event into the system from external source
*/
HandleEvent(subId: string, ev: TaggedNostrEvent): void;
/**
* Send an event to all permanent connections
* @param ev Event to broadcast
* @param cb Callback to handle OkResponse as they arrive
*/
BroadcastEvent(ev: NostrEvent, cb?: (rsp: OkResponse) => void): Promise<Array<OkResponse>>;
/**
* Connect to a specific relay and send an event and wait for the response
* @param relay Relay URL
* @param ev Event to send
*/
WriteOnceToRelay(relay: string, ev: NostrEvent): Promise<OkResponse>;
/**
* Profile cache/loader
*/
get profileLoader(): ProfileLoaderService;
/**
* Relay cache for "Gossip" model
*/
get relayCache(): AuthorsRelaysCache;
/**
* Query optimizer
*/
get optimizer(): Optimizer;
/**
* Generic cache store for events
*/
get eventsCache(): CachedTable<NostrEvent>;
/**
* ContactList cache
*/
get userFollowsCache(): CachedTable<UsersFollows>;
/**
* Relay loader loads relay metadata for a set of profiles
*/
get relayLoader(): RelayMetadataLoader;
/**
* Main connection pool
*/
get pool(): ConnectionPool;
/**
* Local relay cache service
*/
get cacheRelay(): CacheRelay | undefined;
/**
* Request router instance
*/
get requestRouter(): RequestRouter | undefined;
}
export interface SystemSnapshot {
queries: Array<{
id: string;
filters: Array<ReqFilter>;
subFilters: Array<ReqFilter>;
}>;
}
export const enum MessageEncryptorVersion {
Nip4 = 0,
XChaCha20 = 1,

View File

@ -0,0 +1,220 @@
import { EventEmitter } from "eventemitter3";
import { QueryLike, SystemConfig, SystemInterface } from "./system";
import { RelaySettings, SyncCommand } from "./connection";
import { TaggedNostrEvent, NostrEvent, OkResponse, ReqCommand } from "./nostr";
import { BuiltRawReqFilter, RequestBuilder } from "./request-builder";
import NDK, { NDKConstructorParams, NDKEvent, NDKFilter, NDKRelay, NDKSubscription } from "@nostr-dev-kit/ndk";
import { SystemBase } from "./system-base";
import { ConnectionPool, ConnectionType, ConnectionTypeEvents, DefaultConnectionPool } from "./connection-pool";
import { RelayMetadataLoader } from "./outbox";
import { ProfileLoaderService } from "./profile-cache";
import { RequestRouter } from "./request-router";
import { RelayMetricHandler } from "./relay-metric-handler";
import { RelayInfo } from "./relay-info";
import { v4 as uuid } from "uuid";
import { QueryManager } from "./query-manager";
import debug from "debug";
class NDKConnection extends EventEmitter<ConnectionTypeEvents> implements ConnectionType {
#id: string;
#settings: RelaySettings;
#ephemeral: boolean;
constructor(
readonly ndk: NDK,
readonly relay: NDKRelay,
settings: RelaySettings,
ephemeral: boolean,
) {
super();
this.#id = uuid();
this.#settings = settings;
this.#ephemeral = ephemeral;
}
get id() {
return this.#id;
}
get address() {
return this.relay.url;
}
get settings() {
return this.#settings;
}
set settings(v: RelaySettings) {
this.#settings = v;
}
get ephemeral() {
return this.#ephemeral;
}
get isDown() {
return !this.relay.connectivity.isAvailable();
}
info: RelayInfo | undefined;
async connect() {
await this.relay.connect();
}
close() {
this.relay.disconnect();
}
async publish(ev: NostrEvent, timeout?: number | undefined) {
const result = await this.relay.publish(new NDKEvent(undefined, ev), timeout);
return {
id: ev.id,
ok: result,
} as OkResponse;
}
async request(req: ReqCommand | SyncCommand, cbSent?: (() => void) | undefined) {
if (req[0] === "REQ") {
const id = req[1];
const filters = req.slice(2) as NDKFilter[];
const sub = new NDKSubscription(this.ndk, filters);
sub.on("event", (ev: NDKEvent) => {
this.emit("event", id, ev.rawEvent() as TaggedNostrEvent);
});
sub.on("eose", () => {
this.emit("eose", id);
});
this.relay.subscribe(sub, filters);
} else if (req[0] === "SYNC") {
const id = req[1];
const filters = req.slice(3) as NDKFilter[];
const sub = new NDKSubscription(this.ndk, filters);
sub.on("event", (ev: NDKEvent) => {
this.emit("event", id, ev.rawEvent() as TaggedNostrEvent);
});
sub.on("eose", () => {
debugger;
this.emit("eose", id);
});
this.relay.subscribe(sub, filters);
}
}
closeRequest(id: string) {
// idk..
}
}
class NDKConnectionPool extends DefaultConnectionPool<NDKConnection> {
constructor(
system: SystemInterface,
readonly ndk: NDK,
) {
super(system, async (addr, opt, eph) => {
const relay = new NDKRelay(addr);
this.ndk.pool.addRelay(relay);
return new NDKConnection(this.ndk, relay, opt, eph);
});
}
}
export class NDKSystem extends SystemBase implements SystemInterface {
#log = debug("NDKSystem");
#ndk: NDK;
#queryManager: QueryManager;
readonly profileLoader: ProfileLoaderService;
readonly relayMetricsHandler: RelayMetricHandler;
readonly pool: ConnectionPool;
readonly relayLoader: RelayMetadataLoader;
readonly requestRouter: RequestRouter | undefined;
constructor(system: Partial<SystemConfig>, ndk?: NDKConstructorParams) {
super(system);
this.#ndk = new NDK(ndk);
this.profileLoader = new ProfileLoaderService(this, this.profileCache);
this.relayMetricsHandler = new RelayMetricHandler(this.relayMetricsCache);
this.relayLoader = new RelayMetadataLoader(this, this.relayCache);
this.pool = new NDKConnectionPool(this, this.#ndk);
this.#queryManager = new QueryManager(this);
// hook connection pool
this.pool.on("connected", (id, wasReconnect) => {
const c = this.pool.getConnection(id);
if (c) {
this.relayMetricsHandler.onConnect(c.address);
if (wasReconnect) {
for (const [, q] of this.#queryManager) {
q.connectionRestored(c);
}
}
}
});
this.pool.on("connectFailed", address => {
this.relayMetricsHandler.onDisconnect(address, 0);
});
this.pool.on("event", (_, sub, ev) => {
ev.relays?.length && this.relayMetricsHandler.onEvent(ev.relays[0]);
this.emit("event", sub, ev);
});
this.pool.on("disconnect", (id, code) => {
const c = this.pool.getConnection(id);
if (c) {
this.relayMetricsHandler.onDisconnect(c.address, code);
for (const [, q] of this.#queryManager) {
q.connectionLost(c.id);
}
}
});
this.pool.on("auth", (_, c, r, cb) => this.emit("auth", c, r, cb));
this.pool.on("notice", (addr, msg) => {
this.#log("NOTICE: %s %s", addr, msg);
});
//this.#queryManager.on("change", () => this.emit("change", this.takeSnapshot()));
this.#queryManager.on("trace", t => {
this.relayMetricsHandler.onTraceReport(t);
});
this.#queryManager.on("request", (subId: string, f: BuiltRawReqFilter) => this.emit("request", subId, f));
}
async Init(follows?: string[] | undefined) {
await this.#ndk.connect();
}
GetQuery(id: string): QueryLike | undefined {
return this.#queryManager.get(id);
}
Fetch(req: RequestBuilder, cb?: (evs: Array<TaggedNostrEvent>) => void) {
return this.#queryManager.fetch(req, cb);
}
Query(req: RequestBuilder): QueryLike {
return this.#queryManager.query(req);
}
async ConnectToRelay(address: string, options: RelaySettings) {
await this.pool.connect(address, options, false);
}
ConnectEphemeralRelay(address: string) {
return this.pool.connect(address, { read: true, write: true }, true);
}
DisconnectRelay(address: string) {
this.pool.disconnect(address);
}
HandleEvent(subId: string, ev: TaggedNostrEvent): void {
this.emit("event", subId, ev);
}
async BroadcastEvent(ev: NostrEvent, cb?: ((rsp: OkResponse) => void) | undefined): Promise<OkResponse[]> {
return await this.pool.broadcast(ev, cb);
}
async WriteOnceToRelay(relay: string, ev: NostrEvent): Promise<OkResponse> {
return await this.pool.broadcastTo(relay, ev);
}
}

View File

@ -50,7 +50,7 @@ export class NegentropyFlow extends EventEmitter<NegentropyFlowEvents> {
*/
start() {
const init = this.#negentropy.initiate();
this.#connection.send(["NEG-OPEN", this.#id, this.#filters, bytesToHex(init)]);
this.#connection.sendRaw(["NEG-OPEN", this.#id, this.#filters, bytesToHex(init)]);
}
#handleMessage(msg: Array<any>) {
@ -80,9 +80,9 @@ export class NegentropyFlow extends EventEmitter<NegentropyFlowEvents> {
this.#need.push(...need.map(bytesToHex));
}
if (nextMsg) {
this.#connection.send(["NEG-MSG", this.#id, bytesToHex(nextMsg)]);
this.#connection.sendRaw(["NEG-MSG", this.#id, bytesToHex(nextMsg)]);
} else {
this.#connection.send(["NEG-CLOSE", this.#id]);
this.#connection.sendRaw(["NEG-CLOSE", this.#id]);
this.#cleanup();
}
break;

View File

@ -1,9 +1,9 @@
import debug from "debug";
import { EventEmitter } from "eventemitter3";
import { CachedTable, isHex, unixNowMs } from "@snort/shared";
import { CachedTable, unixNowMs } from "@snort/shared";
import { NostrEvent, TaggedNostrEvent, OkResponse } from "./nostr";
import { Connection, RelaySettings } from "./connection";
import { RelaySettings } from "./connection";
import { BuiltRawReqFilter, RequestBuilder } from "./request-builder";
import { RelayMetricHandler } from "./relay-metric-handler";
import {
@ -16,13 +16,14 @@ import {
UserRelaysCache,
RelayMetricCache,
UsersRelays,
SnortSystemDb,
QueryLike,
OutboxModel,
socialGraphInstance,
EventKind,
UsersFollows,
ID,
NostrSystemEvents,
SystemConfig,
} from ".";
import { EventsCache } from "./cache/events";
import { RelayMetadataLoader } from "./outbox";
@ -33,76 +34,6 @@ import { CacheRelay } from "./cache-relay";
import { RequestRouter } from "./request-router";
import { UserFollowsCache } from "./cache/user-follows-lists";
export interface NostrSystemEvents {
change: (state: SystemSnapshot) => void;
auth: (challenge: string, relay: string, cb: (ev: NostrEvent) => void) => void;
event: (subId: string, ev: TaggedNostrEvent) => void;
request: (subId: string, filter: BuiltRawReqFilter) => void;
}
export interface SystemConfig {
/**
* Users configured relays (via kind 3 or kind 10_002)
*/
relays: CachedTable<UsersRelays>;
/**
* Cache of user profiles, (kind 0)
*/
profiles: CachedTable<CachedMetadata>;
/**
* Cache of relay connection stats
*/
relayMetrics: CachedTable<RelayMetrics>;
/**
* Direct reference events cache
*/
events: CachedTable<NostrEvent>;
/**
* Cache of user ContactLists (kind 3)
*/
contactLists: CachedTable<UsersFollows>;
/**
* Optimized cache relay, usually `@snort/worker-relay`
*/
cachingRelay?: CacheRelay;
/**
* Optimized functions, usually `@snort/system-wasm`
*/
optimizer: Optimizer;
/**
* Dexie database storage, usually `@snort/system-web`
*/
db?: SnortSystemDb;
/**
* Check event sigs on receive from relays
*/
checkSigs: boolean;
/**
* Automatically handle outbox model
*
* 1. Fetch relay lists automatically for queried authors
* 2. Write to inbox for all `p` tagged users in broadcasting events
*/
automaticOutboxModel: boolean;
/**
* Automatically populate SocialGraph from kind 3 events fetched.
*
* This is basically free because we always load relays (which includes kind 3 contact lists)
* for users when fetching by author.
*/
buildFollowGraph: boolean;
}
/**
* Manages nostr content retrieval system
*/
@ -233,7 +164,7 @@ export class NostrSystem extends EventEmitter<NostrSystemEvents> implements Syst
this.pool.on("connected", (id, wasReconnect) => {
const c = this.pool.getConnection(id);
if (c) {
this.relayMetricsHandler.onConnect(c.Address);
this.relayMetricsHandler.onConnect(c.address);
if (wasReconnect) {
for (const [, q] of this.#queryManager) {
q.connectionRestored(c);
@ -251,9 +182,9 @@ export class NostrSystem extends EventEmitter<NostrSystemEvents> implements Syst
this.pool.on("disconnect", (id, code) => {
const c = this.pool.getConnection(id);
if (c) {
this.relayMetricsHandler.onDisconnect(c.Address, code);
this.relayMetricsHandler.onDisconnect(c.address, code);
for (const [, q] of this.#queryManager) {
q.connectionLost(c.Id);
q.connectionLost(c.id);
}
}
});

View File

@ -37,7 +37,7 @@ export type MaybeHexKey = HexKey | undefined;
*/
export type u256 = string;
export type ReqCommand = [cmd: "REQ" | "IDS" | "GET", id: string, ...filters: Array<ReqFilter>];
export type ReqCommand = [cmd: "REQ", id: string, ...filters: Array<ReqFilter>];
/**
* Raw REQ filter object

View File

@ -152,7 +152,7 @@ export class QueryManager extends EventEmitter<QueryManagerEvents> {
} else {
const ret = [];
for (const [a, s] of this.#system.pool) {
if (!s.Ephemeral) {
if (!s.ephemeral) {
this.#log("Sending query to %s %s %O", a, q.id, qSend);
const qt = q.sendToRelay(s, qSend);
if (qt) {

View File

@ -3,11 +3,12 @@ import debug from "debug";
import { EventEmitter } from "eventemitter3";
import { unixNowMs, unwrap } from "@snort/shared";
import { Connection, ReqFilter, Nips, TaggedNostrEvent, SystemInterface, ParsedFragment } from ".";
import { ReqFilter, Nips, TaggedNostrEvent, SystemInterface, ParsedFragment } from ".";
import { NoteCollection } from "./note-collection";
import { BuiltRawReqFilter, RequestBuilder } from "./request-builder";
import { eventMatchesFilter } from "./request-matcher";
import { LRUCache } from "lru-cache";
import { ConnectionType } from "./connection-pool";
interface QueryTraceEvents {
change: () => void;
@ -92,7 +93,7 @@ export class QueryTrace extends EventEmitter<QueryTraceEvents> {
export interface TraceReport {
id: string;
conn: Connection;
conn: ConnectionType;
wasForced: boolean;
queued: number;
responseTime: number;
@ -275,7 +276,7 @@ export class Query extends EventEmitter<QueryEvents> {
return qt;
}
sendToRelay(c: Connection, subq: BuiltRawReqFilter) {
sendToRelay(c: ConnectionType, subq: BuiltRawReqFilter) {
if (!this.#canSendQuery(c, subq)) {
return;
}
@ -286,12 +287,12 @@ export class Query extends EventEmitter<QueryEvents> {
this.#tracing.filter(a => a.connId == id).forEach(a => a.forceEose());
}
connectionRestored(c: Connection) {
connectionRestored(c: ConnectionType) {
if (this.isOpen()) {
for (const qt of this.#tracing) {
if (qt.relay === c.Address) {
if (qt.relay === c.address) {
// todo: queue sync?
c.queueReq(["REQ", qt.id, ...qt.filters], () => qt.sentToRelay());
c.request(["REQ", qt.id, ...qt.filters], () => qt.sentToRelay());
}
}
}
@ -329,8 +330,8 @@ export class Query extends EventEmitter<QueryEvents> {
}
}
#eose(sub: string, conn: Readonly<Connection>) {
const qt = this.#tracing.find(a => a.id === sub && a.connId === conn.Id);
handleEose(sub: string, conn: Readonly<ConnectionType>) {
const qt = this.#tracing.find(a => a.id === sub && a.connId === conn.id);
if (qt) {
qt.gotEose();
if (!this.#leaveOpen) {
@ -379,9 +380,9 @@ export class Query extends EventEmitter<QueryEvents> {
}, 500);
}
#canSendQuery(c: Connection, q: BuiltRawReqFilter) {
#canSendQuery(c: ConnectionType, q: BuiltRawReqFilter) {
// query is not for this relay
if (q.relay && q.relay !== c.Address) {
if (q.relay && q.relay !== c.address) {
return false;
}
// connection is down, dont send
@ -389,13 +390,13 @@ export class Query extends EventEmitter<QueryEvents> {
return false;
}
// cannot send unless relay is tagged on ephemeral relay connection
if (!q.relay && c.Ephemeral) {
if (!q.relay && c.ephemeral) {
this.#log("Cant send non-specific REQ to ephemeral connection %O %O %O", q, q.relay, c);
return false;
}
// search not supported, cant send
if (q.filters.some(a => a.search) && !c.supportsNip(Nips.Search)) {
this.#log("Cant send REQ to non-search relay", c.Address);
if (q.filters.some(a => a.search) && !c.info?.supported_nips?.includes(Nips.Search)) {
this.#log("Cant send REQ to non-search relay", c.address);
return false;
}
// query already closed, cant send
@ -406,10 +407,10 @@ export class Query extends EventEmitter<QueryEvents> {
return true;
}
#sendQueryInternal(c: Connection, q: BuiltRawReqFilter) {
#sendQueryInternal(c: ConnectionType, q: BuiltRawReqFilter) {
let filters = q.filters;
const qt = new QueryTrace(c.Address, filters, c.Id);
qt.on("close", x => c.closeReq(x));
const qt = new QueryTrace(c.address, filters, c.id);
qt.on("close", x => c.closeRequest(x));
qt.on("change", () => this.#onProgress());
qt.on("eose", (id, connId, forced) =>
this.emit("trace", {
@ -426,7 +427,7 @@ export class Query extends EventEmitter<QueryEvents> {
}
};
const eoseHandler = (sub: string) => {
this.#eose(sub, c);
this.handleEose(sub, c);
};
c.on("event", eventHandler);
c.on("eose", eoseHandler);
@ -439,9 +440,9 @@ export class Query extends EventEmitter<QueryEvents> {
this.#tracing.push(qt);
if (q.syncFrom !== undefined) {
c.queueReq(["SYNC", qt.id, q.syncFrom, ...qt.filters], () => qt.sentToRelay());
c.request(["SYNC", qt.id, q.syncFrom, ...qt.filters], () => qt.sentToRelay());
} else {
c.queueReq(["REQ", qt.id, ...qt.filters], () => qt.sentToRelay());
c.request(["REQ", qt.id, ...qt.filters], () => qt.sentToRelay());
}
return qt;
}

View File

@ -55,7 +55,7 @@ export class RelayMetricHandler {
}
onTraceReport(t: TraceReport) {
const v = this.#cache.getFromCache(t.conn.Address);
const v = this.#cache.getFromCache(t.conn.address);
if (v) {
v.latency.push(t.responseTime);
v.latency = v.latency.slice(-50);

View File

@ -0,0 +1,81 @@
import { CachedTable } from "@snort/shared";
import { UsersRelays, CachedMetadata, RelayMetrics, UsersFollows } from "./cache";
import { CacheRelay } from "./cache-relay";
import { EventsCache } from "./cache/events";
import { UserFollowsCache } from "./cache/user-follows-lists";
import { UserRelaysCache, UserProfileCache, RelayMetricCache, NostrEvent } from "./index";
import { DefaultOptimizer, Optimizer } from "./query-optimizer";
import { NostrSystemEvents, SystemConfig } from "./system";
import { EventEmitter } from "eventemitter3";
export abstract class SystemBase extends EventEmitter<NostrSystemEvents> {
#config: SystemConfig;
constructor(props: Partial<SystemConfig>) {
super();
this.#config = {
relays: props.relays ?? new UserRelaysCache(props.db?.userRelays),
profiles: props.profiles ?? new UserProfileCache(props.db?.users),
relayMetrics: props.relayMetrics ?? new RelayMetricCache(props.db?.relayMetrics),
events: props.events ?? new EventsCache(props.db?.events),
contactLists: props.contactLists ?? new UserFollowsCache(props.db?.contacts),
optimizer: props.optimizer ?? DefaultOptimizer,
checkSigs: props.checkSigs ?? false,
cachingRelay: props.cachingRelay,
db: props.db,
automaticOutboxModel: props.automaticOutboxModel ?? true,
buildFollowGraph: props.buildFollowGraph ?? false,
};
}
/**
* Storage class for user relay lists
*/
get relayCache(): CachedTable<UsersRelays> {
return this.#config.relays;
}
/**
* Storage class for user profiles
*/
get profileCache(): CachedTable<CachedMetadata> {
return this.#config.profiles;
}
/**
* Storage class for relay metrics (connects/disconnects)
*/
get relayMetricsCache(): CachedTable<RelayMetrics> {
return this.#config.relayMetrics;
}
/**
* Optimizer instance, contains optimized functions for processing data
*/
get optimizer(): Optimizer {
return this.#config.optimizer;
}
get eventsCache(): CachedTable<NostrEvent> {
return this.#config.events;
}
get userFollowsCache(): CachedTable<UsersFollows> {
return this.#config.contactLists;
}
get cacheRelay(): CacheRelay | undefined {
return this.#config.cachingRelay;
}
/**
* Check event signatures (recommended)
*/
get checkSigs(): boolean {
return this.#config.checkSigs;
}
set checkSigs(v: boolean) {
this.#config.checkSigs = v;
}
}

View File

@ -0,0 +1,211 @@
import { CachedTable } from "@snort/shared";
import { UsersRelays, CachedMetadata, RelayMetrics, UsersFollows, SnortSystemDb } from "./cache";
import { CacheRelay } from "./cache-relay";
import { RelaySettings } from "./connection";
import { ConnectionPool } from "./connection-pool";
import { TaggedNostrEvent, OkResponse, ReqFilter, NostrEvent } from "./nostr";
import { AuthorsRelaysCache, RelayMetadataLoader } from "./outbox";
import { ProfileLoaderService } from "./profile-cache";
import { Optimizer } from "./query-optimizer";
import { BuiltRawReqFilter, RequestBuilder } from "./request-builder";
import { RequestRouter } from "./request-router";
import { QueryEvents } from "./query";
import EventEmitter from "eventemitter3";
export type QueryLike = {
get progress(): number;
feed: {
add: (evs: Array<TaggedNostrEvent>) => void;
clear: () => void;
};
cancel: () => void;
uncancel: () => void;
get snapshot(): Array<TaggedNostrEvent>;
} & EventEmitter<QueryEvents>;
export interface NostrSystemEvents {
change: (state: SystemSnapshot) => void;
auth: (challenge: string, relay: string, cb: (ev: NostrEvent) => void) => void;
event: (subId: string, ev: TaggedNostrEvent) => void;
request: (subId: string, filter: BuiltRawReqFilter) => void;
}
export interface SystemConfig {
/**
* Users configured relays (via kind 3 or kind 10_002)
*/
relays: CachedTable<UsersRelays>;
/**
* Cache of user profiles, (kind 0)
*/
profiles: CachedTable<CachedMetadata>;
/**
* Cache of relay connection stats
*/
relayMetrics: CachedTable<RelayMetrics>;
/**
* Direct reference events cache
*/
events: CachedTable<NostrEvent>;
/**
* Cache of user ContactLists (kind 3)
*/
contactLists: CachedTable<UsersFollows>;
/**
* Optimized cache relay, usually `@snort/worker-relay`
*/
cachingRelay?: CacheRelay;
/**
* Optimized functions, usually `@snort/system-wasm`
*/
optimizer: Optimizer;
/**
* Dexie database storage, usually `@snort/system-web`
*/
db?: SnortSystemDb;
/**
* Check event sigs on receive from relays
*/
checkSigs: boolean;
/**
* Automatically handle outbox model
*
* 1. Fetch relay lists automatically for queried authors
* 2. Write to inbox for all `p` tagged users in broadcasting events
*/
automaticOutboxModel: boolean;
/**
* Automatically populate SocialGraph from kind 3 events fetched.
*
* This is basically free because we always load relays (which includes kind 3 contact lists)
* for users when fetching by author.
*/
buildFollowGraph: boolean;
}
export interface SystemInterface {
/**
* Check event signatures (reccomended)
*/
checkSigs: boolean;
/**
* Do some initialization
* @param follows A follower list to preload content for
*/
Init(follows?: Array<string>): Promise<void>;
/**
* Get an active query by ID
* @param id Query ID
*/
GetQuery(id: string): QueryLike | undefined;
/**
* Open a new query to relays
* @param req Request to send to relays
*/
Query(req: RequestBuilder): QueryLike;
/**
* Fetch data from nostr relays asynchronously
* @param req Request to send to relays
* @param cb A callback which will fire every 100ms when new data is received
*/
Fetch(req: RequestBuilder, cb?: (evs: Array<TaggedNostrEvent>) => void): Promise<Array<TaggedNostrEvent>>;
/**
* Create a new permanent connection to a relay
* @param address Relay URL
* @param options Read/Write settings
*/
ConnectToRelay(address: string, options: RelaySettings): Promise<void>;
/**
* Disconnect permanent relay connection
* @param address Relay URL
*/
DisconnectRelay(address: string): void;
/**
* Push an event into the system from external source
*/
HandleEvent(subId: string, ev: TaggedNostrEvent): void;
/**
* Send an event to all permanent connections
* @param ev Event to broadcast
* @param cb Callback to handle OkResponse as they arrive
*/
BroadcastEvent(ev: NostrEvent, cb?: (rsp: OkResponse) => void): Promise<Array<OkResponse>>;
/**
* Connect to a specific relay and send an event and wait for the response
* @param relay Relay URL
* @param ev Event to send
*/
WriteOnceToRelay(relay: string, ev: NostrEvent): Promise<OkResponse>;
/**
* Profile cache/loader
*/
get profileLoader(): ProfileLoaderService;
/**
* Relay cache for "Gossip" model
*/
get relayCache(): AuthorsRelaysCache;
/**
* Query optimizer
*/
get optimizer(): Optimizer;
/**
* Generic cache store for events
*/
get eventsCache(): CachedTable<NostrEvent>;
/**
* ContactList cache
*/
get userFollowsCache(): CachedTable<UsersFollows>;
/**
* Relay loader loads relay metadata for a set of profiles
*/
get relayLoader(): RelayMetadataLoader;
/**
* Main connection pool
*/
get pool(): ConnectionPool;
/**
* Local relay cache service
*/
get cacheRelay(): CacheRelay | undefined;
/**
* Request router instance
*/
get requestRouter(): RequestRouter | undefined;
}
export interface SystemSnapshot {
queries: Array<{
id: string;
filters: Array<ReqFilter>;
subFilters: Array<ReqFilter>;
}>;
}

View File

@ -156,9 +156,7 @@ export class NostrConnectWallet extends EventEmitter<WalletEvents> implements LN
},
reject,
});
this.#conn?.queueReq(["REQ", "info", { kinds: [13194], limit: 1 }], () => {
// ignored
});
this.#conn?.request(["REQ", "info", { kinds: [13194], limit: 1 }]);
});
} else {
throw new WalletError(WalletErrorCode.GeneralError, rsp.error.message);
@ -292,7 +290,7 @@ export class NostrConnectWallet extends EventEmitter<WalletEvents> implements LN
pending.resolve(e.content);
this.#commandQueue.delete(replyTo[1]);
this.#conn?.closeReq(sub);
this.#conn?.closeRequest(sub);
}
async #rpc<T>(method: string, params: Record<string, string | number | undefined>) {
@ -320,21 +318,16 @@ export class NostrConnectWallet extends EventEmitter<WalletEvents> implements LN
.tag(["p", this.#config.walletPubkey]);
const evCommand = await eb.buildAndSign(this.#config.secret);
this.#conn.queueReq(
[
"REQ",
evCommand.id.slice(0, 12),
{
kinds: [23195 as EventKind],
authors: [this.#config.walletPubkey],
["#e"]: [evCommand.id],
},
],
() => {
// ignored
this.#conn.request([
"REQ",
evCommand.id.slice(0, 12),
{
kinds: [23195 as EventKind],
authors: [this.#config.walletPubkey],
["#e"]: [evCommand.id],
},
);
await this.#conn.sendEventAsync(evCommand);
]);
await this.#conn.publish(evCommand);
return await new Promise<T>((resolve, reject) => {
this.#commandQueue.set(evCommand.id, {
resolve: async (o: string) => {