@nostr-dev-kit/ndk
Version:
NDK - Nostr Development Kit. Includes AI Guardrails to catch common mistakes during development.
1,487 lines (1,437 loc) • 433 kB
JavaScript
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// test/index.ts
var index_exports = {};
__export(index_exports, {
EventGenerator: () => EventGenerator,
RelayMock: () => RelayMock,
RelayPoolMock: () => RelayPoolMock,
SignerGenerator: () => SignerGenerator,
TestEventFactory: () => TestEventFactory,
TestFixture: () => TestFixture,
TimeController: () => TimeController,
UserGenerator: () => UserGenerator,
mockNutzap: () => mockNutzap,
mockProof: () => mockProof,
withTimeControl: () => withTimeControl
});
module.exports = __toCommonJS(index_exports);
// src/ndk/index.ts
var import_debug12 = __toESM(require("debug"));
var import_nostr_tools10 = require("nostr-tools");
var import_tseep9 = require("tseep");
// src/ai-guardrails/event/signing.ts
function checkMissingKind(event, error) {
if (event.kind === void 0 || event.kind === null) {
error(
"event-missing-kind",
`Cannot sign event without 'kind'.
\u{1F4E6} Event data:
\u2022 content: ${event.content ? `"${event.content.substring(0, 50)}${event.content.length > 50 ? "..." : ""}"` : "(empty)"}
\u2022 tags: ${event.tags.length} tag${event.tags.length !== 1 ? "s" : ""}
\u2022 kind: ${event.kind} \u274C
Set event.kind before signing.`,
"Example: event.kind = 1; // for text note",
false
// Fatal error - cannot be disabled
);
}
}
function checkContentIsObject(event, error) {
if (typeof event.content === "object") {
const contentPreview = JSON.stringify(event.content, null, 2).substring(0, 200);
error(
"event-content-is-object",
`Event content is an object. Content must be a string.
\u{1F4E6} Your content (${typeof event.content}):
${contentPreview}${JSON.stringify(event.content).length > 200 ? "..." : ""}
\u274C event.content = { ... } // WRONG
\u2705 event.content = JSON.stringify({ ... }) // CORRECT`,
"Use JSON.stringify() for structured data: event.content = JSON.stringify(data)",
false
// Fatal error - cannot be disabled
);
}
}
function checkCreatedAtMilliseconds(event, error) {
if (event.created_at && event.created_at > 1e10) {
const correctValue = Math.floor(event.created_at / 1e3);
const dateString = new Date(event.created_at).toISOString();
error(
"event-created-at-milliseconds",
`Event created_at is in milliseconds, not seconds.
\u{1F4E6} Your value:
\u2022 created_at: ${event.created_at} \u274C
\u2022 Interpreted as: ${dateString}
\u2022 Should be: ${correctValue} \u2705
Nostr timestamps MUST be in seconds since Unix epoch.`,
"Use Math.floor(Date.now() / 1000) instead of Date.now()",
false
// Fatal error - cannot be disabled
);
}
}
function checkInvalidPTags(event, error) {
const pTags = event.getMatchingTags("p");
pTags.forEach((tag, idx) => {
if (tag[1] && !/^[0-9a-f]{64}$/i.test(tag[1])) {
const tagPreview = JSON.stringify(tag);
error(
"tag-invalid-p-tag",
`p-tag[${idx}] has invalid pubkey.
\u{1F4E6} Your tag:
${tagPreview}
\u274C Invalid value: "${tag[1]}"
\u2022 Length: ${tag[1].length} (expected 64)
\u2022 Format: ${tag[1].startsWith("npub") ? "bech32 (npub)" : "unknown"}
p-tags MUST contain 64-character hex pubkeys.`,
tag[1].startsWith("npub") ? "Use ndkUser.pubkey instead of npub:\n \u2705 event.tags.push(['p', ndkUser.pubkey])\n \u274C event.tags.push(['p', 'npub1...'])" : "p-tags must contain valid hex pubkeys (64 characters, 0-9a-f)",
false
// Fatal error - cannot be disabled
);
}
});
}
function checkInvalidETags(event, error) {
const eTags = event.getMatchingTags("e");
eTags.forEach((tag, idx) => {
if (tag[1] && !/^[0-9a-f]{64}$/i.test(tag[1])) {
const tagPreview = JSON.stringify(tag);
const isBech32 = tag[1].startsWith("note") || tag[1].startsWith("nevent");
error(
"tag-invalid-e-tag",
`e-tag[${idx}] has invalid event ID.
\u{1F4E6} Your tag:
${tagPreview}
\u274C Invalid value: "${tag[1]}"
\u2022 Length: ${tag[1].length} (expected 64)
\u2022 Format: ${isBech32 ? "bech32 (note/nevent)" : "unknown"}
e-tags MUST contain 64-character hex event IDs.`,
isBech32 ? "Use event.id instead of bech32:\n \u2705 event.tags.push(['e', referencedEvent.id])\n \u274C event.tags.push(['e', 'note1...'])" : "e-tags must contain valid hex event IDs (64 characters, 0-9a-f)",
false
// Fatal error - cannot be disabled
);
}
});
}
function checkManualReplyMarkers(event, warn, replyEvents) {
if (event.kind !== 1) return;
if (replyEvents.has(event)) return;
const eTagsWithMarkers = event.tags.filter((tag) => tag[0] === "e" && (tag[3] === "reply" || tag[3] === "root"));
if (eTagsWithMarkers.length > 0) {
const tagList = eTagsWithMarkers.map((tag, idx) => ` ${idx + 1}. ${JSON.stringify(tag)}`).join("\n");
warn(
"event-manual-reply-markers",
`Event has ${eTagsWithMarkers.length} e-tag(s) with manual reply/root markers.
\u{1F4E6} Your tags with markers:
${tagList}
\u26A0\uFE0F Manual reply markers detected! This will cause incorrect threading.`,
`Reply events MUST be created using .reply():
\u2705 CORRECT:
const replyEvent = originalEvent.reply();
replyEvent.content = 'good point!';
await replyEvent.publish();
\u274C WRONG:
event.tags.push(['e', eventId, '', 'reply']);
NDK handles all reply threading automatically - never add reply/root markers manually.`
);
}
}
function checkHashtagsWithPrefix(event, error) {
const tTags = event.getMatchingTags("t");
tTags.forEach((tag, idx) => {
if (tag[1] && tag[1].startsWith("#")) {
const tagPreview = JSON.stringify(tag);
error(
"tag-hashtag-with-prefix",
`t-tag[${idx}] contains hashtag with # prefix.
\u{1F4E6} Your tag:
${tagPreview}
\u274C Invalid value: "${tag[1]}"
Hashtag tags should NOT include the # symbol.`,
`Remove the # prefix from hashtag tags:
\u2705 event.tags.push(['t', 'nostr'])
\u274C event.tags.push(['t', '#nostr'])`,
false
// Fatal error - cannot be disabled
);
}
});
}
function checkReplaceableWithOldTimestamp(event, warn) {
if (event.kind === void 0 || event.kind === null || !event.created_at) return;
if (!event.isReplaceable()) return;
const nowSeconds = Math.floor(Date.now() / 1e3);
const ageSeconds = nowSeconds - event.created_at;
const TEN_SECONDS = 10;
if (ageSeconds > TEN_SECONDS) {
const ageMinutes = Math.floor(ageSeconds / 60);
const ageDescription = ageMinutes > 0 ? `${ageMinutes} minute${ageMinutes !== 1 ? "s" : ""}` : `${ageSeconds} seconds`;
warn(
"event-replaceable-old-timestamp",
`Publishing a replaceable event with an old created_at timestamp.
\u{1F4E6} Event details:
\u2022 kind: ${event.kind} (replaceable)
\u2022 created_at: ${event.created_at}
\u2022 age: ${ageDescription} old
\u2022 current time: ${nowSeconds}
\u26A0\uFE0F This is wrong and will be rejected by relays.`,
`For replaceable events, use publishReplaceable():
\u2705 CORRECT:
await event.publishReplaceable();
// Automatically updates created_at to now
\u274C WRONG:
await event.publish();
// Uses old created_at`
);
}
}
function signing(event, error, warn, replyEvents) {
checkMissingKind(event, error);
checkContentIsObject(event, error);
checkCreatedAtMilliseconds(event, error);
checkInvalidPTags(event, error);
checkInvalidETags(event, error);
checkHashtagsWithPrefix(event, error);
checkManualReplyMarkers(event, warn, replyEvents);
}
function publishing(event, warn) {
checkReplaceableWithOldTimestamp(event, warn);
}
// src/ai-guardrails/ndk/fetch-events.ts
function isNip33Pattern(filters) {
const filterArray = Array.isArray(filters) ? filters : [filters];
if (filterArray.length !== 1) return false;
const filter = filterArray[0];
return filter.kinds && Array.isArray(filter.kinds) && filter.kinds.length === 1 && filter.authors && Array.isArray(filter.authors) && filter.authors.length === 1 && filter["#d"] && Array.isArray(filter["#d"]) && filter["#d"].length === 1;
}
function isReplaceableEventFilter(filters) {
const filterArray = Array.isArray(filters) ? filters : [filters];
if (filterArray.length === 0) {
return false;
}
return filterArray.every((filter) => {
if (!filter.kinds || !Array.isArray(filter.kinds) || filter.kinds.length === 0) {
return false;
}
if (!filter.authors || !Array.isArray(filter.authors) || filter.authors.length === 0) {
return false;
}
const allKindsReplaceable = filter.kinds.every((kind) => {
return kind === 0 || kind === 3 || kind >= 1e4 && kind <= 19999;
});
return allKindsReplaceable;
});
}
function formatFilter(filter) {
const formatted = JSON.stringify(filter, null, 2);
return formatted.split("\n").map((line, idx) => idx === 0 ? line : ` ${line}`).join("\n");
}
function fetchingEvents(filters, opts, warn, shouldWarnRatio, incrementCount) {
incrementCount();
if (opts?.cacheUsage === "ONLY_CACHE") {
return;
}
const filterArray = Array.isArray(filters) ? filters : [filters];
const formattedFilters = filterArray.map(formatFilter).join("\n\n ---\n\n ");
if (isNip33Pattern(filters)) {
const filter = filterArray[0];
warn(
"fetch-events-usage",
"For fetching a NIP-33 addressable event, use fetchEvent() with the naddr directly.\n\n\u{1F4E6} Your filter:\n " + formattedFilters + `
\u274C BAD: const decoded = nip19.decode(naddr);
const events = await ndk.fetchEvents({
kinds: [decoded.data.kind],
authors: [decoded.data.pubkey],
"#d": [decoded.data.identifier]
});
const event = Array.from(events)[0];
\u2705 GOOD: const event = await ndk.fetchEvent(naddr);
\u2705 GOOD: const event = await ndk.fetchEvent('naddr1...');
fetchEvent() handles naddr decoding automatically and returns the event directly.`
);
} else if (isReplaceableEventFilter(filters)) {
return;
} else {
if (!shouldWarnRatio()) {
return;
}
let filterAnalysis = "";
const hasLimit = filterArray.some((f) => f.limit !== void 0);
const totalKinds = new Set(filterArray.flatMap((f) => f.kinds || [])).size;
const totalAuthors = new Set(filterArray.flatMap((f) => f.authors || [])).size;
if (hasLimit) {
const maxLimit = Math.max(...filterArray.map((f) => f.limit || 0));
filterAnalysis += `
\u2022 Limit: ${maxLimit} event${maxLimit !== 1 ? "s" : ""}`;
}
if (totalKinds > 0) {
filterAnalysis += `
\u2022 Kinds: ${totalKinds} type${totalKinds !== 1 ? "s" : ""}`;
}
if (totalAuthors > 0) {
filterAnalysis += `
\u2022 Authors: ${totalAuthors} author${totalAuthors !== 1 ? "s" : ""}`;
}
warn(
"fetch-events-usage",
"fetchEvents() is a BLOCKING operation that waits for EOSE.\nIn most cases, you should use subscribe() instead.\n\n\u{1F4E6} Your filter" + (filterArray.length > 1 ? "s" : "") + ":\n " + formattedFilters + (filterAnalysis ? "\n\n\u{1F4CA} Filter analysis:" + filterAnalysis : "") + "\n\n \u274C BAD: const events = await ndk.fetchEvents(filter);\n \u2705 GOOD: ndk.subscribe(filter, { onEvent: (e) => ... });\n\nOnly use fetchEvents() when you MUST block until data arrives.",
"For one-time queries, use fetchEvent() instead of fetchEvents() when expecting a single result."
);
}
}
// src/ai-guardrails/types.ts
var GuardrailCheckId = {
// NDK lifecycle
NDK_NO_CACHE: "ndk-no-cache",
// Filter-related
FILTER_BECH32_IN_ARRAY: "filter-bech32-in-array",
FILTER_INVALID_HEX: "filter-invalid-hex",
FILTER_ONLY_LIMIT: "filter-only-limit",
FILTER_LARGE_LIMIT: "filter-large-limit",
FILTER_EMPTY: "filter-empty",
FILTER_SINCE_AFTER_UNTIL: "filter-since-after-until",
FILTER_INVALID_A_TAG: "filter-invalid-a-tag",
FILTER_HASHTAG_WITH_PREFIX: "filter-hashtag-with-prefix",
// fetchEvents anti-pattern
FETCH_EVENTS_USAGE: "fetch-events-usage",
// Event construction
EVENT_MISSING_KIND: "event-missing-kind",
EVENT_PARAM_REPLACEABLE_NO_DTAG: "event-param-replaceable-no-dtag",
EVENT_CREATED_AT_MILLISECONDS: "event-created-at-milliseconds",
EVENT_NO_NDK_INSTANCE: "event-no-ndk-instance",
EVENT_CONTENT_IS_OBJECT: "event-content-is-object",
EVENT_MODIFIED_AFTER_SIGNING: "event-modified-after-signing",
EVENT_MANUAL_REPLY_MARKERS: "event-manual-reply-markers",
// Tag construction
TAG_E_FOR_PARAM_REPLACEABLE: "tag-e-for-param-replaceable",
TAG_BECH32_VALUE: "tag-bech32-value",
TAG_DUPLICATE: "tag-duplicate",
TAG_INVALID_P_TAG: "tag-invalid-p-tag",
TAG_INVALID_E_TAG: "tag-invalid-e-tag",
TAG_HASHTAG_WITH_PREFIX: "tag-hashtag-with-prefix",
// Subscription
SUBSCRIBE_NOT_STARTED: "subscribe-not-started",
SUBSCRIBE_CLOSE_ON_EOSE_NO_HANDLER: "subscribe-close-on-eose-no-handler",
SUBSCRIBE_PASSED_EVENT_NOT_FILTER: "subscribe-passed-event-not-filter",
SUBSCRIBE_AWAITED: "subscribe-awaited",
// Relay
RELAY_INVALID_URL: "relay-invalid-url",
RELAY_HTTP_INSTEAD_OF_WS: "relay-http-instead-of-ws",
RELAY_NO_ERROR_HANDLERS: "relay-no-error-handlers",
// Validation
VALIDATION_PUBKEY_IS_NPUB: "validation-pubkey-is-npub",
VALIDATION_PUBKEY_WRONG_LENGTH: "validation-pubkey-wrong-length",
VALIDATION_EVENT_ID_IS_BECH32: "validation-event-id-is-bech32",
VALIDATION_EVENT_ID_WRONG_LENGTH: "validation-event-id-wrong-length"
};
// src/ai-guardrails/ndk.ts
function checkCachePresence(ndk, shouldCheck) {
if (!shouldCheck(GuardrailCheckId.NDK_NO_CACHE)) return;
setTimeout(() => {
if (!ndk.cacheAdapter) {
const isBrowser = typeof window !== "undefined";
const suggestion = isBrowser ? "Consider using @nostr-dev-kit/ndk-cache-dexie or @nostr-dev-kit/ndk-cache-sqlite-wasm" : "Consider using @nostr-dev-kit/ndk-cache-redis or @nostr-dev-kit/ndk-cache-sqlite";
const message = `
\u{1F916} AI_GUARDRAILS WARNING: NDK initialized without a cache adapter. Apps perform significantly better with caching.
\u{1F4A1} ${suggestion}
\u{1F507} To disable this check:
ndk.aiGuardrails.skip('${GuardrailCheckId.NDK_NO_CACHE}')
or set: ndk.aiGuardrails = { skip: new Set(['${GuardrailCheckId.NDK_NO_CACHE}']) }`;
console.warn(message);
}
}, 2500);
}
// src/ai-guardrails/index.ts
var AIGuardrails = class {
enabled = false;
skipSet = /* @__PURE__ */ new Set();
extensions = /* @__PURE__ */ new Map();
_nextCallDisabled = null;
_replyEvents = /* @__PURE__ */ new WeakSet();
_fetchEventsCount = 0;
_subscribeCount = 0;
constructor(mode = false) {
this.setMode(mode);
}
/**
* Register an extension namespace with custom guardrail hooks.
* This allows external packages to add their own guardrails.
*
* @example
* ```typescript
* // In NDKSvelte package:
* ndk.aiGuardrails.register('ndkSvelte', {
* constructing: (params) => {
* if (!params.session) {
* warn('ndksvelte-no-session', 'NDKSvelte instantiated without session parameter...');
* }
* }
* });
*
* // In NDKSvelte constructor:
* this.ndk.aiGuardrails?.ndkSvelte?.constructing(params);
* ```
*/
register(namespace, hooks) {
if (this.extensions.has(namespace)) {
console.warn(`AIGuardrails: Extension '${namespace}' already registered, overwriting`);
}
const wrappedHooks = {};
for (const [key, fn] of Object.entries(hooks)) {
if (typeof fn === "function") {
wrappedHooks[key] = (...args) => {
if (!this.enabled) return;
fn(...args, this.shouldCheck.bind(this), this.error.bind(this), this.warn.bind(this));
};
}
}
this.extensions.set(namespace, wrappedHooks);
this[namespace] = wrappedHooks;
}
/**
* Set the guardrails mode.
*/
setMode(mode) {
if (typeof mode === "boolean") {
this.enabled = mode;
this.skipSet.clear();
} else if (mode && typeof mode === "object") {
this.enabled = true;
this.skipSet = mode.skip || /* @__PURE__ */ new Set();
}
}
/**
* Check if guardrails are enabled at all.
*/
isEnabled() {
return this.enabled;
}
/**
* Check if a specific guardrail check should run.
*/
shouldCheck(id) {
if (!this.enabled) return false;
if (this.skipSet.has(id)) return false;
if (this._nextCallDisabled === "all") return false;
if (this._nextCallDisabled && this._nextCallDisabled.has(id)) return false;
return true;
}
/**
* Disable a specific guardrail check.
*/
skip(id) {
this.skipSet.add(id);
}
/**
* Re-enable a specific guardrail check.
*/
enable(id) {
this.skipSet.delete(id);
}
/**
* Get all currently skipped guardrails.
*/
getSkipped() {
return Array.from(this.skipSet);
}
/**
* Capture the current _nextCallDisabled set and clear it atomically.
* This is used by hook methods to handle one-time guardrail disabling.
*/
captureAndClearNextCallDisabled() {
const captured = this._nextCallDisabled;
this._nextCallDisabled = null;
return captured;
}
/**
* Increment fetchEvents call counter for ratio tracking.
*/
incrementFetchEventsCount() {
this._fetchEventsCount++;
}
/**
* Increment subscribe call counter for ratio tracking.
*/
incrementSubscribeCount() {
this._subscribeCount++;
}
/**
* Check if fetchEvents usage ratio exceeds the threshold.
* Returns true if more than 50% of calls are fetchEvents AND total calls > 6.
*/
shouldWarnAboutFetchEventsRatio() {
const totalCalls = this._fetchEventsCount + this._subscribeCount;
if (totalCalls <= 6) {
return false;
}
const ratio = this._fetchEventsCount / totalCalls;
return ratio > 0.5;
}
/**
* Throw an error if the check should run.
* Also logs to console.error in case the throw gets swallowed.
* @param canDisable - If false, this is a fatal error that cannot be disabled (default: true)
*/
error(id, message, hint, canDisable = true) {
if (!this.shouldCheck(id)) return;
const fullMessage = this.formatMessage(id, "ERROR", message, hint, canDisable);
console.error(fullMessage);
throw new Error(fullMessage);
}
/**
* Throw a warning if the check should run.
* Also logs to console.error in case the throw gets swallowed.
* Warnings can always be disabled.
*/
warn(id, message, hint) {
if (!this.shouldCheck(id)) return;
const fullMessage = this.formatMessage(id, "WARNING", message, hint, true);
console.error(fullMessage);
throw new Error(fullMessage);
}
/**
* Format a guardrail message with helpful metadata.
*/
formatMessage(id, level, message, hint, canDisable = true) {
let output = `
\u{1F916} AI_GUARDRAILS ${level}: ${message}`;
if (hint) {
output += `
\u{1F4A1} ${hint}`;
}
if (canDisable) {
output += `
\u{1F507} To disable this check:
ndk.guardrailOff('${id}').yourMethod() // For one call`;
output += `
ndk.aiGuardrails.skip('${id}') // Permanently`;
output += `
or set: ndk.aiGuardrails = { skip: new Set(['${id}']) }`;
}
return output;
}
// ============================================================================
// Hook Methods - Type-safe, domain-organized insertion points
// ============================================================================
/**
* Called when NDK instance is created.
* Checks for cache presence and other initialization concerns.
*/
ndkInstantiated(ndk) {
if (!this.enabled) return;
checkCachePresence(ndk, this.shouldCheck.bind(this));
}
/**
* NDK-related guardrails
*/
ndk = {
/**
* Called when fetchEvents is about to be called
*/
fetchingEvents: (filters, opts) => {
if (!this.enabled) return;
fetchingEvents(
filters,
opts,
this.warn.bind(this),
this.shouldWarnAboutFetchEventsRatio.bind(this),
this.incrementFetchEventsCount.bind(this)
);
}
};
/**
* Event-related guardrails
*/
event = {
/**
* Called when an event is about to be signed
*/
signing: (event) => {
if (!this.enabled) return;
signing(event, this.error.bind(this), this.warn.bind(this), this._replyEvents);
},
/**
* Called before an event is published
*/
publishing: (event) => {
if (!this.enabled) return;
publishing(event, this.warn.bind(this));
},
/**
* Called when an event is received from a relay
*/
received: (_event, _relay) => {
if (!this.enabled) return;
},
/**
* Called when a reply event is being created via .reply()
* This allows guardrails to track legitimate reply events
*/
creatingReply: (event) => {
if (!this.enabled) return;
this._replyEvents.add(event);
}
};
/**
* Subscription-related guardrails
*/
subscription = {
/**
* Called when a subscription is created
*/
created: (_filters, _opts) => {
if (!this.enabled) return;
this.incrementSubscribeCount();
}
};
/**
* Relay-related guardrails
*/
relay = {
/**
* Called when a relay connection is established
*/
connected: (_relay) => {
if (!this.enabled) return;
}
};
};
// src/events/dedup.ts
function dedup(event1, event2) {
if (event1.created_at > event2.created_at) {
return event1;
}
return event2;
}
// src/events/index.ts
var import_tseep2 = require("tseep");
// src/relay/sets/calculate.ts
var import_debug3 = __toESM(require("debug"));
// src/outbox/write.ts
function getRelaysForSync(ndk, author, type = "write") {
if (!ndk.outboxTracker) return void 0;
const item = ndk.outboxTracker.data.get(author);
if (!item) return void 0;
if (type === "write") {
return item.writeRelays;
}
return item.readRelays;
}
async function getWriteRelaysFor(ndk, author, type = "write") {
if (!ndk.outboxTracker) return void 0;
if (!ndk.outboxTracker.data.has(author)) {
await ndk.outboxTracker.trackUsers([author]);
}
return getRelaysForSync(ndk, author, type);
}
// src/outbox/relay-ranking.ts
function getTopRelaysForAuthors(ndk, authors) {
const relaysWithCount = /* @__PURE__ */ new Map();
authors.forEach((author) => {
const writeRelays = getRelaysForSync(ndk, author);
if (writeRelays) {
writeRelays.forEach((relay) => {
const count = relaysWithCount.get(relay) || 0;
relaysWithCount.set(relay, count + 1);
});
}
});
const sortedRelays = Array.from(relaysWithCount.entries()).sort((a, b) => b[1] - a[1]);
return sortedRelays.map((entry) => entry[0]);
}
// src/outbox/index.ts
function getAllRelaysForAllPubkeys(ndk, pubkeys, type = "read") {
const pubkeysToRelays = /* @__PURE__ */ new Map();
const authorsMissingRelays = /* @__PURE__ */ new Set();
pubkeys.forEach((pubkey) => {
const relays = getRelaysForSync(ndk, pubkey, type);
if (relays && relays.size > 0) {
relays.forEach((relay) => {
const pubkeysInRelay = pubkeysToRelays.get(relay) || /* @__PURE__ */ new Set();
pubkeysInRelay.add(pubkey);
});
pubkeysToRelays.set(pubkey, relays);
} else {
authorsMissingRelays.add(pubkey);
}
});
return { pubkeysToRelays, authorsMissingRelays };
}
function chooseRelayCombinationForPubkeys(ndk, pubkeys, type, { count, preferredRelays } = {}) {
count ??= 2;
preferredRelays ??= /* @__PURE__ */ new Set();
const pool = ndk.pool;
const connectedRelays = pool.connectedRelays();
connectedRelays.forEach((relay) => {
preferredRelays?.add(relay.url);
});
const relayToAuthorsMap = /* @__PURE__ */ new Map();
const { pubkeysToRelays, authorsMissingRelays } = getAllRelaysForAllPubkeys(ndk, pubkeys, type);
const sortedRelays = getTopRelaysForAuthors(ndk, pubkeys);
const addAuthorToRelay = (author, relay) => {
const authorsInRelay = relayToAuthorsMap.get(relay) || [];
authorsInRelay.push(author);
relayToAuthorsMap.set(relay, authorsInRelay);
};
for (const [author, authorRelays] of pubkeysToRelays.entries()) {
let missingRelayCount = count;
const addedRelaysForAuthor = /* @__PURE__ */ new Set();
for (const relay of connectedRelays) {
if (authorRelays.has(relay.url)) {
addAuthorToRelay(author, relay.url);
addedRelaysForAuthor.add(relay.url);
missingRelayCount--;
}
}
for (const authorRelay of authorRelays) {
if (addedRelaysForAuthor.has(authorRelay)) continue;
if (relayToAuthorsMap.has(authorRelay)) {
addAuthorToRelay(author, authorRelay);
addedRelaysForAuthor.add(authorRelay);
missingRelayCount--;
}
}
if (missingRelayCount <= 0) continue;
for (const relay of sortedRelays) {
if (missingRelayCount <= 0) break;
if (addedRelaysForAuthor.has(relay)) continue;
if (authorRelays.has(relay)) {
addAuthorToRelay(author, relay);
addedRelaysForAuthor.add(relay);
missingRelayCount--;
}
}
}
for (const author of authorsMissingRelays) {
pool.permanentAndConnectedRelays().forEach((relay) => {
const authorsInRelay = relayToAuthorsMap.get(relay.url) || [];
authorsInRelay.push(author);
relayToAuthorsMap.set(relay.url, authorsInRelay);
});
}
return relayToAuthorsMap;
}
// src/outbox/read/with-authors.ts
function getRelaysForFilterWithAuthors(ndk, authors, relayGoalPerAuthor = 2) {
return chooseRelayCombinationForPubkeys(ndk, authors, "write", { count: relayGoalPerAuthor });
}
// src/utils/normalize-url.ts
function tryNormalizeRelayUrl(url) {
try {
return normalizeRelayUrl(url);
} catch {
return void 0;
}
}
function normalizeRelayUrl(url) {
let r = normalizeUrl(url, {
stripAuthentication: false,
stripWWW: false,
stripHash: true
});
if (!r.endsWith("/")) {
r += "/";
}
return r;
}
function normalize(urls) {
const normalized = /* @__PURE__ */ new Set();
for (const url of urls) {
try {
normalized.add(normalizeRelayUrl(url));
} catch {
}
}
return Array.from(normalized);
}
var DATA_URL_DEFAULT_MIME_TYPE = "text/plain";
var DATA_URL_DEFAULT_CHARSET = "us-ascii";
var testParameter = (name, filters) => filters.some((filter) => filter instanceof RegExp ? filter.test(name) : filter === name);
var supportedProtocols = /* @__PURE__ */ new Set(["https:", "http:", "file:"]);
var hasCustomProtocol = (urlString) => {
try {
const { protocol } = new URL(urlString);
return protocol.endsWith(":") && !protocol.includes(".") && !supportedProtocols.has(protocol);
} catch {
return false;
}
};
var normalizeDataURL = (urlString, { stripHash }) => {
const match = /^data:(?<type>[^,]*?),(?<data>[^#]*?)(?:#(?<hash>.*))?$/.exec(urlString);
if (!match) {
throw new Error(`Invalid URL: ${urlString}`);
}
const type = match.groups?.type ?? "";
const data = match.groups?.data ?? "";
let hash = match.groups?.hash ?? "";
const mediaType = type.split(";");
hash = stripHash ? "" : hash;
let isBase64 = false;
if (mediaType[mediaType.length - 1] === "base64") {
mediaType.pop();
isBase64 = true;
}
const mimeType = mediaType.shift()?.toLowerCase() ?? "";
const attributes = mediaType.map((attribute) => {
let [key, value = ""] = attribute.split("=").map((string) => string.trim());
if (key === "charset") {
value = value.toLowerCase();
if (value === DATA_URL_DEFAULT_CHARSET) {
return "";
}
}
return `${key}${value ? `=${value}` : ""}`;
}).filter(Boolean);
const normalizedMediaType = [...attributes];
if (isBase64) {
normalizedMediaType.push("base64");
}
if (normalizedMediaType.length > 0 || mimeType && mimeType !== DATA_URL_DEFAULT_MIME_TYPE) {
normalizedMediaType.unshift(mimeType);
}
return `data:${normalizedMediaType.join(";")},${isBase64 ? data.trim() : data}${hash ? `#${hash}` : ""}`;
};
function normalizeUrl(urlString, options = {}) {
options = {
defaultProtocol: "http",
normalizeProtocol: true,
forceHttp: false,
forceHttps: false,
stripAuthentication: true,
stripHash: false,
stripTextFragment: true,
stripWWW: true,
removeQueryParameters: [/^utm_\w+/i],
removeTrailingSlash: true,
removeSingleSlash: true,
removeDirectoryIndex: false,
removeExplicitPort: false,
sortQueryParameters: true,
...options
};
if (typeof options.defaultProtocol === "string" && !options.defaultProtocol.endsWith(":")) {
options.defaultProtocol = `${options.defaultProtocol}:`;
}
urlString = urlString.trim();
if (/^data:/i.test(urlString)) {
return normalizeDataURL(urlString, options);
}
if (hasCustomProtocol(urlString)) {
return urlString;
}
const hasRelativeProtocol = urlString.startsWith("//");
const isRelativeUrl = !hasRelativeProtocol && /^\.*\//.test(urlString);
if (!isRelativeUrl) {
urlString = urlString.replace(/^(?!(?:\w+:)?\/\/)|^\/\//, options.defaultProtocol);
}
const urlObject = new URL(urlString);
urlObject.hostname = urlObject.hostname.toLowerCase();
if (options.forceHttp && options.forceHttps) {
throw new Error("The `forceHttp` and `forceHttps` options cannot be used together");
}
if (options.forceHttp && urlObject.protocol === "https:") {
urlObject.protocol = "http:";
}
if (options.forceHttps && urlObject.protocol === "http:") {
urlObject.protocol = "https:";
}
if (options.stripAuthentication) {
urlObject.username = "";
urlObject.password = "";
}
if (options.stripHash) {
urlObject.hash = "";
} else if (options.stripTextFragment) {
urlObject.hash = urlObject.hash.replace(/#?:~:text.*?$/i, "");
}
if (urlObject.pathname) {
const protocolRegex = /\b[a-z][a-z\d+\-.]{1,50}:\/\//g;
let lastIndex = 0;
let result = "";
for (; ; ) {
const match = protocolRegex.exec(urlObject.pathname);
if (!match) {
break;
}
const protocol = match[0];
const protocolAtIndex = match.index;
const intermediate = urlObject.pathname.slice(lastIndex, protocolAtIndex);
result += intermediate.replace(/\/{2,}/g, "/");
result += protocol;
lastIndex = protocolAtIndex + protocol.length;
}
const remnant = urlObject.pathname.slice(lastIndex, urlObject.pathname.length);
result += remnant.replace(/\/{2,}/g, "/");
urlObject.pathname = result;
}
if (urlObject.pathname) {
try {
urlObject.pathname = decodeURI(urlObject.pathname);
} catch {
}
}
if (options.removeDirectoryIndex === true) {
options.removeDirectoryIndex = [/^index\.[a-z]+$/];
}
if (Array.isArray(options.removeDirectoryIndex) && options.removeDirectoryIndex.length > 0) {
let pathComponents = urlObject.pathname.split("/");
const lastComponent = pathComponents[pathComponents.length - 1];
if (testParameter(lastComponent, options.removeDirectoryIndex)) {
pathComponents = pathComponents.slice(0, -1);
urlObject.pathname = `${pathComponents.slice(1).join("/")}/`;
}
}
if (urlObject.hostname) {
urlObject.hostname = urlObject.hostname.replace(/\.$/, "");
if (options.stripWWW && /^www\.(?!www\.)[a-z\-\d]{1,63}\.[a-z.\-\d]{2,63}$/.test(urlObject.hostname)) {
urlObject.hostname = urlObject.hostname.replace(/^www\./, "");
}
}
if (Array.isArray(options.removeQueryParameters)) {
for (const key of [...urlObject.searchParams.keys()]) {
if (testParameter(key, options.removeQueryParameters)) {
urlObject.searchParams.delete(key);
}
}
}
if (!Array.isArray(options.keepQueryParameters) && options.removeQueryParameters === true) {
urlObject.search = "";
}
if (Array.isArray(options.keepQueryParameters) && options.keepQueryParameters.length > 0) {
for (const key of [...urlObject.searchParams.keys()]) {
if (!testParameter(key, options.keepQueryParameters)) {
urlObject.searchParams.delete(key);
}
}
}
if (options.sortQueryParameters) {
urlObject.searchParams.sort();
try {
urlObject.search = decodeURIComponent(urlObject.search);
} catch {
}
}
if (options.removeTrailingSlash) {
urlObject.pathname = urlObject.pathname.replace(/\/$/, "");
}
if (options.removeExplicitPort && urlObject.port) {
urlObject.port = "";
}
const oldUrlString = urlString;
urlString = urlObject.toString();
if (!options.removeSingleSlash && urlObject.pathname === "/" && !oldUrlString.endsWith("/") && urlObject.hash === "") {
urlString = urlString.replace(/\/$/, "");
}
if ((options.removeTrailingSlash || urlObject.pathname === "/") && urlObject.hash === "" && options.removeSingleSlash) {
urlString = urlString.replace(/\/$/, "");
}
if (hasRelativeProtocol && !options.normalizeProtocol) {
urlString = urlString.replace(/^http:\/\//, "//");
}
if (options.stripProtocol) {
urlString = urlString.replace(/^(?:https?:)?\/\//, "");
}
return urlString;
}
// src/count/index.ts
var HLL_REGISTER_COUNT = 256;
var NDKCountHll = class _NDKCountHll {
/**
* The 256 uint8 registers used for HLL estimation
*/
registers;
constructor(registers) {
if (registers) {
if (registers.length !== HLL_REGISTER_COUNT) {
throw new Error(`HLL must have exactly ${HLL_REGISTER_COUNT} registers, got ${registers.length}`);
}
this.registers = registers;
} else {
this.registers = new Uint8Array(HLL_REGISTER_COUNT);
}
}
/**
* Creates an NDKCountHll from a hex-encoded string (512 characters).
* Each register is a uint8 value encoded as 2 hex characters.
*
* @param hex - The hex string (512 characters = 256 bytes)
* @returns A new NDKCountHll instance
* @throws Error if the hex string is invalid
*/
static fromHex(hex) {
if (hex.length !== HLL_REGISTER_COUNT * 2) {
throw new Error(`HLL hex string must be ${HLL_REGISTER_COUNT * 2} characters, got ${hex.length}`);
}
const registers = new Uint8Array(HLL_REGISTER_COUNT);
for (let i = 0; i < HLL_REGISTER_COUNT; i++) {
registers[i] = parseInt(hex.substring(i * 2, i * 2 + 2), 16);
}
return new _NDKCountHll(registers);
}
/**
* Converts the HLL registers to a hex-encoded string.
*
* @returns The hex string representation (512 characters)
*/
toHex() {
return Array.from(this.registers).map((v) => v.toString(16).padStart(2, "0")).join("");
}
/**
* Merges this HLL with another HLL by taking the maximum value for each register.
* This is the standard HLL merge operation that allows combining counts
* from multiple relays without double-counting.
*
* @param other - The other HLL to merge with
* @returns A new NDKCountHll with the merged registers
*/
merge(other) {
const merged = new Uint8Array(HLL_REGISTER_COUNT);
for (let i = 0; i < HLL_REGISTER_COUNT; i++) {
merged[i] = Math.max(this.registers[i], other.registers[i]);
}
return new _NDKCountHll(merged);
}
/**
* Merges multiple HLLs by taking the maximum value for each register.
*
* @param hlls - Array of HLLs to merge
* @returns A new NDKCountHll with the merged registers
*/
static merge(hlls) {
if (hlls.length === 0) {
return new _NDKCountHll();
}
const merged = new Uint8Array(HLL_REGISTER_COUNT);
for (let i = 0; i < HLL_REGISTER_COUNT; i++) {
merged[i] = Math.max(...hlls.map((hll) => hll.registers[i]));
}
return new _NDKCountHll(merged);
}
/**
* Estimates the cardinality (unique count) using the HyperLogLog algorithm.
*
* Uses the standard HLL formula with bias correction for small and large cardinalities.
*
* @returns The estimated unique count
*/
estimate() {
const m = HLL_REGISTER_COUNT;
const alpha = 0.7213 / (1 + 1.079 / m);
let sum = 0;
let zeros = 0;
for (let i = 0; i < m; i++) {
sum += Math.pow(2, -this.registers[i]);
if (this.registers[i] === 0) {
zeros++;
}
}
let estimate = alpha * m * m / sum;
if (estimate <= 2.5 * m && zeros > 0) {
estimate = m * Math.log(m / zeros);
}
return Math.round(estimate);
}
/**
* Checks if this HLL is empty (all registers are zero).
*
* @returns True if all registers are zero
*/
isEmpty() {
return this.registers.every((v) => v === 0);
}
/**
* Creates a copy of this HLL.
*
* @returns A new NDKCountHll with the same register values
*/
clone() {
return new _NDKCountHll(new Uint8Array(this.registers));
}
};
// src/relay/index.ts
var import_debug2 = __toESM(require("debug"));
var import_tseep = require("tseep");
// src/relay/keepalive.ts
var NDKRelayKeepalive = class {
/**
* @param timeout - Time in milliseconds to wait before considering connection stale (default 30s)
* @param onSilenceDetected - Callback when silence is detected
*/
constructor(timeout = 3e4, onSilenceDetected) {
this.onSilenceDetected = onSilenceDetected;
this.timeout = timeout;
}
lastActivity = Date.now();
timer;
timeout;
isRunning = false;
/**
* Records activity from the relay, resetting the silence timer
*/
recordActivity() {
this.lastActivity = Date.now();
if (this.isRunning) {
this.resetTimer();
}
}
/**
* Starts monitoring for relay silence
*/
start() {
if (this.isRunning) return;
this.isRunning = true;
this.lastActivity = Date.now();
this.resetTimer();
}
/**
* Stops monitoring for relay silence
*/
stop() {
this.isRunning = false;
if (this.timer) {
clearTimeout(this.timer);
this.timer = void 0;
}
}
resetTimer() {
if (this.timer) {
clearTimeout(this.timer);
}
this.timer = setTimeout(() => {
const silenceTime = Date.now() - this.lastActivity;
if (silenceTime >= this.timeout) {
this.onSilenceDetected();
} else {
const remainingTime = this.timeout - silenceTime;
this.timer = setTimeout(() => {
this.onSilenceDetected();
}, remainingTime);
}
}, this.timeout);
}
};
async function probeRelayConnection(relay) {
const probeId = `probe-${Math.random().toString(36).substring(7)}`;
return new Promise((resolve) => {
let responded = false;
const timeout = setTimeout(() => {
if (!responded) {
responded = true;
relay.send(["CLOSE", probeId]);
resolve(false);
}
}, 5e3);
const handler = () => {
if (!responded) {
responded = true;
clearTimeout(timeout);
relay.send(["CLOSE", probeId]);
resolve(true);
}
};
relay.once("message", handler);
relay.send([
"REQ",
probeId,
{
kinds: [99999],
limit: 0
}
]);
});
}
// src/relay/connectivity.ts
var FLAPPING_THRESHOLD_MS = 1e3;
var NDKRelayConnectivity = class {
ndkRelay;
ws;
_status;
timeoutMs;
connectedAt;
_connectionStats = {
attempts: 0,
success: 0,
durations: []
};
debug;
netDebug;
connectTimeout;
reconnectTimeout;
ndk;
openSubs = /* @__PURE__ */ new Map();
openCountRequests = /* @__PURE__ */ new Map();
openEventPublishes = /* @__PURE__ */ new Map();
pendingAuthPublishes = /* @__PURE__ */ new Map();
serial = 0;
baseEoseTimeout = 4400;
// Keepalive and monitoring
keepalive;
wsStateMonitor;
sleepDetector;
lastSleepCheck = Date.now();
lastMessageSent = Date.now();
wasIdle = false;
constructor(ndkRelay, ndk) {
this.ndkRelay = ndkRelay;
this._status = 1 /* DISCONNECTED */;
const rand = Math.floor(Math.random() * 1e3);
this.debug = this.ndkRelay.debug.extend(`connectivity${rand}`);
this.ndk = ndk;
this.setupMonitoring();
}
/**
* Sets up keepalive, WebSocket state monitoring, and sleep detection
*/
setupMonitoring() {
this.keepalive = new NDKRelayKeepalive(12e4, async () => {
this.debug("Relay silence detected, probing connection");
const isAlive = await probeRelayConnection({
send: (msg) => this.send(JSON.stringify(msg)),
once: (event, handler) => {
const messageHandler = (e) => {
try {
const data = JSON.parse(e.data);
if (data[0] === "EOSE" || data[0] === "EVENT" || data[0] === "NOTICE") {
handler();
this.ws?.removeEventListener("message", messageHandler);
}
} catch {
}
};
this.ws?.addEventListener("message", messageHandler);
}
});
if (!isAlive) {
this.debug("Probe failed, connection is stale");
this.handleStaleConnection();
}
});
this.wsStateMonitor = setInterval(() => {
if (this._status === 5 /* CONNECTED */) {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
this.debug("WebSocket died silently, reconnecting");
this.handleStaleConnection();
}
}
}, 5e3);
this.sleepDetector = setInterval(() => {
const now = Date.now();
const elapsed = now - this.lastSleepCheck;
if (elapsed > 15e3) {
this.debug(`Detected possible sleep/wake (${elapsed}ms gap)`);
this.handlePossibleWake();
}
this.lastSleepCheck = now;
}, 1e4);
}
/**
* Handles detection of a stale connection by cleaning up and triggering reconnection.
*/
handleStaleConnection() {
this.wasIdle = true;
this.keepalive?.stop();
if (this.ws) {
try {
this.ws.close();
} catch (e) {
}
this.ws = void 0;
}
this._status = 1 /* DISCONNECTED */;
this.ndkRelay.emit("disconnect");
this.handleReconnection();
}
/**
* Handles possible system wake event
*/
handlePossibleWake() {
this.debug("System wake detected, checking all connections");
this.wasIdle = true;
if (this._status >= 5 /* CONNECTED */) {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
this.handleStaleConnection();
} else {
probeRelayConnection({
send: (msg) => this.send(JSON.stringify(msg)),
once: (event, handler) => {
const messageHandler = (e) => {
try {
const data = JSON.parse(e.data);
if (data[0] === "EOSE" || data[0] === "EVENT" || data[0] === "NOTICE") {
handler();
this.ws?.removeEventListener("message", messageHandler);
}
} catch {
}
};
this.ws?.addEventListener("message", messageHandler);
}
}).then((isAlive) => {
if (!isAlive) {
this.handleStaleConnection();
}
});
}
}
}
/**
* Resets the reconnection state for system-wide events
* Used by NDKPool when detecting system sleep/wake
*/
resetReconnectionState() {
this.wasIdle = true;
if (this.reconnectTimeout) {
clearTimeout(this.reconnectTimeout);
this.reconnectTimeout = void 0;
}
}
/**
* Connects to the NDK relay and handles the connection lifecycle.
*
* This method attempts to establish a WebSocket connection to the NDK relay specified in the `ndkRelay` object.
* If the connection is successful, it updates the connection statistics, sets the connection status to `CONNECTED`,
* and emits `connect` and `ready` events on the `ndkRelay` object.
*
* If the connection attempt fails, it handles the error by either initiating a reconnection attempt or emitting a
* `delayed-connect` event on the `ndkRelay` object, depending on the `reconnect` parameter.
*
* @param timeoutMs - The timeout in milliseconds for the connection attempt. If not provided, the default timeout from the `ndkRelay` object is used.
* @param reconnect - Indicates whether a reconnection should be attempted if the connection fails. Defaults to `true`.
* @returns A Promise that resolves when the connection is established, or rejects if the connection fails.
*/
async connect(timeoutMs, reconnect = true) {
if (this.ws && this.ws.readyState !== WebSocket.OPEN && this.ws.readyState !== WebSocket.CONNECTING) {
this.debug("Cleaning up stale WebSocket connection");
try {
this.ws.close();
} catch (e) {
}
this.ws = void 0;
this._status = 1 /* DISCONNECTED */;
}
if (this._status !== 2 /* RECONNECTING */ && this._status !== 1 /* DISCONNECTED */ || this.reconnectTimeout) {
this.debug(
"Relay requested to be connected but was in state %s or it had a reconnect timeout",
this._status
);
return;
}
if (this.reconnectTimeout) {
clearTimeout(this.reconnectTimeout);
this.reconnectTimeout = void 0;
}
if (this.connectTimeout) {
clearTimeout(this.connectTimeout);
this.connectTimeout = void 0;
}
timeoutMs ??= this.timeoutMs;
if (!this.timeoutMs && timeoutMs) this.timeoutMs = timeoutMs;
if (this.timeoutMs) this.connectTimeout = setTimeout(() => this.onConnectionError(reconnect), this.timeoutMs);
try {
this.updateConnectionStats.attempt();
if (this._status === 1 /* DISCONNECTED */) this._status = 4 /* CONNECTING */;
else this._status = 2 /* RECONNECTING */;
this.ws = new WebSocket(this.ndkRelay.url);
this.ws.onopen = this.onConnect.bind(this);
this.ws.onclose = this.onDisconnect.bind(this);
this.ws.onmessage = this.onMessage.bind(this);
this.ws.onerror = this.onError.bind(this);
} catch (e) {
this.debug(`Failed to connect to ${this.ndkRelay.url}`, e);
this._status = 1 /* DISCONNECTED */;
if (reconnect) this.handleReconnection();
else this.ndkRelay.emit("delayed-connect", 2 * 24 * 60 * 60 * 1e3);
throw e;
}
}
/**
* Disconnects the WebSocket connection to the NDK relay.
* This method sets the connection status to `NDKRelayStatus.DISCONNECTING`,
* attempts to close the WebSocket connection, and sets the status to
* `NDKRelayStatus.DISCONNECTED` if the disconnect operation fails.
*/
disconnect() {
this._status = 0 /* DISCONNECTING */;
this.keepalive?.stop();
if (this.wsStateMonitor) {
clearInterval(this.wsStateMonitor);
this.wsStateMonitor = void 0;
}
if (this.sleepDetector) {
clearInterval(this.sleepDetector);
this.sleepDetector = void 0;
}
try {
this.ws?.close();
} catch (e) {
this.debug("Failed to disconnect", e);
this._status = 1 /* DISCONNECTED */;
}
}
/**
* Handles the error that occurred when attempting to connect to the NDK relay.
* If `reconnect` is `true`, this method will initiate a reconnection attempt.
* Otherwise, it will emit a `delayed-connect` event on the `ndkRelay` object,
* indicating that a reconnection should be attempted after a delay.
*
* @param reconnect - Indicates whether a reconnection should be attempted.
*/
onConnectionError(reconnect) {
this.debug(`Error connecting to ${this.ndkRelay.url}`, this.timeoutMs);
if (reconnect && !this.reconnectTimeout) {
this.handleReconnection();
}
}
/**
* Handles the connection event when the WebSocket connection is established.
* This method is called when the WebSocket connection is successfully opened.
* It clears any existing connection and reconnection timeouts, updates the connection statistics,
* sets the connection status to `CONNECTED`, and emits `connect` and `ready` events on the `ndkRelay` object.
*/
onConnect() {
this.netDebug?.("connected", this.ndkRelay);
if (this.reconnectTimeout) {
clearTimeout(this.reconnectTimeout);
this.reconnectTimeout = void 0;
}
if (this.connectTimeout) {
clearTimeout(this.connectTimeout);
this.connectTimeout = void 0;
}
this.updateConnectionStats.connected();
this._status = 5 /* CONNECTED */;
this.keepalive?.start();
this.wasIdle = false;
this.ndkRelay.emit("connect");
this.ndkRelay.emit("ready");
}
/**
* Handles the disconnection event when the WebSocket conne