@nostr-dev-kit/ndk
Version:
NDK - Nostr Development Kit
1,290 lines (1,277 loc) • 378 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 __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);
// src/index.ts
var index_exports = {};
__export(index_exports, {
BECH32_REGEX: () => BECH32_REGEX,
NDKAppHandlerEvent: () => NDKAppHandlerEvent,
NDKAppSettings: () => NDKAppSettings,
NDKArticle: () => NDKArticle,
NDKBlossomList: () => NDKBlossomList,
NDKCashuMintList: () => NDKCashuMintList,
NDKCashuToken: () => NDKCashuToken,
NDKCashuWalletTx: () => NDKCashuWalletTx,
NDKClassified: () => NDKClassified,
NDKDVMJobFeedback: () => NDKDVMJobFeedback,
NDKDVMJobResult: () => NDKDVMJobResult,
NDKDVMRequest: () => NDKDVMRequest,
NDKDraft: () => NDKDraft,
NDKDvmJobFeedbackStatus: () => NDKDvmJobFeedbackStatus,
NDKEvent: () => NDKEvent,
NDKFollowPack: () => NDKFollowPack,
NDKHighlight: () => NDKHighlight,
NDKImage: () => NDKImage,
NDKKind: () => NDKKind,
NDKList: () => NDKList,
NDKListKinds: () => NDKListKinds,
NDKNip07Signer: () => NDKNip07Signer,
NDKNip46Backend: () => NDKNip46Backend,
NDKNip46Signer: () => NDKNip46Signer,
NDKNostrRpc: () => NDKNostrRpc,
NDKNutzap: () => NDKNutzap,
NDKPool: () => NDKPool,
NDKPrivateKeySigner: () => NDKPrivateKeySigner,
NDKProject: () => NDKProject,
NDKProjectTemplate: () => NDKProjectTemplate,
NDKPublishError: () => NDKPublishError,
NDKRelay: () => NDKRelay,
NDKRelayAuthPolicies: () => NDKRelayAuthPolicies,
NDKRelayList: () => NDKRelayList,
NDKRelaySet: () => NDKRelaySet,
NDKRelayStatus: () => NDKRelayStatus,
NDKRepost: () => NDKRepost,
NDKSimpleGroup: () => NDKSimpleGroup,
NDKSimpleGroupMemberList: () => NDKSimpleGroupMemberList,
NDKSimpleGroupMetadata: () => NDKSimpleGroupMetadata,
NDKStory: () => NDKStory,
NDKStorySticker: () => NDKStorySticker,
NDKStoryStickerType: () => NDKStoryStickerType,
NDKSubscription: () => NDKSubscription,
NDKSubscriptionCacheUsage: () => NDKSubscriptionCacheUsage,
NDKSubscriptionReceipt: () => NDKSubscriptionReceipt,
NDKSubscriptionStart: () => NDKSubscriptionStart,
NDKSubscriptionTier: () => NDKSubscriptionTier,
NDKTask: () => NDKTask,
NDKThread: () => NDKThread,
NDKTranscriptionDVM: () => NDKTranscriptionDVM,
NDKUser: () => NDKUser,
NDKVideo: () => NDKVideo,
NDKWiki: () => NDKWiki,
NDKWikiMergeRequest: () => NDKWikiMergeRequest,
NDKZapper: () => NDKZapper,
NIP33_A_REGEX: () => NIP33_A_REGEX,
NdkNutzapStatus: () => NdkNutzapStatus,
SignatureVerificationStats: () => SignatureVerificationStats,
assertSignedEvent: () => assertSignedEvent,
calculateRelaySetFromEvent: () => calculateRelaySetFromEvent,
calculateTermDurationInSeconds: () => calculateTermDurationInSeconds,
cashuPubkeyToNostrPubkey: () => cashuPubkeyToNostrPubkey,
compareFilter: () => compareFilter,
createSignedEvent: () => createSignedEvent,
default: () => NDK,
defaultOpts: () => defaultOpts,
deserialize: () => deserialize,
dvmSchedule: () => dvmSchedule,
eventHasETagMarkers: () => eventHasETagMarkers,
eventIsPartOfThread: () => eventIsPartOfThread,
eventIsReply: () => eventIsReply,
eventReplies: () => eventReplies,
eventThreadIds: () => eventThreadIds,
eventThreads: () => eventThreads,
eventsBySameAuthor: () => eventsBySameAuthor,
filterAndRelaySetFromBech32: () => filterAndRelaySetFromBech32,
filterFingerprint: () => filterFingerprint,
filterForEventsTaggingId: () => filterForEventsTaggingId,
filterFromId: () => filterFromId,
generateContentTags: () => generateContentTags,
generateHashtags: () => generateHashtags,
generateSubId: () => generateSubId,
generateZapRequest: () => generateZapRequest,
getEventReplyId: () => getEventReplyId,
getNip57ZapSpecFromLud: () => getNip57ZapSpecFromLud,
getRegisteredEventClasses: () => getRegisteredEventClasses,
getRelayListForUser: () => getRelayListForUser,
getRelayListForUsers: () => getRelayListForUsers,
getReplyTag: () => getReplyTag,
getRootEventId: () => getRootEventId,
getRootTag: () => getRootTag,
giftUnwrap: () => giftUnwrap,
giftWrap: () => giftWrap,
imetaTagToTag: () => imetaTagToTag,
isEventOriginalPost: () => isEventOriginalPost,
isNip33AValue: () => isNip33AValue,
isSignedEvent: () => isSignedEvent,
isUnsignedEvent: () => isUnsignedEvent,
mapImetaTag: () => mapImetaTag,
matchFilter: () => matchFilter,
mergeFilters: () => mergeFilters,
mergeTags: () => mergeTags,
ndkSignerFromPayload: () => ndkSignerFromPayload,
newAmount: () => newAmount,
normalize: () => normalize,
normalizeRelayUrl: () => normalizeRelayUrl,
normalizeUrl: () => normalizeUrl,
parseTagToSubscriptionAmount: () => parseTagToSubscriptionAmount,
pinEvent: () => pinEvent,
possibleIntervalFrequencies: () => possibleIntervalFrequencies,
profileFromEvent: () => profileFromEvent,
proofP2pk: () => proofP2pk,
proofP2pkNostr: () => proofP2pkNostr,
proofsTotalBalance: () => proofsTotalBalance,
queryFullyFilled: () => queryFullyFilled,
registerEventClass: () => registerEventClass,
registerSigner: () => registerSigner,
relayListFromKind3: () => relayListFromKind3,
relaysFromBech32: () => relaysFromBech32,
serialize: () => serialize,
serializeProfile: () => serializeProfile,
startSignatureVerificationStats: () => startSignatureVerificationStats,
strToDimension: () => strToDimension,
strToPosition: () => strToPosition,
tryNormalizeRelayUrl: () => tryNormalizeRelayUrl,
uniqueTag: () => uniqueTag,
unregisterEventClass: () => unregisterEventClass,
wrapEvent: () => wrapEvent,
zapInvoiceFromEvent: () => zapInvoiceFromEvent
});
module.exports = __toCommonJS(index_exports);
// src/types.ts
var NdkNutzapStatus = /* @__PURE__ */ ((NdkNutzapStatus2) => {
NdkNutzapStatus2["INITIAL"] = "initial";
NdkNutzapStatus2["PROCESSING"] = "processing";
NdkNutzapStatus2["REDEEMED"] = "redeemed";
NdkNutzapStatus2["SPENT"] = "spent";
NdkNutzapStatus2["MISSING_PRIVKEY"] = "missing_privkey";
NdkNutzapStatus2["TEMPORARY_ERROR"] = "temporary_error";
NdkNutzapStatus2["PERMANENT_ERROR"] = "permanent_error";
NdkNutzapStatus2["INVALID_NUTZAP"] = "invalid_nutzap";
return NdkNutzapStatus2;
})(NdkNutzapStatus || {});
// src/events/kinds/index.ts
var NDKKind = /* @__PURE__ */ ((NDKKind2) => {
NDKKind2[NDKKind2["Metadata"] = 0] = "Metadata";
NDKKind2[NDKKind2["Text"] = 1] = "Text";
NDKKind2[NDKKind2["RecommendRelay"] = 2] = "RecommendRelay";
NDKKind2[NDKKind2["Contacts"] = 3] = "Contacts";
NDKKind2[NDKKind2["EncryptedDirectMessage"] = 4] = "EncryptedDirectMessage";
NDKKind2[NDKKind2["EventDeletion"] = 5] = "EventDeletion";
NDKKind2[NDKKind2["Repost"] = 6] = "Repost";
NDKKind2[NDKKind2["Reaction"] = 7] = "Reaction";
NDKKind2[NDKKind2["BadgeAward"] = 8] = "BadgeAward";
NDKKind2[NDKKind2["GroupChat"] = 9] = "GroupChat";
NDKKind2[NDKKind2["Thread"] = 11] = "Thread";
NDKKind2[NDKKind2["GroupReply"] = 12] = "GroupReply";
NDKKind2[NDKKind2["GiftWrapSeal"] = 13] = "GiftWrapSeal";
NDKKind2[NDKKind2["PrivateDirectMessage"] = 14] = "PrivateDirectMessage";
NDKKind2[NDKKind2["Image"] = 20] = "Image";
NDKKind2[NDKKind2["Video"] = 21] = "Video";
NDKKind2[NDKKind2["ShortVideo"] = 22] = "ShortVideo";
NDKKind2[NDKKind2["Story"] = 23] = "Story";
NDKKind2[NDKKind2["Vanish"] = 62] = "Vanish";
NDKKind2[NDKKind2["CashuWalletBackup"] = 375] = "CashuWalletBackup";
NDKKind2[NDKKind2["GiftWrap"] = 1059] = "GiftWrap";
NDKKind2[NDKKind2["GenericRepost"] = 16] = "GenericRepost";
NDKKind2[NDKKind2["ChannelCreation"] = 40] = "ChannelCreation";
NDKKind2[NDKKind2["ChannelMetadata"] = 41] = "ChannelMetadata";
NDKKind2[NDKKind2["ChannelMessage"] = 42] = "ChannelMessage";
NDKKind2[NDKKind2["ChannelHideMessage"] = 43] = "ChannelHideMessage";
NDKKind2[NDKKind2["ChannelMuteUser"] = 44] = "ChannelMuteUser";
NDKKind2[NDKKind2["WikiMergeRequest"] = 818] = "WikiMergeRequest";
NDKKind2[NDKKind2["GenericReply"] = 1111] = "GenericReply";
NDKKind2[NDKKind2["Media"] = 1063] = "Media";
NDKKind2[NDKKind2["DraftCheckpoint"] = 1234] = "DraftCheckpoint";
NDKKind2[NDKKind2["Task"] = 1934] = "Task";
NDKKind2[NDKKind2["Report"] = 1984] = "Report";
NDKKind2[NDKKind2["Label"] = 1985] = "Label";
NDKKind2[NDKKind2["DVMReqTextExtraction"] = 5e3] = "DVMReqTextExtraction";
NDKKind2[NDKKind2["DVMReqTextSummarization"] = 5001] = "DVMReqTextSummarization";
NDKKind2[NDKKind2["DVMReqTextTranslation"] = 5002] = "DVMReqTextTranslation";
NDKKind2[NDKKind2["DVMReqTextGeneration"] = 5050] = "DVMReqTextGeneration";
NDKKind2[NDKKind2["DVMReqImageGeneration"] = 5100] = "DVMReqImageGeneration";
NDKKind2[NDKKind2["DVMReqTextToSpeech"] = 5250] = "DVMReqTextToSpeech";
NDKKind2[NDKKind2["DVMReqDiscoveryNostrContent"] = 5300] = "DVMReqDiscoveryNostrContent";
NDKKind2[NDKKind2["DVMReqDiscoveryNostrPeople"] = 5301] = "DVMReqDiscoveryNostrPeople";
NDKKind2[NDKKind2["DVMReqTimestamping"] = 5900] = "DVMReqTimestamping";
NDKKind2[NDKKind2["DVMEventSchedule"] = 5905] = "DVMEventSchedule";
NDKKind2[NDKKind2["DVMJobFeedback"] = 7e3] = "DVMJobFeedback";
NDKKind2[NDKKind2["Subscribe"] = 7001] = "Subscribe";
NDKKind2[NDKKind2["Unsubscribe"] = 7002] = "Unsubscribe";
NDKKind2[NDKKind2["SubscriptionReceipt"] = 7003] = "SubscriptionReceipt";
NDKKind2[NDKKind2["CashuReserve"] = 7373] = "CashuReserve";
NDKKind2[NDKKind2["CashuQuote"] = 7374] = "CashuQuote";
NDKKind2[NDKKind2["CashuToken"] = 7375] = "CashuToken";
NDKKind2[NDKKind2["CashuWalletTx"] = 7376] = "CashuWalletTx";
NDKKind2[NDKKind2["GroupAdminAddUser"] = 9e3] = "GroupAdminAddUser";
NDKKind2[NDKKind2["GroupAdminRemoveUser"] = 9001] = "GroupAdminRemoveUser";
NDKKind2[NDKKind2["GroupAdminEditMetadata"] = 9002] = "GroupAdminEditMetadata";
NDKKind2[NDKKind2["GroupAdminEditStatus"] = 9006] = "GroupAdminEditStatus";
NDKKind2[NDKKind2["GroupAdminCreateGroup"] = 9007] = "GroupAdminCreateGroup";
NDKKind2[NDKKind2["GroupAdminRequestJoin"] = 9021] = "GroupAdminRequestJoin";
NDKKind2[NDKKind2["MuteList"] = 1e4] = "MuteList";
NDKKind2[NDKKind2["PinList"] = 10001] = "PinList";
NDKKind2[NDKKind2["RelayList"] = 10002] = "RelayList";
NDKKind2[NDKKind2["BookmarkList"] = 10003] = "BookmarkList";
NDKKind2[NDKKind2["CommunityList"] = 10004] = "CommunityList";
NDKKind2[NDKKind2["PublicChatList"] = 10005] = "PublicChatList";
NDKKind2[NDKKind2["BlockRelayList"] = 10006] = "BlockRelayList";
NDKKind2[NDKKind2["SearchRelayList"] = 10007] = "SearchRelayList";
NDKKind2[NDKKind2["SimpleGroupList"] = 10009] = "SimpleGroupList";
NDKKind2[NDKKind2["InterestList"] = 10015] = "InterestList";
NDKKind2[NDKKind2["CashuMintList"] = 10019] = "CashuMintList";
NDKKind2[NDKKind2["EmojiList"] = 10030] = "EmojiList";
NDKKind2[NDKKind2["DirectMessageReceiveRelayList"] = 10050] = "DirectMessageReceiveRelayList";
NDKKind2[NDKKind2["BlossomList"] = 10063] = "BlossomList";
NDKKind2[NDKKind2["NostrWaletConnectInfo"] = 13194] = "NostrWaletConnectInfo";
NDKKind2[NDKKind2["TierList"] = 17e3] = "TierList";
NDKKind2[NDKKind2["CashuWallet"] = 17375] = "CashuWallet";
NDKKind2[NDKKind2["FollowSet"] = 3e4] = "FollowSet";
NDKKind2[NDKKind2["CategorizedPeopleList"] = 3e4 /* FollowSet */] = "CategorizedPeopleList";
NDKKind2[NDKKind2["CategorizedBookmarkList"] = 30001] = "CategorizedBookmarkList";
NDKKind2[NDKKind2["RelaySet"] = 30002] = "RelaySet";
NDKKind2[NDKKind2["CategorizedRelayList"] = 30002 /* RelaySet */] = "CategorizedRelayList";
NDKKind2[NDKKind2["BookmarkSet"] = 30003] = "BookmarkSet";
NDKKind2[NDKKind2["CurationSet"] = 30004] = "CurationSet";
NDKKind2[NDKKind2["ArticleCurationSet"] = 30004] = "ArticleCurationSet";
NDKKind2[NDKKind2["VideoCurationSet"] = 30005] = "VideoCurationSet";
NDKKind2[NDKKind2["ImageCurationSet"] = 30006] = "ImageCurationSet";
NDKKind2[NDKKind2["InterestSet"] = 30015] = "InterestSet";
NDKKind2[NDKKind2["InterestsList"] = 30015 /* InterestSet */] = "InterestsList";
NDKKind2[NDKKind2["ProjectTemplate"] = 30717] = "ProjectTemplate";
NDKKind2[NDKKind2["EmojiSet"] = 30030] = "EmojiSet";
NDKKind2[NDKKind2["ModularArticle"] = 30040] = "ModularArticle";
NDKKind2[NDKKind2["ModularArticleItem"] = 30041] = "ModularArticleItem";
NDKKind2[NDKKind2["Wiki"] = 30818] = "Wiki";
NDKKind2[NDKKind2["Draft"] = 31234] = "Draft";
NDKKind2[NDKKind2["Project"] = 31933] = "Project";
NDKKind2[NDKKind2["SubscriptionTier"] = 37001] = "SubscriptionTier";
NDKKind2[NDKKind2["EcashMintRecommendation"] = 38e3] = "EcashMintRecommendation";
NDKKind2[NDKKind2["HighlightSet"] = 39802] = "HighlightSet";
NDKKind2[NDKKind2["CategorizedHighlightList"] = 39802 /* HighlightSet */] = "CategorizedHighlightList";
NDKKind2[NDKKind2["Nutzap"] = 9321] = "Nutzap";
NDKKind2[NDKKind2["ZapRequest"] = 9734] = "ZapRequest";
NDKKind2[NDKKind2["Zap"] = 9735] = "Zap";
NDKKind2[NDKKind2["Highlight"] = 9802] = "Highlight";
NDKKind2[NDKKind2["ClientAuth"] = 22242] = "ClientAuth";
NDKKind2[NDKKind2["NostrWalletConnectReq"] = 23194] = "NostrWalletConnectReq";
NDKKind2[NDKKind2["NostrWalletConnectRes"] = 23195] = "NostrWalletConnectRes";
NDKKind2[NDKKind2["NostrConnect"] = 24133] = "NostrConnect";
NDKKind2[NDKKind2["BlossomUpload"] = 24242] = "BlossomUpload";
NDKKind2[NDKKind2["HttpAuth"] = 27235] = "HttpAuth";
NDKKind2[NDKKind2["ProfileBadge"] = 30008] = "ProfileBadge";
NDKKind2[NDKKind2["BadgeDefinition"] = 30009] = "BadgeDefinition";
NDKKind2[NDKKind2["MarketStall"] = 30017] = "MarketStall";
NDKKind2[NDKKind2["MarketProduct"] = 30018] = "MarketProduct";
NDKKind2[NDKKind2["Article"] = 30023] = "Article";
NDKKind2[NDKKind2["AppSpecificData"] = 30078] = "AppSpecificData";
NDKKind2[NDKKind2["Classified"] = 30402] = "Classified";
NDKKind2[NDKKind2["HorizontalVideo"] = 34235] = "HorizontalVideo";
NDKKind2[NDKKind2["VerticalVideo"] = 34236] = "VerticalVideo";
NDKKind2[NDKKind2["LegacyCashuWallet"] = 37375] = "LegacyCashuWallet";
NDKKind2[NDKKind2["GroupMetadata"] = 39e3] = "GroupMetadata";
NDKKind2[NDKKind2["GroupAdmins"] = 39001] = "GroupAdmins";
NDKKind2[NDKKind2["GroupMembers"] = 39002] = "GroupMembers";
NDKKind2[NDKKind2["FollowPack"] = 39089] = "FollowPack";
NDKKind2[NDKKind2["MediaFollowPack"] = 39092] = "MediaFollowPack";
NDKKind2[NDKKind2["AppRecommendation"] = 31989] = "AppRecommendation";
NDKKind2[NDKKind2["AppHandler"] = 31990] = "AppHandler";
return NDKKind2;
})(NDKKind || {});
var NDKListKinds = [
1e4 /* MuteList */,
10001 /* PinList */,
10002 /* RelayList */,
10003 /* BookmarkList */,
10004 /* CommunityList */,
10005 /* PublicChatList */,
10006 /* BlockRelayList */,
10007 /* SearchRelayList */,
10015 /* InterestList */,
10030 /* EmojiList */,
10050 /* DirectMessageReceiveRelayList */,
3e4 /* FollowSet */,
30003 /* BookmarkSet */,
30001 /* CategorizedBookmarkList */,
// Backwards compatibility
30002 /* RelaySet */,
30004 /* ArticleCurationSet */,
30005 /* VideoCurationSet */,
30015 /* InterestSet */,
30030 /* EmojiSet */,
39802 /* HighlightSet */
];
// 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;
for (const relay of connectedRelays) {
if (authorRelays.has(relay.url)) {
addAuthorToRelay(author, relay.url);
missingRelayCount--;
}
}
for (const authorRelay of authorRelays) {
if (relayToAuthorsMap.has(authorRelay)) {
addAuthorToRelay(author, authorRelay);
missingRelayCount--;
}
}
if (missingRelayCount <= 0) continue;
for (const relay of sortedRelays) {
if (missingRelayCount <= 0) break;
if (authorRelays.has(relay)) {
addAuthorToRelay(author, 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/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 MAX_RECONNECT_ATTEMPTS = 5;
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();
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(3e4, 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
*/
handleStaleConnection() {
this._status = 1 /* DISCONNECTED */;
this.wasIdle = true;
this.onDisconnect();
}
/**
* 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 connection is closed.
* This method is called when the WebSocket connection is successfully closed.
* It updates the connection statistics, sets the connection status to `DISCONNECTED`,
* initiates a reconnection attempt if we didn't disconnect ourselves,
* and emits a `disconnect` event on the `ndkRelay` object.
*/
onDisconnect() {
this.netDebug?.("disconnected", this.ndkRelay);
this.updateConnectionStats.disconnected();
this.keepalive?.stop();
if (this._status === 5 /* CONNECTED */) {
this.handleReconnection();
}
this._status = 1 /* DISCONNECTED */;
this.ndkRelay.emit("disconnect");
}
/**
* Handles incoming messages from the NDK relay WebSocket connection.
* This method is called whenever a message is received from the relay.
* It parses the message data and dispatches the appropriate handling logic based on the message type.
*
* @param event - The MessageEvent containing the received message data.
*/
onMessage(event) {
this.netDebug?.(event.data, this.ndkRelay, "recv");
this.keepalive?.recordActivity();
try {
const data = JSON.parse(event.data);
const [cmd, id, ..._rest] = data;
switch (cmd) {
case "EVENT": {
const so = this.openSubs.get(id);
const event2 = data[2];
if (!so) {
this.debug(`Received event for unknown subscription ${id}`);
return;
}
so.onevent(event2);
return;
}
case "COUNT": {
const payload = data[2];
const cr = this.openCountRequests.get(id);
if (cr) {
cr.resolve(payload.count);
this.openCountRequests.delete(id);
}
return;
}
case "EOSE": {
const so = this.openSubs.get(id);
if (!so) return;
so.oneose(id);
return;
}
case "OK": {
const ok = data[2];
const reason = data[3];
const ep = this.openEventPublishes.get(id);
const firstEp = ep?.pop();
if (!ep || !firstEp) {
this.debug("Received OK for unknown event publish", id);
return;
}
if (ok) firstEp.resolve(reason);
else firstEp.reject(new Error(reason));
if (ep.length === 0) {
this.openEventPublishes.delete(id);
} else {
this.openEventPublishes.set(id, ep);
}
return;
}
case "CLOSED": {
const so = this.openSubs.get(id);
if (!so) return;
so.onclosed(data[2]);
return;
}
case "NOTICE":
this.onNotice(data[1]);
return;
case "AUTH": {
this.onAuthRequested(data[1]);
return;
}
}
} catch (error) {
this.debug(`Error parsing message from ${this.ndkRelay.url}: ${error.message}`, error?.stack);
return;
}
}
/**
* Handles an authentication request from the NDK relay.
*
* If an authentication policy is configured, it will be used to authenticate the connection.
* Otherwise, the `auth` event will be emitted to allow the application to handle the authentication.
*
* @param challenge - The authentication challenge provided by the NDK relay.
*/
async onAuthRequested(challenge) {
const authPolicy = this.ndkRelay.authPolicy ?? this.ndk?.relayAuthDefaultPolicy;
this.debug("Relay requested authentication", {
havePolicy: !!authPolicy
});
if (this._status === 7 /* AUTHENTICATING */) {
this.debug("Already authenticating, ignoring");
return;
}
this._status = 6 /* AUTH_REQUESTED */;
if (authPolicy) {
if (this._status >= 5 /* CONNECTED */) {
this._status = 7 /* AUTHENTICATING */;
let res;
try {
res = await authPolicy(this.ndkRelay, challenge);
} catch (e) {
this.debug("Authentication policy threw an error", e);
res = false;
}
this.debug("Authentication policy returned", !!res);
if (res instanceof NDKEvent || res === true) {
if (res instanceof NDKEvent) {
await this.auth(res);
}
const authenticate = async () => {
if (this._status >= 5 /* CONNECTED */ && this._status < 8 /* AUTHENTICATED */) {
const event = new NDKEvent(this.ndk);
event.kind = 22242 /* ClientAuth */;
event.tags = [
["relay", this.ndkRelay.url],
["challenge", challenge]
];
await event.sign();
this.auth(event).then(() => {
this._status = 8 /* AUTHENTICATED */;
this.ndkRelay.emit("authed");
this.debug("Authentication successful");
}).catch((e) => {
this._status = 6 /* AUTH_REQUESTED */;
this.ndkRelay.emit("auth:failed", e);
this.debug("Authentication failed", e);
});
} else {
this.debug("Authentication failed, it changed status, status is %d", this._status);
}
};
if (res === true) {
if (!this.ndk?.signer) {
this.debug("No signer available for authentication localhost");
this.ndk?.once("signer:ready", authenticate);
} else {
authenticate().catch((e) => {
console.error("Error authenticating", e);
});
}
}
this._status = 5 /* CONNECTED */;
this.ndkRelay.emit("authed");
}
}
} else {
this.ndkRelay.emit("auth", challenge);
}
}
/**
* Handles errors that occur on the WebSocket connection to the relay.
* @param error - The error or event that occurred.
*/
onError(error) {
this.debug(`WebSocket error on ${this.ndkRelay.url}:`, error);
}
/**
* Gets the current status of the NDK relay connection.
* @returns {NDKRelayStatus} The current status of the NDK relay connection.
*/
get status() {
return this._status;
}
/**
* Checks if the NDK relay connection is currently available.
* @returns {boolean} `true` if the relay connection is in the `CONNECTED` status, `false` otherwise.
*/
isAvailable() {
return this._status === 5 /* CONNECTED */;
}
/**
* Checks if the NDK relay connection is flapping, which means the connection is rapidly
* disconnecting and reconnecting. This is determined by analyzing the durations of the
* last three connection attempts. If the standard deviation of the durations is less
* than 1000 milliseconds, the connection is considered to be flapping.
*
* @returns {boolean} `true` if the connection is flapping, `false` otherwise.
*/
isFlapping() {
const durations = this._connectionStats.durations;
if (durations.length % 3 !== 0) return false;
const sum = durations.reduce((a, b) => a + b, 0);
const avg = sum / durations.length;
const variance = durations.map((x) => (x - avg) ** 2).reduce((a, b) => a + b, 0) / durations.length;
const stdDev = Math.sqrt(variance);
const isFlapping = stdDev < FLAPPING_THRESHOLD_MS;
return isFlapping;
}
/**
* Handles a notice received from the NDK relay.
* If the notice indicates the relay is complaining (e.g. "too many" or "maximum"),
* the method disconnects from the relay and attempts to reconnect after a 2-second delay.
* A debug message is logged with the relay URL and the notice text.
* The "notice" event is emitted on the ndkRelay instance with the notice text.
*
* @param notice - The notice text received from the NDK relay.
*/
async onNotice(notice) {
this.ndkRelay.emit("notice", notice);
}
/**
* Attempts to reconnect to the NDK relay after a connection is lost.
* This function is called recursively to handle multiple reconnection attempts.
* It checks if the relay is flapping and emits a "flapping" event if so.
* It then calculates a delay before the next reconnection attempt based on the number of previous attempts.
* The function sets a timeout to execute the next reconnection attempt after the calculated delay.
* If the maximum number of reconnection attempts is reached, a debug message is logged.
*
* @param attempt - The current attempt number (default is 0).
*/
handleReconnection(attempt = 0) {
if (this.reconnectTimeout) return;
if (this.isFlapping()) {
this.ndkRelay.emit("flapping", this._connectionStats);
this._status = 3 /* FLAPPING */;
return;
}
let reconnectDelay;
if (this.wasIdle) {
const aggressiveDelays = [0, 1e3, 2e3, 5e3, 1e4, 3e4];
reconnectDelay = aggressiveDelays[Math.min(attempt, aggressiveDelays.length - 1)];
this.debug(`Using aggressive reconnect after idle, attempt ${attempt}, delay ${reconnectDelay}ms`);
} else if (this.connectedAt) {
reconnectDelay = Math.max(0, 6e4 - (Date.now() - this.connectedAt));
} else {
reconnectDelay = Math.min(1e3 * Math.pow(2, attempt), 3e4);
this.debug(`Using standard backoff, attempt ${attempt}, delay ${reconnectDelay}ms`);
}
this.reconnectTimeout = setTimeout(() => {
this.reconnectTimeout = void 0;
this._status = 2 /* RECONNECTING */;
this.connect().catch((_err) => {
if (attempt < MAX_RECONNECT_ATTEMPTS) {
this.handleReconnection(attempt + 1);
} else {
this.debug("Max reconnect attempts reached");
this.wasIdle = false;
}
});
}, reconnectDelay);
this.ndkRelay.emit("delayed-connect", reconnectDelay);
this.debug("Reconnecting in", reconnectDelay);
this._connectionStats.nextReconnectAt = Date.now() + reconnectDelay;
}
/**
* Sends a message to the NDK relay if the connection is in the CONNECTED state and the WebSocket is open.
* If the connection is not in the CONNECTED state or the WebSocket is not open, logs a debug message and throws an error.
*
* @param message - The message to send to the NDK relay.
* @throws {Error} If attempting to send on a closed relay connection.
*/
async send(message) {
const idleTime = Date.now() - this.lastMessageSent;
if (idleTime > 12e4) {
this.wasIdle = true;
}
if (this._status >= 5 /* CONNECTED */ && this.ws?.readyState === WebSocket.OPEN) {
this.ws?.send(message);
this.netDebug?.(message, this.ndkRelay, "send");
this.lastMessageSent = Date.now();
} else {
this.debug(`Not connected to ${this.ndkRelay.url} (%d), not sending message ${message}`, this._status);
if (this._status >= 5 /* CONNECTED */ && this.ws?.readyState !== WebSocket.OPEN) {
this.debug(`Stale connection detected, WebSocket state: ${this.ws?.readyState}`);
this.handleStale