UNPKG

@nostr-dev-kit/ndk

Version:

NDK - Nostr Development Kit

1,290 lines (1,277 loc) 378 kB
"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