feat: NDK (WIP)
continuous-integration/drone/push Build is failing Details

This commit is contained in:
kieran 2024-04-23 13:08:23 +01:00
parent ea54ee2b00
commit eee76e64e5
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
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) => {

323
yarn.lock
View File

@ -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"