From eee76e64e501ac324c4f26ca2ca49e25f89b1897 Mon Sep 17 00:00:00 2001 From: kieran Date: Tue, 23 Apr 2024 13:08:23 +0100 Subject: [PATCH] feat: NDK (WIP) --- packages/system/package.json | 1 + packages/system/src/connection-pool.ts | 131 +++++-- packages/system/src/connection.ts | 104 +++--- packages/system/src/impl/nip46.ts | 27 +- packages/system/src/impl/nip7.ts | 24 +- packages/system/src/index.ts | 144 +------- packages/system/src/ndk-system.ts | 220 ++++++++++++ .../system/src/negentropy/negentropy-flow.ts | 6 +- packages/system/src/nostr-system.ts | 83 +---- packages/system/src/nostr.ts | 2 +- packages/system/src/query-manager.ts | 2 +- packages/system/src/query.ts | 39 +-- packages/system/src/relay-metric-handler.ts | 2 +- packages/system/src/system-base.ts | 81 +++++ packages/system/src/system.ts | 211 ++++++++++++ packages/wallet/src/NostrWalletConnect.ts | 29 +- yarn.lock | 323 +++++++++++++++++- 17 files changed, 1048 insertions(+), 381 deletions(-) create mode 100644 packages/system/src/ndk-system.ts create mode 100644 packages/system/src/system-base.ts create mode 100644 packages/system/src/system.ts diff --git a/packages/system/package.json b/packages/system/package.json index 07777e38..7eb8985c 100644 --- a/packages/system/package.json +++ b/packages/system/package.json @@ -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", diff --git a/packages/system/src/connection-pool.ts b/packages/system/src/connection-pool.ts index cb08cb97..de8a6f5e 100644 --- a/packages/system/src/connection-pool.ts +++ b/packages/system/src/connection-pool.ts @@ -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) => 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; + + /** + * Disconnect relay + */ + close: () => void; + + /** + * Publish an event to this relay + */ + publish: (ev: NostrEvent, timeout?: number) => Promise; + + /** + * Queue request + */ + request: (req: ReqCommand | SyncCommand, cbSent?: () => void) => void; + + /** + * Close a request + */ + closeRequest: (id: string) => void; +} & EventEmitter; + +/** + * 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; + getConnection(id: string): ConnectionType | undefined; + connect(address: string, options: RelaySettings, ephemeral: boolean): Promise; disconnect(address: string): void; broadcast(ev: NostrEvent, cb?: (rsp: OkResponse) => void): Promise; broadcastTo(address: string, ev: NostrEvent): Promise; -} & EventEmitter & - Iterable<[string, Connection]>; +} & EventEmitter & + Iterable<[string, ConnectionType]>; + +/** + * Function for building new connections + */ +export type ConnectionBuilder = ( + address: string, + options: RelaySettings, + ephemeral: boolean, +) => Promise; /** * Simple connection pool containing connections to multiple nostr relays */ -export class DefaultConnectionPool extends EventEmitter implements ConnectionPool { +export class DefaultConnectionPool + extends EventEmitter + implements ConnectionPool +{ #system: SystemInterface; - - #log = debug("NostrConnectionPool"); + #log = debug("ConnectionPool"); /** * All currently connected websockets */ - #sockets = new Map(); + #sockets = new Map(); - constructor(system: SystemInterface) { + /** + * Builder function to create new sockets + */ + #connectionBuilder: ConnectionBuilder; + + constructor(system: SystemInterface, builder?: ConnectionBuilder) { 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 { @@ -77,14 +161,15 @@ export class DefaultConnectionPool extends EventEmitter 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((resolve, reject) => { - const c = new Connection(address, { write: true, read: true }, true); + return await new Promise(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); }); diff --git a/packages/system/src/connection.ts b/packages/system/src/connection.ts index c0e0e8c6..155ab480 100644 --- a/packages/system/src/connection.ts +++ b/packages/system/src/connection.ts @@ -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) => 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, */ interface ConnectionQueueItem { obj: ReqCommand | SyncCommand; - cb: () => void; + cb?: () => void; } -export class Connection extends EventEmitter { +export class Connection extends EventEmitter implements ConnectionType { #log: debug.Debugger; #ephemeralCheck?: ReturnType; #activity: number = unixNowMs(); @@ -55,15 +44,15 @@ export class Connection extends EventEmitter { #downCount = 0; #activeRequests = new Set(); - Id: string; - readonly Address: string; + id: string; + readonly address: string; Socket: WebSocket | null = null; PendingRaw: Array = []; PendingRequests: Array = []; - Settings: RelaySettings; - Info?: RelayInfo; + settings: RelaySettings; + info: RelayInfo | undefined; ConnectTimeout: number = DefaultConnectTimeout; HasStateChange: boolean = true; ReconnectTimer?: ReturnType; @@ -74,20 +63,20 @@ export class Connection extends EventEmitter { 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 { 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 { data[k] = undefined; } } - this.Info = data; + this.info = data; } } } catch { @@ -130,14 +119,14 @@ export class Connection extends EventEmitter { 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 { 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 { * 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 { /** * 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((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 { 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 { 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 { 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 { * 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 { 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 { #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 { #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 { 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 { }; this.AwaitingAuth.set(challenge, true); const authEvent = await new Promise((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 { } 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 { 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) { diff --git a/packages/system/src/impl/nip46.ts b/packages/system/src/impl/nip46.ts index de88fc21..b3736294 100644 --- a/packages/system/src/impl/nip46.ts +++ b/packages/system/src/impl/nip46.ts @@ -109,19 +109,16 @@ export class Nip46Signer extends EventEmitter 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 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 implements EventSigne this.#log("Send: %O", payload); const evCommand = await eb.buildAndSign(this.#insideSigner); - await this.#conn.sendEventAsync(evCommand); + await this.#conn.publish(evCommand); } } diff --git a/packages/system/src/impl/nip7.ts b/packages/system/src/impl/nip7.ts index 625d0445..1acd57e9 100644 --- a/packages/system/src/impl/nip7.ts +++ b/packages/system/src/impl/nip7.ts @@ -4,22 +4,6 @@ import { EventSigner, HexKey, NostrEvent } from ".."; const Nip7Queue: Array = []; processWorkQueue(Nip7Queue); -declare global { - interface Window { - nostr?: { - getPublicKey: () => Promise; - signEvent: (event: T) => Promise; - - getRelays?: () => Promise>; - - nip04?: { - encrypt?: (pubkey: HexKey, plaintext: string) => Promise; - decrypt?: (pubkey: HexKey, ciphertext: string) => Promise; - }; - }; - } -} - 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, + }; + }); } } diff --git a/packages/system/src/index.ts b/packages/system/src/index.ts index ada1655a..1002aa0c 100644 --- a/packages/system/src/index.ts +++ b/packages/system/src/index.ts @@ -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) => void; - clear: () => void; - }; - cancel: () => void; - uncancel: () => void; - get snapshot(): Array; -} & EventEmitter; - -export interface SystemInterface { - /** - * Check event signatures (reccomended) - */ - checkSigs: boolean; - - /** - * Do some initialization - * @param follows A follower list to preload content for - */ - Init(follows?: Array): Promise; - - /** - * 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) => void): Promise>; - - /** - * Create a new permanent connection to a relay - * @param address Relay URL - * @param options Read/Write settings - */ - ConnectToRelay(address: string, options: RelaySettings): Promise; - - /** - * 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>; - - /** - * 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; - - /** - * 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; - - /** - * ContactList cache - */ - get userFollowsCache(): CachedTable; - - /** - * 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; - subFilters: Array; - }>; -} - export const enum MessageEncryptorVersion { Nip4 = 0, XChaCha20 = 1, diff --git a/packages/system/src/ndk-system.ts b/packages/system/src/ndk-system.ts new file mode 100644 index 00000000..a7caf76c --- /dev/null +++ b/packages/system/src/ndk-system.ts @@ -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 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 { + 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, 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) => 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 { + return await this.pool.broadcast(ev, cb); + } + + async WriteOnceToRelay(relay: string, ev: NostrEvent): Promise { + return await this.pool.broadcastTo(relay, ev); + } +} diff --git a/packages/system/src/negentropy/negentropy-flow.ts b/packages/system/src/negentropy/negentropy-flow.ts index 0b37fcf8..32ce2018 100644 --- a/packages/system/src/negentropy/negentropy-flow.ts +++ b/packages/system/src/negentropy/negentropy-flow.ts @@ -50,7 +50,7 @@ export class NegentropyFlow extends EventEmitter { */ 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) { @@ -80,9 +80,9 @@ export class NegentropyFlow extends EventEmitter { 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; diff --git a/packages/system/src/nostr-system.ts b/packages/system/src/nostr-system.ts index 7e2777a5..83f15a4e 100644 --- a/packages/system/src/nostr-system.ts +++ b/packages/system/src/nostr-system.ts @@ -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; - - /** - * Cache of user profiles, (kind 0) - */ - profiles: CachedTable; - - /** - * Cache of relay connection stats - */ - relayMetrics: CachedTable; - - /** - * Direct reference events cache - */ - events: CachedTable; - - /** - * Cache of user ContactLists (kind 3) - */ - contactLists: CachedTable; - - /** - * 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 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 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); } } }); diff --git a/packages/system/src/nostr.ts b/packages/system/src/nostr.ts index 20f0dc39..c88ea894 100644 --- a/packages/system/src/nostr.ts +++ b/packages/system/src/nostr.ts @@ -37,7 +37,7 @@ export type MaybeHexKey = HexKey | undefined; */ export type u256 = string; -export type ReqCommand = [cmd: "REQ" | "IDS" | "GET", id: string, ...filters: Array]; +export type ReqCommand = [cmd: "REQ", id: string, ...filters: Array]; /** * Raw REQ filter object diff --git a/packages/system/src/query-manager.ts b/packages/system/src/query-manager.ts index 5c996aa1..73a4d2e8 100644 --- a/packages/system/src/query-manager.ts +++ b/packages/system/src/query-manager.ts @@ -152,7 +152,7 @@ export class QueryManager extends EventEmitter { } 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) { diff --git a/packages/system/src/query.ts b/packages/system/src/query.ts index 8d09f8c9..d1d33b38 100644 --- a/packages/system/src/query.ts +++ b/packages/system/src/query.ts @@ -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 { export interface TraceReport { id: string; - conn: Connection; + conn: ConnectionType; wasForced: boolean; queued: number; responseTime: number; @@ -275,7 +276,7 @@ export class Query extends EventEmitter { 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 { 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 { } } - #eose(sub: string, conn: Readonly) { - const qt = this.#tracing.find(a => a.id === sub && a.connId === conn.Id); + handleEose(sub: string, conn: Readonly) { + 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 { }, 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 { 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 { 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 { } }; 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 { 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; } diff --git a/packages/system/src/relay-metric-handler.ts b/packages/system/src/relay-metric-handler.ts index e6b427bf..a78538fa 100644 --- a/packages/system/src/relay-metric-handler.ts +++ b/packages/system/src/relay-metric-handler.ts @@ -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); diff --git a/packages/system/src/system-base.ts b/packages/system/src/system-base.ts new file mode 100644 index 00000000..a150c1e5 --- /dev/null +++ b/packages/system/src/system-base.ts @@ -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 { + #config: SystemConfig; + + constructor(props: Partial) { + 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 { + return this.#config.relays; + } + + /** + * Storage class for user profiles + */ + get profileCache(): CachedTable { + return this.#config.profiles; + } + + /** + * Storage class for relay metrics (connects/disconnects) + */ + get relayMetricsCache(): CachedTable { + return this.#config.relayMetrics; + } + + /** + * Optimizer instance, contains optimized functions for processing data + */ + get optimizer(): Optimizer { + return this.#config.optimizer; + } + + get eventsCache(): CachedTable { + return this.#config.events; + } + + get userFollowsCache(): CachedTable { + 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; + } +} diff --git a/packages/system/src/system.ts b/packages/system/src/system.ts new file mode 100644 index 00000000..427e8685 --- /dev/null +++ b/packages/system/src/system.ts @@ -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) => void; + clear: () => void; + }; + cancel: () => void; + uncancel: () => void; + get snapshot(): Array; +} & EventEmitter; + +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; + + /** + * Cache of user profiles, (kind 0) + */ + profiles: CachedTable; + + /** + * Cache of relay connection stats + */ + relayMetrics: CachedTable; + + /** + * Direct reference events cache + */ + events: CachedTable; + + /** + * Cache of user ContactLists (kind 3) + */ + contactLists: CachedTable; + + /** + * 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): Promise; + + /** + * 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) => void): Promise>; + + /** + * Create a new permanent connection to a relay + * @param address Relay URL + * @param options Read/Write settings + */ + ConnectToRelay(address: string, options: RelaySettings): Promise; + + /** + * 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>; + + /** + * 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; + + /** + * 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; + + /** + * ContactList cache + */ + get userFollowsCache(): CachedTable; + + /** + * 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; + subFilters: Array; + }>; +} diff --git a/packages/wallet/src/NostrWalletConnect.ts b/packages/wallet/src/NostrWalletConnect.ts index fdddf8ff..625b971d 100644 --- a/packages/wallet/src/NostrWalletConnect.ts +++ b/packages/wallet/src/NostrWalletConnect.ts @@ -156,9 +156,7 @@ export class NostrConnectWallet extends EventEmitter 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 implements LN pending.resolve(e.content); this.#commandQueue.delete(replyTo[1]); - this.#conn?.closeReq(sub); + this.#conn?.closeRequest(sub); } async #rpc(method: string, params: Record) { @@ -320,21 +318,16 @@ export class NostrConnectWallet extends EventEmitter 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((resolve, reject) => { this.#commandQueue.set(evCommand.id, { resolve: async (o: string) => { diff --git a/yarn.lock b/yarn.lock index efa26c7b..01a9e07f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3991,6 +3991,15 @@ __metadata: languageName: node linkType: hard +"@noble/curves@npm:1.1.0, @noble/curves@npm:~1.1.0": + version: 1.1.0 + resolution: "@noble/curves@npm:1.1.0" + dependencies: + "@noble/hashes": "npm:1.3.1" + checksum: 10/7028e3f19a4a2a601f9159e5423f51ae86ab231bed79a6e40649b063e1ed7f55f5da0475f1377bd2c5a8e5fc485af9ce0549ad89da6b983d6af48e5d0a2041ca + languageName: node + linkType: hard + "@noble/curves@npm:1.2.0, @noble/curves@npm:^1.0.0, @noble/curves@npm:^1.2.0, @noble/curves@npm:~1.2.0": version: 1.2.0 resolution: "@noble/curves@npm:1.2.0" @@ -4000,7 +4009,7 @@ __metadata: languageName: node linkType: hard -"@noble/curves@npm:^1.3.0, @noble/curves@npm:~1.4.0": +"@noble/curves@npm:^1.3.0, @noble/curves@npm:^1.4.0, @noble/curves@npm:~1.4.0": version: 1.4.0 resolution: "@noble/curves@npm:1.4.0" dependencies: @@ -4009,15 +4018,6 @@ __metadata: languageName: node linkType: hard -"@noble/curves@npm:~1.1.0": - version: 1.1.0 - resolution: "@noble/curves@npm:1.1.0" - dependencies: - "@noble/hashes": "npm:1.3.1" - checksum: 10/7028e3f19a4a2a601f9159e5423f51ae86ab231bed79a6e40649b063e1ed7f55f5da0475f1377bd2c5a8e5fc485af9ce0549ad89da6b983d6af48e5d0a2041ca - languageName: node - linkType: hard - "@noble/hashes@npm:1.3.1": version: 1.3.1 resolution: "@noble/hashes@npm:1.3.1" @@ -4032,7 +4032,7 @@ __metadata: languageName: node linkType: hard -"@noble/hashes@npm:1.4.0, @noble/hashes@npm:~1.4.0": +"@noble/hashes@npm:1.4.0, @noble/hashes@npm:^1.3.1, @noble/hashes@npm:~1.4.0": version: 1.4.0 resolution: "@noble/hashes@npm:1.4.0" checksum: 10/e156e65794c473794c52fa9d06baf1eb20903d0d96719530f523cc4450f6c721a957c544796e6efd0197b2296e7cd70efeb312f861465e17940a3e3c7e0febc6 @@ -4046,6 +4046,13 @@ __metadata: languageName: node linkType: hard +"@noble/secp256k1@npm:^2.0.0": + version: 2.1.0 + resolution: "@noble/secp256k1@npm:2.1.0" + checksum: 10/ffd7e7b555d253b2403a01939ab9d2d8d25c3aec89a7380d569385d1a36bd6f15234dcfa0ab215eda23590258454032f447b9847a9d2754ba31c70147a4cc4dd + languageName: node + linkType: hard + "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -4073,6 +4080,26 @@ __metadata: languageName: node linkType: hard +"@nostr-dev-kit/ndk@npm:^2.7.1": + version: 2.7.1 + resolution: "@nostr-dev-kit/ndk@npm:2.7.1" + dependencies: + "@noble/curves": "npm:^1.4.0" + "@noble/hashes": "npm:^1.3.1" + "@noble/secp256k1": "npm:^2.0.0" + "@scure/base": "npm:^1.1.1" + debug: "npm:^4.3.4" + light-bolt11-decoder: "npm:^3.0.0" + node-fetch: "npm:^3.3.1" + nostr-tools: "npm:^1.15.0" + tseep: "npm:^1.1.1" + typescript-lru-cache: "npm:^2.0.0" + utf8-buffer: "npm:^1.0.0" + websocket-polyfill: "npm:^0.0.3" + checksum: 10/8553167dbc8e93952f5deb15dea443272827039a8e03410f851ec898c68811f6d3d21b36becc7194009ff5994cf073c8997355b43d29a55ad98d1ce95176f1f7 + languageName: node + linkType: hard + "@npmcli/agent@npm:^2.0.0": version: 2.2.0 resolution: "@npmcli/agent@npm:2.2.0" @@ -4666,6 +4693,7 @@ __metadata: "@jest/globals": "npm:^29.5.0" "@noble/curves": "npm:^1.2.0" "@noble/hashes": "npm:^1.3.2" + "@nostr-dev-kit/ndk": "npm:^2.7.1" "@peculiar/webcrypto": "npm:^1.4.3" "@scure/base": "npm:^1.1.2" "@snort/shared": "npm:^1.0.14" @@ -6743,6 +6771,16 @@ __metadata: languageName: node linkType: hard +"bufferutil@npm:^4.0.1": + version: 4.0.8 + resolution: "bufferutil@npm:4.0.8" + dependencies: + node-gyp: "npm:latest" + node-gyp-build: "npm:^4.3.0" + checksum: 10/d9337badc960a19d5a031db5de47159d7d8a11b6bab399bdfbf464ffa9ecd2972fef19bb61a7d2827e0c55f912c20713e12343386b86cb013f2b99c2324ab6a3 + languageName: node + linkType: hard + "builtin-modules@npm:^3.1.0": version: 3.3.0 resolution: "builtin-modules@npm:3.3.0" @@ -7423,6 +7461,16 @@ __metadata: languageName: node linkType: hard +"d@npm:1, d@npm:^1.0.1, d@npm:^1.0.2": + version: 1.0.2 + resolution: "d@npm:1.0.2" + dependencies: + es5-ext: "npm:^0.10.64" + type: "npm:^2.7.2" + checksum: 10/a3f45ef964622f683f6a1cb9b8dcbd75ce490cd2f4ac9794099db3d8f0e2814d412d84cd3fe522e58feb1f273117bb480f29c5381f6225f0abca82517caaa77a + languageName: node + linkType: hard + "damerau-levenshtein@npm:^1.0.8": version: 1.0.8 resolution: "damerau-levenshtein@npm:1.0.8" @@ -7439,6 +7487,13 @@ __metadata: languageName: node linkType: hard +"data-uri-to-buffer@npm:^4.0.0": + version: 4.0.1 + resolution: "data-uri-to-buffer@npm:4.0.1" + checksum: 10/0d0790b67ffec5302f204c2ccca4494f70b4e2d940fea3d36b09f0bb2b8539c2e86690429eb1f1dc4bcc9e4df0644193073e63d9ee48ac9fce79ec1506e4aa4c + languageName: node + linkType: hard + "data-urls@npm:^3.0.2": version: 3.0.2 resolution: "data-urls@npm:3.0.2" @@ -7495,6 +7550,15 @@ __metadata: languageName: node linkType: hard +"debug@npm:^2.2.0": + version: 2.6.9 + resolution: "debug@npm:2.6.9" + dependencies: + ms: "npm:2.0.0" + checksum: 10/e07005f2b40e04f1bd14a3dd20520e9c4f25f60224cb006ce9d6781732c917964e9ec029fc7f1a151083cd929025ad5133814d4dc624a9aaf020effe4914ed14 + languageName: node + linkType: hard + "debug@npm:^3.2.7": version: 3.2.7 resolution: "debug@npm:3.2.7" @@ -8037,6 +8101,39 @@ __metadata: languageName: node linkType: hard +"es5-ext@npm:^0.10.35, es5-ext@npm:^0.10.50, es5-ext@npm:^0.10.62, es5-ext@npm:^0.10.64, es5-ext@npm:~0.10.14": + version: 0.10.64 + resolution: "es5-ext@npm:0.10.64" + dependencies: + es6-iterator: "npm:^2.0.3" + es6-symbol: "npm:^3.1.3" + esniff: "npm:^2.0.1" + next-tick: "npm:^1.1.0" + checksum: 10/0c5d8657708b1695ddc4b06f4e0b9fbdda4d2fe46d037b6bedb49a7d1931e542ec9eecf4824d59e1d357e93229deab014bb4b86485db2d41b1d68e54439689ce + languageName: node + linkType: hard + +"es6-iterator@npm:^2.0.3": + version: 2.0.3 + resolution: "es6-iterator@npm:2.0.3" + dependencies: + d: "npm:1" + es5-ext: "npm:^0.10.35" + es6-symbol: "npm:^3.1.1" + checksum: 10/dbadecf3d0e467692815c2b438dfa99e5a97cbbecf4a58720adcb467a04220e0e36282399ba297911fd472c50ae4158fffba7ed0b7d4273fe322b69d03f9e3a5 + languageName: node + linkType: hard + +"es6-symbol@npm:^3.1.1, es6-symbol@npm:^3.1.3": + version: 3.1.4 + resolution: "es6-symbol@npm:3.1.4" + dependencies: + d: "npm:^1.0.2" + ext: "npm:^1.7.0" + checksum: 10/3743119fe61f89e2f049a6ce52bd82fab5f65d13e2faa72453b73f95c15292c3cb9bdf3747940d504517e675e45fd375554c6b5d35d2bcbefd35f5489ecba546 + languageName: node + linkType: hard + "esbuild@npm:^0.19.3": version: 0.19.5 resolution: "esbuild@npm:0.19.5" @@ -8566,6 +8663,18 @@ __metadata: languageName: node linkType: hard +"esniff@npm:^2.0.1": + version: 2.0.1 + resolution: "esniff@npm:2.0.1" + dependencies: + d: "npm:^1.0.1" + es5-ext: "npm:^0.10.62" + event-emitter: "npm:^0.3.5" + type: "npm:^2.7.2" + checksum: 10/f6a2abd2f8c5fe57c5fcf53e5407c278023313d0f6c3a92688e7122ab9ac233029fd424508a196ae5bc561aa1f67d23f4e2435b1a0d378030f476596129056ac + languageName: node + linkType: hard + "espree@npm:^9.6.0, espree@npm:^9.6.1": version: 9.6.1 resolution: "espree@npm:9.6.1" @@ -8649,6 +8758,16 @@ __metadata: languageName: node linkType: hard +"event-emitter@npm:^0.3.5": + version: 0.3.5 + resolution: "event-emitter@npm:0.3.5" + dependencies: + d: "npm:1" + es5-ext: "npm:~0.10.14" + checksum: 10/a7f5ea80029193f4869782d34ef7eb43baa49cd397013add1953491b24588468efbe7e3cc9eb87d53f33397e7aab690fd74c079ec440bf8b12856f6bdb6e9396 + languageName: node + linkType: hard + "eventemitter3@npm:^4.0.1": version: 4.0.7 resolution: "eventemitter3@npm:4.0.7" @@ -8707,6 +8826,15 @@ __metadata: languageName: node linkType: hard +"ext@npm:^1.7.0": + version: 1.7.0 + resolution: "ext@npm:1.7.0" + dependencies: + type: "npm:^2.7.2" + checksum: 10/666a135980b002df0e75c8ac6c389140cdc59ac953db62770479ee2856d58ce69d2f845e5f2586716350b725400f6945e51e9159573158c39f369984c72dcd84 + languageName: node + linkType: hard + "fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": version: 3.1.3 resolution: "fast-deep-equal@npm:3.1.3" @@ -8766,6 +8894,16 @@ __metadata: languageName: node linkType: hard +"fetch-blob@npm:^3.1.2, fetch-blob@npm:^3.1.4": + version: 3.2.0 + resolution: "fetch-blob@npm:3.2.0" + dependencies: + node-domexception: "npm:^1.0.0" + web-streams-polyfill: "npm:^3.0.3" + checksum: 10/5264ecceb5fdc19eb51d1d0359921f12730941e333019e673e71eb73921146dceabcb0b8f534582be4497312d656508a439ad0f5edeec2b29ab2e10c72a1f86b + languageName: node + linkType: hard + "fflate@npm:~0.6.10": version: 0.6.10 resolution: "fflate@npm:0.6.10" @@ -8868,6 +9006,15 @@ __metadata: languageName: node linkType: hard +"formdata-polyfill@npm:^4.0.10": + version: 4.0.10 + resolution: "formdata-polyfill@npm:4.0.10" + dependencies: + fetch-blob: "npm:^3.1.2" + checksum: 10/9b5001d2edef3c9449ac3f48bd4f8cc92e7d0f2e7c1a5c8ba555ad4e77535cc5cf621fabe49e97f304067037282dd9093b9160a3cb533e46420b446c4e6bc06f + languageName: node + linkType: hard + "fraction.js@npm:^4.3.6": version: 4.3.7 resolution: "fraction.js@npm:4.3.7" @@ -9825,6 +9972,13 @@ __metadata: languageName: node linkType: hard +"is-typedarray@npm:^1.0.0": + version: 1.0.0 + resolution: "is-typedarray@npm:1.0.0" + checksum: 10/4b433bfb0f9026f079f4eb3fbaa4ed2de17c9995c3a0b5c800bec40799b4b2a8b4e051b1ada77749deb9ded4ae52fe2096973f3a93ff83df1a5a7184a669478c + languageName: node + linkType: hard + "is-weakmap@npm:^2.0.1": version: 2.0.1 resolution: "is-weakmap@npm:2.0.1" @@ -11255,6 +11409,13 @@ __metadata: languageName: node linkType: hard +"ms@npm:2.0.0": + version: 2.0.0 + resolution: "ms@npm:2.0.0" + checksum: 10/0e6a22b8b746d2e0b65a430519934fefd41b6db0682e3477c10f60c76e947c4c0ad06f63ffdf1d78d335f83edee8c0aa928aa66a36c7cd95b69b26f468d527f4 + languageName: node + linkType: hard + "ms@npm:2.1.2": version: 2.1.2 resolution: "ms@npm:2.1.2" @@ -11310,6 +11471,13 @@ __metadata: languageName: node linkType: hard +"next-tick@npm:^1.1.0": + version: 1.1.0 + resolution: "next-tick@npm:1.1.0" + checksum: 10/83b5cf36027a53ee6d8b7f9c0782f2ba87f4858d977342bfc3c20c21629290a2111f8374d13a81221179603ffc4364f38374b5655d17b6a8f8a8c77bdea4fe8b + languageName: node + linkType: hard + "ngraph.events@npm:^1.0.0, ngraph.events@npm:^1.2.1": version: 1.2.2 resolution: "ngraph.events@npm:1.2.2" @@ -11351,6 +11519,35 @@ __metadata: languageName: node linkType: hard +"node-domexception@npm:^1.0.0": + version: 1.0.0 + resolution: "node-domexception@npm:1.0.0" + checksum: 10/e332522f242348c511640c25a6fc7da4f30e09e580c70c6b13cb0be83c78c3e71c8d4665af2527e869fc96848924a4316ae7ec9014c091e2156f41739d4fa233 + languageName: node + linkType: hard + +"node-fetch@npm:^3.3.1": + version: 3.3.2 + resolution: "node-fetch@npm:3.3.2" + dependencies: + data-uri-to-buffer: "npm:^4.0.0" + fetch-blob: "npm:^3.1.4" + formdata-polyfill: "npm:^4.0.10" + checksum: 10/24207ca8c81231c7c59151840e3fded461d67a31cf3e3b3968e12201a42f89ce4a0b5fb7079b1fa0a4655957b1ca9257553200f03a9f668b45ebad265ca5593d + languageName: node + linkType: hard + +"node-gyp-build@npm:^4.3.0": + version: 4.8.0 + resolution: "node-gyp-build@npm:4.8.0" + bin: + node-gyp-build: bin.js + node-gyp-build-optional: optional.js + node-gyp-build-test: build-test.js + checksum: 10/80f410ab412df38e84171d3634a5716b6c6f14ecfa4eb971424d289381fb76f8bcbe1b666419ceb2c81060e558fd7c6d70cc0f60832bcca6a1559098925d9657 + languageName: node + linkType: hard + "node-gyp@npm:latest": version: 10.0.1 resolution: "node-gyp@npm:10.0.1" @@ -11417,6 +11614,25 @@ __metadata: languageName: node linkType: hard +"nostr-tools@npm:^1.15.0": + version: 1.17.0 + resolution: "nostr-tools@npm:1.17.0" + dependencies: + "@noble/ciphers": "npm:0.2.0" + "@noble/curves": "npm:1.1.0" + "@noble/hashes": "npm:1.3.1" + "@scure/base": "npm:1.1.1" + "@scure/bip32": "npm:1.3.1" + "@scure/bip39": "npm:1.2.1" + peerDependencies: + typescript: ">=5.0.0" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/c903582f6df9b5a17a02bd2fef5a5bb2ab3e80800d6f6568be8e2c2d75bfc46fc2bcd50f3dd48c775682fe427904099d723141b5bde6578ccf56ff68eb89e3b5 + languageName: node + linkType: hard + "nostr-tools@npm:^2.0.2": version: 2.0.2 resolution: "nostr-tools@npm:2.0.2" @@ -14235,6 +14451,13 @@ __metadata: languageName: node linkType: hard +"tseep@npm:^1.1.1": + version: 1.2.1 + resolution: "tseep@npm:1.2.1" + checksum: 10/36b285d8aa333dc25b1ecb9f22ee751c4342694c2f1e0dba00d4a7c2011d796deb00122050760bf8a951b0c7aecaa2691dfc129a2757e793c15976bb32b28068 + languageName: node + linkType: hard + "tslib@npm:2.6.2, tslib@npm:^2.0.0, tslib@npm:^2.4.0, tslib@npm:^2.5.0, tslib@npm:^2.6.1, tslib@npm:^2.6.2": version: 2.6.2 resolution: "tslib@npm:2.6.2" @@ -14249,6 +14472,13 @@ __metadata: languageName: node linkType: hard +"tstl@npm:^2.0.7": + version: 2.5.16 + resolution: "tstl@npm:2.5.16" + checksum: 10/aaff2582f6963f33f1891e3d06690f6535a3b74ee68f9323f3d791fbf6dbe414abfb562b852c790923322b4e374f9dbbad5d8c98755eba64282e6aa1d735f253 + languageName: node + linkType: hard + "tsutils@npm:^3.21.0": version: 3.21.0 resolution: "tsutils@npm:3.21.0" @@ -14297,6 +14527,13 @@ __metadata: languageName: node linkType: hard +"type@npm:^2.7.2": + version: 2.7.2 + resolution: "type@npm:2.7.2" + checksum: 10/602f1b369fba60687fa4d0af6fcfb814075bcaf9ed3a87637fb384d9ff849e2ad15bc244a431f341374562e51a76c159527ffdb1f1f24b0f1f988f35a301c41d + languageName: node + linkType: hard + "typed-array-buffer@npm:^1.0.0": version: 1.0.0 resolution: "typed-array-buffer@npm:1.0.0" @@ -14396,6 +14633,15 @@ __metadata: languageName: node linkType: hard +"typedarray-to-buffer@npm:^3.1.5": + version: 3.1.5 + resolution: "typedarray-to-buffer@npm:3.1.5" + dependencies: + is-typedarray: "npm:^1.0.0" + checksum: 10/7c850c3433fbdf4d04f04edfc751743b8f577828b8e1eb93b95a3bce782d156e267d83e20fb32b3b47813e69a69ab5e9b5342653332f7d21c7d1210661a7a72c + languageName: node + linkType: hard + "typedoc@npm:^0.25.7": version: 0.25.7 resolution: "typedoc@npm:0.25.7" @@ -14659,6 +14905,23 @@ __metadata: languageName: node linkType: hard +"utf-8-validate@npm:^5.0.2": + version: 5.0.10 + resolution: "utf-8-validate@npm:5.0.10" + dependencies: + node-gyp: "npm:latest" + node-gyp-build: "npm:^4.3.0" + checksum: 10/b89cbc13b4badad04828349ebb7aa2ab1edcb02b46ab12ce0ba5b2d6886d684ad4e93347819e3c8d36224c8742422d2dca69f5cc16c72ae4d7eeecc0c5cb544b + languageName: node + linkType: hard + +"utf8-buffer@npm:^1.0.0": + version: 1.0.0 + resolution: "utf8-buffer@npm:1.0.0" + checksum: 10/7028825ec46347042a9e82ad1189f487f08a562d4f006d66fccd6bc4504fe720f481c2ee351bdca9f8a0f829566efcb3c852ba0f19f6810ad730686ba9d3ae94 + languageName: node + linkType: hard + "util-deprecate@npm:^1.0.2": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2" @@ -14952,6 +15215,13 @@ __metadata: languageName: node linkType: hard +"web-streams-polyfill@npm:^3.0.3": + version: 3.3.3 + resolution: "web-streams-polyfill@npm:3.3.3" + checksum: 10/8e7e13501b3834094a50abe7c0b6456155a55d7571312b89570012ef47ec2a46d766934768c50aabad10a9c30dd764a407623e8bfcc74fcb58495c29130edea9 + languageName: node + linkType: hard + "webcrypto-core@npm:^1.7.7": version: 1.7.7 resolution: "webcrypto-core@npm:1.7.7" @@ -14979,6 +15249,30 @@ __metadata: languageName: node linkType: hard +"websocket-polyfill@npm:^0.0.3": + version: 0.0.3 + resolution: "websocket-polyfill@npm:0.0.3" + dependencies: + tstl: "npm:^2.0.7" + websocket: "npm:^1.0.28" + checksum: 10/c0e385c163978a95e70438fff37ac1534f91211c1f026deeedcbfd174c90db1a0cc5c1b30fe05aaf903210a8355bd6de4c4f6d956bbae36f621641d1f178e09b + languageName: node + linkType: hard + +"websocket@npm:^1.0.28": + version: 1.0.34 + resolution: "websocket@npm:1.0.34" + dependencies: + bufferutil: "npm:^4.0.1" + debug: "npm:^2.2.0" + es5-ext: "npm:^0.10.50" + typedarray-to-buffer: "npm:^3.1.5" + utf-8-validate: "npm:^5.0.2" + yaeti: "npm:^0.0.6" + checksum: 10/b72e3dcc3fa92b4a4511f0df89b25feed6ab06979cb9e522d2736f09855f4bf7588d826773b9405fcf3f05698200eb55ba9da7ef333584653d4912a5d3b13c18 + languageName: node + linkType: hard + "whatwg-encoding@npm:^2.0.0": version: 2.0.0 resolution: "whatwg-encoding@npm:2.0.0" @@ -15426,6 +15720,13 @@ __metadata: languageName: node linkType: hard +"yaeti@npm:^0.0.6": + version: 0.0.6 + resolution: "yaeti@npm:0.0.6" + checksum: 10/6db12c152f7c363b80071086a3ebf5032e03332604eeda988872be50d6c8469e1f13316175544fa320f72edad696c2d83843ad0ff370659045c1a68bcecfcfea + languageName: node + linkType: hard + "yallist@npm:^3.0.2": version: 3.1.1 resolution: "yallist@npm:3.1.1"