@qntx/nostr 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/dist/abort-Ba8tDrOI.mjs +38 -0
  2. package/dist/abort-Ba8tDrOI.mjs.map +1 -0
  3. package/dist/{builder-BphJa-Y-.d.mts → builder-BWn55Paf.d.mts} +20 -14
  4. package/dist/{builder-q0VsvnYE.mjs → builder-CyrGKRVP.mjs} +32 -16
  5. package/dist/builder-CyrGKRVP.mjs.map +1 -0
  6. package/dist/{client-Bd8B1mYm.mjs → client-BWsX9Cv1.mjs} +354 -195
  7. package/dist/client-BWsX9Cv1.mjs.map +1 -0
  8. package/dist/client.d.mts +1 -1
  9. package/dist/client.mjs +1 -1
  10. package/dist/core.d.mts +8 -8
  11. package/dist/core.mjs +8 -9
  12. package/dist/error-BW7re1UN.d.mts +9 -0
  13. package/dist/error-B__OMBRY.mjs +14 -0
  14. package/dist/error-B__OMBRY.mjs.map +1 -0
  15. package/dist/{error-WEFS-R-4.d.mts → error-CR8Urc84.d.mts} +20 -8
  16. package/dist/{error-CwBxle6N.mjs → error-DrEIM7FO.mjs} +5 -3
  17. package/dist/error-DrEIM7FO.mjs.map +1 -0
  18. package/dist/{event-CzoQEFMF.mjs → event-D8Nfoh4K.mjs} +40 -32
  19. package/dist/event-D8Nfoh4K.mjs.map +1 -0
  20. package/dist/{event-B5D2jxt_.d.mts → event-fU3Y01PR.d.mts} +23 -20
  21. package/dist/{filter-BxwjEkZp.mjs → filter-BQl3GlHt.mjs} +42 -35
  22. package/dist/filter-BQl3GlHt.mjs.map +1 -0
  23. package/dist/filter-C0IMByE3.d.mts +37 -0
  24. package/dist/{gossip-C5RVB1UB.d.mts → gossip-BRQwIM_q.d.mts} +25 -23
  25. package/dist/{gossip-3xkRRCm_.mjs → gossip-oloDMWEj.mjs} +52 -34
  26. package/dist/gossip-oloDMWEj.mjs.map +1 -0
  27. package/dist/gossip.d.mts +1 -1
  28. package/dist/gossip.mjs +1 -1
  29. package/dist/{http-6vevbBlu.d.mts → http-C4HwIu4B.d.mts} +7 -7
  30. package/dist/{http-D2rhokZo.mjs → http-CuazNRIP.mjs} +10 -6
  31. package/dist/http-CuazNRIP.mjs.map +1 -0
  32. package/dist/index-B425CAcQ.d.mts +328 -0
  33. package/dist/index-BVyfUBuG.d.mts +181 -0
  34. package/dist/{index-BYMnhRIC.d.mts → index-Ba7bJ7DD.d.mts} +68 -50
  35. package/dist/{index-DaRyjo7O.d.mts → index-Boe8FshM.d.mts} +13 -7
  36. package/dist/index-CUIvFQdE.d.mts +73 -0
  37. package/dist/index-JdaS_uce.d.mts +207 -0
  38. package/dist/index.d.mts +22 -20
  39. package/dist/index.mjs +19 -18
  40. package/dist/{key-Dn-aX4x4.mjs → key-Bbtys6o7.mjs} +22 -14
  41. package/dist/key-Bbtys6o7.mjs.map +1 -0
  42. package/dist/{key-BiGxGLhg.d.mts → key-BlsYhN1V.d.mts} +9 -6
  43. package/dist/{kind-BLl4cLOc.mjs → kind-Cxwhhkx8.mjs} +2 -1
  44. package/dist/kind-Cxwhhkx8.mjs.map +1 -0
  45. package/dist/{loaders-BNQszDAn.mjs → loaders-o6YXB_Ri.mjs} +329 -305
  46. package/dist/loaders-o6YXB_Ri.mjs.map +1 -0
  47. package/dist/loaders.d.mts +3 -3
  48. package/dist/loaders.mjs +2 -2
  49. package/dist/memory-C2EVx0_O.mjs +46 -0
  50. package/dist/memory-C2EVx0_O.mjs.map +1 -0
  51. package/dist/{memory-BCmhZPek.mjs → memory-index-Do1mz0WF.mjs} +212 -82
  52. package/dist/memory-index-Do1mz0WF.mjs.map +1 -0
  53. package/dist/{message-C_Uj-Y-c.mjs → message-DcY6Fi9o.mjs} +38 -24
  54. package/dist/message-DcY6Fi9o.mjs.map +1 -0
  55. package/dist/{message-DJC-Jz6T.d.mts → message-iGPIqnn0.d.mts} +15 -8
  56. package/dist/{nip17-BWq-Kfq5.d.mts → nip17-DNEkz8rM.d.mts} +18 -17
  57. package/dist/{nip19-D7905oIv.d.mts → nip19-BevCJZsp.d.mts} +23 -3
  58. package/dist/{nip46-C_DoxiS_.d.mts → nip46-DDV4dC56.d.mts} +13 -12
  59. package/dist/{nip59-DljW_sYh.d.mts → nip59-zQgdLcEr.d.mts} +28 -25
  60. package/dist/{nip65-Di5hZRuV.d.mts → nip65-DBfh2smt.d.mts} +8 -3
  61. package/dist/{nip77-ByxlgoQO.d.mts → nip77-DH4RhS5r.d.mts} +9 -8
  62. package/dist/nips/blossom.d.mts +37 -37
  63. package/dist/nips/blossom.mjs +67 -52
  64. package/dist/nips/blossom.mjs.map +1 -1
  65. package/dist/nips/nip04.d.mts +2 -3
  66. package/dist/nips/nip04.mjs +6 -10
  67. package/dist/nips/nip04.mjs.map +1 -1
  68. package/dist/nips/nip05.d.mts +37 -43
  69. package/dist/nips/nip05.mjs +57 -57
  70. package/dist/nips/nip05.mjs.map +1 -1
  71. package/dist/nips/nip10.d.mts +18 -19
  72. package/dist/nips/nip10.mjs +76 -61
  73. package/dist/nips/nip10.mjs.map +1 -1
  74. package/dist/nips/nip11.d.mts +9 -8
  75. package/dist/nips/nip11.mjs +14 -11
  76. package/dist/nips/nip11.mjs.map +1 -1
  77. package/dist/nips/nip13.d.mts +9 -9
  78. package/dist/nips/nip13.mjs +15 -17
  79. package/dist/nips/nip13.mjs.map +1 -1
  80. package/dist/nips/nip17.d.mts +1 -1
  81. package/dist/nips/nip17.mjs +17 -12
  82. package/dist/nips/nip17.mjs.map +1 -1
  83. package/dist/nips/nip19.d.mts +1 -1
  84. package/dist/nips/nip19.mjs +52 -29
  85. package/dist/nips/nip19.mjs.map +1 -1
  86. package/dist/nips/nip21.d.mts +5 -6
  87. package/dist/nips/nip21.mjs +8 -7
  88. package/dist/nips/nip21.mjs.map +1 -1
  89. package/dist/nips/nip27.d.mts +7 -8
  90. package/dist/nips/nip27.mjs +26 -25
  91. package/dist/nips/nip27.mjs.map +1 -1
  92. package/dist/nips/nip42.d.mts +5 -6
  93. package/dist/nips/nip42.mjs +3 -3
  94. package/dist/nips/nip42.mjs.map +1 -1
  95. package/dist/nips/nip44.d.mts +24 -7
  96. package/dist/nips/nip44.mjs +25 -15
  97. package/dist/nips/nip44.mjs.map +1 -1
  98. package/dist/nips/nip46.d.mts +1 -1
  99. package/dist/nips/nip46.mjs +52 -49
  100. package/dist/nips/nip46.mjs.map +1 -1
  101. package/dist/nips/nip49.d.mts +8 -7
  102. package/dist/nips/nip49.mjs +16 -12
  103. package/dist/nips/nip49.mjs.map +1 -1
  104. package/dist/nips/nip51.d.mts +25 -27
  105. package/dist/nips/nip51.mjs +47 -36
  106. package/dist/nips/nip51.mjs.map +1 -1
  107. package/dist/nips/nip57.d.mts +12 -13
  108. package/dist/nips/nip57.mjs +35 -38
  109. package/dist/nips/nip57.mjs.map +1 -1
  110. package/dist/nips/nip59.d.mts +1 -1
  111. package/dist/nips/nip59.mjs +14 -18
  112. package/dist/nips/nip59.mjs.map +1 -1
  113. package/dist/nips/nip65.d.mts +1 -1
  114. package/dist/nips/nip65.mjs +11 -8
  115. package/dist/nips/nip65.mjs.map +1 -1
  116. package/dist/nips/nip77.d.mts +1 -1
  117. package/dist/nips/nip77.mjs +56 -34
  118. package/dist/nips/nip77.mjs.map +1 -1
  119. package/dist/nips/nip96.d.mts +21 -12
  120. package/dist/nips/nip96.mjs +59 -36
  121. package/dist/nips/nip96.mjs.map +1 -1
  122. package/dist/nips/nip98.d.mts +12 -12
  123. package/dist/nips/nip98.mjs +18 -19
  124. package/dist/nips/nip98.mjs.map +1 -1
  125. package/dist/{pool-C_j1OmBB.mjs → pool-XmAGaufS.mjs} +611 -487
  126. package/dist/pool-XmAGaufS.mjs.map +1 -0
  127. package/dist/pool-tEr7c5bG.d.mts +302 -0
  128. package/dist/reactive-DDlThDSZ.mjs +324 -0
  129. package/dist/reactive-DDlThDSZ.mjs.map +1 -0
  130. package/dist/reactive-DFpqtIf_.d.mts +66 -0
  131. package/dist/relay.d.mts +5 -4
  132. package/dist/relay.mjs +2 -2
  133. package/dist/report-CHld4uWj.mjs +32 -0
  134. package/dist/report-CHld4uWj.mjs.map +1 -0
  135. package/dist/{signer-CZ6K6FUc.mjs → signer-CKR9EHce.mjs} +107 -72
  136. package/dist/signer-CKR9EHce.mjs.map +1 -0
  137. package/dist/signer.d.mts +3 -3
  138. package/dist/signer.mjs +3 -2
  139. package/dist/storage-xXxLGCVb.mjs +1376 -0
  140. package/dist/storage-xXxLGCVb.mjs.map +1 -0
  141. package/dist/storage.d.mts +4 -4
  142. package/dist/storage.mjs +5 -4
  143. package/dist/store.d.mts +2 -0
  144. package/dist/store.mjs +2 -0
  145. package/dist/testing.d.mts +125 -0
  146. package/dist/testing.mjs +957 -0
  147. package/dist/testing.mjs.map +1 -0
  148. package/dist/types-2pCUprRU.d.mts +47 -0
  149. package/dist/types-CRQEk0Oi.d.mts +19 -0
  150. package/dist/util-LfQirgDk.mjs +113 -0
  151. package/dist/util-LfQirgDk.mjs.map +1 -0
  152. package/dist/wasm.d.mts +4 -4
  153. package/dist/wasm.mjs +54 -55
  154. package/dist/wasm.mjs.map +1 -1
  155. package/dist/websocket-CmeqvAkp.d.mts +27 -0
  156. package/package.json +31 -9
  157. package/dist/builder-q0VsvnYE.mjs.map +0 -1
  158. package/dist/client-Bd8B1mYm.mjs.map +0 -1
  159. package/dist/error-B49rbDzT.d.mts +0 -7
  160. package/dist/error-CwBxle6N.mjs.map +0 -1
  161. package/dist/error-DZEs5OuZ.mjs +0 -24
  162. package/dist/error-DZEs5OuZ.mjs.map +0 -1
  163. package/dist/event-CzoQEFMF.mjs.map +0 -1
  164. package/dist/filter-BxwjEkZp.mjs.map +0 -1
  165. package/dist/filter-DUEKP3q1.d.mts +0 -40
  166. package/dist/gossip-3xkRRCm_.mjs.map +0 -1
  167. package/dist/http-D2rhokZo.mjs.map +0 -1
  168. package/dist/index-C0C4e-Gn.d.mts +0 -55
  169. package/dist/index-DgOugrpF.d.mts +0 -275
  170. package/dist/index-EwL6fHLo.d.mts +0 -267
  171. package/dist/index-k2YhCgsh.d.mts +0 -57
  172. package/dist/key-Dn-aX4x4.mjs.map +0 -1
  173. package/dist/kind-BLl4cLOc.mjs.map +0 -1
  174. package/dist/loaders-BNQszDAn.mjs.map +0 -1
  175. package/dist/memory-BCmhZPek.mjs.map +0 -1
  176. package/dist/message-C_Uj-Y-c.mjs.map +0 -1
  177. package/dist/pool-C_j1OmBB.mjs.map +0 -1
  178. package/dist/pool-DhRi7l-b.d.mts +0 -297
  179. package/dist/signer-CZ6K6FUc.mjs.map +0 -1
  180. package/dist/storage-DOy0iLRE.mjs +0 -870
  181. package/dist/storage-DOy0iLRE.mjs.map +0 -1
  182. package/dist/types-BzyADqnw.d.mts +0 -19
  183. package/dist/types-Dn24TPFs.d.mts +0 -43
  184. package/dist/util-6tfLQ9JF.mjs +0 -73
  185. package/dist/util-6tfLQ9JF.mjs.map +0 -1
@@ -1,26 +1,43 @@
1
- import { s as normalizeURL } from "./util-6tfLQ9JF.mjs";
2
- import { a as NostrError, i as MessageError, s as WasmVerifyPoisonedError } from "./error-DZEs5OuZ.mjs";
3
- import { s as verifyEvent } from "./key-Dn-aX4x4.mjs";
4
- import { i as filterFingerprint, n as canonicalizeFilters, t as canonicalizeFilter } from "./filter-BxwjEkZp.mjs";
5
- import { n as createSubscriptionId, r as encodeClientMessage, s as parseRelayMessage, t as assertSubscriptionId } from "./message-C_Uj-Y-c.mjs";
1
+ import { g as NostrError, h as MessageError, l as normalizeURL, v as WasmVerifyPoisonedError } from "./util-LfQirgDk.mjs";
2
+ import { s as verifyEvent } from "./key-Bbtys6o7.mjs";
3
+ import { n as canonicalizeFilters, r as filterFingerprint, t as canonicalizeFilter } from "./filter-BQl3GlHt.mjs";
4
+ import { n as createSubscriptionId, r as encodeClientMessage, s as parseRelayMessage, t as assertSubscriptionId } from "./message-DcY6Fi9o.mjs";
5
+ import { t as NoSignerError } from "./error-B__OMBRY.mjs";
6
+ import { t as invokeSafely } from "./report-CHld4uWj.mjs";
6
7
  import { isAuthRequired, makeAuthEvent } from "./nips/nip42.mjs";
8
+ import { n as raceSignal, r as throwIfAborted, t as abortReason } from "./abort-Ba8tDrOI.mjs";
7
9
  import { Nip77Error, runNegSession } from "./nips/nip77.mjs";
8
10
  //#region src/relay/error.ts
11
+ /** Base class for relay errors; carries the relay URL when known. */
9
12
  var RelayError = class extends NostrError {
13
+ name = "RelayError";
10
14
  url;
11
- constructor(message, url) {
12
- super(url ? `${message} (${url})` : message);
15
+ constructor(message, url, options) {
16
+ super(url !== void 0 && url !== "" ? `${message} (${url})` : message, options);
13
17
  this.url = url;
14
18
  }
15
19
  };
16
- var RelayConnectionError = class extends RelayError {};
17
- var RelayPublishError = class extends RelayError {};
18
- var RelayClosedError = class extends RelayError {};
20
+ /** A relay connection attempt or socket failed. */
21
+ var RelayConnectionError = class extends RelayError {
22
+ name = "RelayConnectionError";
23
+ };
24
+ /** A relay answered an EVENT with `ok: false`. */
25
+ var RelayPublishError = class extends RelayError {
26
+ name = "RelayPublishError";
27
+ };
28
+ /** A subscription was closed by the relay (`CLOSED`) or the socket dropped. */
29
+ var RelayClosedError = class extends RelayError {
30
+ name = "RelayClosedError";
31
+ };
32
+ /** A relay operation exceeded its deadline. */
33
+ var RelayTimeoutError = class extends RelayError {
34
+ name = "RelayTimeoutError";
35
+ };
19
36
  //#endregion
20
37
  //#region src/relay/websocket.ts
21
38
  let impl;
22
39
  try {
23
- if (typeof globalThis.WebSocket !== "undefined") impl = globalThis.WebSocket;
40
+ if (globalThis.WebSocket !== void 0) impl = globalThis.WebSocket;
24
41
  } catch {}
25
42
  /** Inject a WebSocket implementation (required on Node unless undici/global is present). */
26
43
  function useWebSocketImplementation(websocketImplementation) {
@@ -33,8 +50,8 @@ function getWebSocketImplementation() {
33
50
  //#endregion
34
51
  //#region src/relay/url.ts
35
52
  /**
36
- * True for `ws:` / `http:` URLs that are not `.onion`.
37
- * Local-network `ws://` is still insecure; allow those via a trusted set.
53
+ * True for `ws:` / `http:` URLs that are not `.onion`. Local-network `ws://` is still insecure;
54
+ * allow those via a trusted set.
38
55
  */
39
56
  function isInsecureRelayUrl(url) {
40
57
  try {
@@ -46,9 +63,211 @@ function isInsecureRelayUrl(url) {
46
63
  }
47
64
  }
48
65
  //#endregion
66
+ //#region src/relay/neg-session.ts
67
+ function createNegSession() {
68
+ return {
69
+ queue: [],
70
+ waiter: void 0,
71
+ error: void 0
72
+ };
73
+ }
74
+ function pushNegMsg(session, hex) {
75
+ if (session.waiter) {
76
+ const { waiter } = session;
77
+ session.waiter = void 0;
78
+ waiter.resolve(hex);
79
+ } else session.queue.push(hex);
80
+ }
81
+ function failNegSession(session, err) {
82
+ session.error = err;
83
+ if (session.waiter) {
84
+ const { waiter } = session;
85
+ session.waiter = void 0;
86
+ waiter.reject(err);
87
+ }
88
+ }
89
+ function failNegErr(session, reason) {
90
+ failNegSession(session, new Nip77Error(reason));
91
+ }
92
+ /** Wire queue/timeout/abort around `runNegSession`. Not a second session class. */
93
+ async function runWiredNegSession(opts) {
94
+ const { session, storage, filter, id, timeoutMs, signal, send, url } = opts;
95
+ const deadline = Date.now() + timeoutMs;
96
+ const timedOut = () => new RelayTimeoutError("negentropy timed out", url);
97
+ const remainingMs = () => deadline - Date.now();
98
+ const next = async () => {
99
+ if (session.error !== void 0) throw session.error;
100
+ if (remainingMs() <= 0) throw timedOut();
101
+ const queued = session.queue.shift();
102
+ if (queued !== void 0) return queued;
103
+ return new Promise((resolve, reject) => {
104
+ const timer = setTimeout(() => {
105
+ session.waiter = void 0;
106
+ reject(timedOut());
107
+ }, remainingMs());
108
+ const fail = (err) => {
109
+ clearTimeout(timer);
110
+ session.waiter = void 0;
111
+ reject(err);
112
+ };
113
+ const onAbort = () => {
114
+ if (signal) fail(abortReason(signal));
115
+ };
116
+ session.waiter = {
117
+ resolve: (hex) => {
118
+ clearTimeout(timer);
119
+ signal?.removeEventListener("abort", onAbort);
120
+ resolve(hex);
121
+ },
122
+ reject: (err) => fail(err)
123
+ };
124
+ signal?.addEventListener("abort", onAbort, { once: true });
125
+ });
126
+ };
127
+ return runNegSession({
128
+ storage,
129
+ openingSend: (hex) => {
130
+ send([
131
+ "NEG-OPEN",
132
+ id,
133
+ filter,
134
+ hex
135
+ ]);
136
+ },
137
+ msgSend: (hex) => {
138
+ send([
139
+ "NEG-MSG",
140
+ id,
141
+ hex
142
+ ]);
143
+ },
144
+ next
145
+ });
146
+ }
147
+ const PING_DUMMY_FILTER = {
148
+ ids: ["a".repeat(64)],
149
+ limit: 0
150
+ };
151
+ /** Browser WebSocket and `ws` both use 1 for OPEN. */
152
+ const WS_OPEN = 1;
153
+ /** Node `ws` delivers `pong` on the EventEmitter, not via addEventListener. */
154
+ function canNativePing(ws) {
155
+ return typeof ws.ping === "function" && (typeof ws.once === "function" || typeof ws.on === "function");
156
+ }
157
+ /** REQ-dummy or native ping/pong keepalive. Does not import class Relay. */
158
+ var PingLoop = class {
159
+ #opts;
160
+ #timer;
161
+ #waiters = /* @__PURE__ */ new Map();
162
+ #gen = 0;
163
+ #nativePing;
164
+ constructor(opts) {
165
+ this.#opts = opts;
166
+ }
167
+ start() {
168
+ this.stop();
169
+ this.#timer = setInterval(() => {
170
+ this.#pingpong();
171
+ }, this.#opts.pingIntervalMs);
172
+ }
173
+ stop() {
174
+ if (this.#timer !== void 0) {
175
+ clearInterval(this.#timer);
176
+ this.#timer = void 0;
177
+ }
178
+ this.#abortCurrentPing();
179
+ }
180
+ hasWaiter(id) {
181
+ return this.#waiters.has(id);
182
+ }
183
+ finishDummyPing(id) {
184
+ const waiter = this.#waiters.get(id);
185
+ if (!waiter) return false;
186
+ this.#waiters.delete(id);
187
+ waiter.resolve(true);
188
+ try {
189
+ this.#opts.send(["CLOSE", id]);
190
+ } catch {}
191
+ return true;
192
+ }
193
+ #abortCurrentPing() {
194
+ this.#gen += 1;
195
+ const native = this.#nativePing;
196
+ this.#nativePing = void 0;
197
+ native?.abort();
198
+ for (const waiter of this.#waiters.values()) waiter.resolve(false);
199
+ this.#waiters.clear();
200
+ }
201
+ async #pingpong() {
202
+ const ws = this.#opts.getWs();
203
+ if (!ws || ws.readyState !== WS_OPEN) return;
204
+ if (this.#nativePing !== void 0 || this.#waiters.size > 0) return;
205
+ const gen = this.#gen;
206
+ const ping = canNativePing(ws) ? this.#waitForNativePing(ws) : this.#waitForDummyPing();
207
+ const ok = await new Promise((resolve) => {
208
+ let settled = false;
209
+ const done = (alive) => {
210
+ if (settled) return;
211
+ settled = true;
212
+ clearTimeout(timer);
213
+ resolve(alive);
214
+ };
215
+ const timer = setTimeout(() => {
216
+ if (this.#nativePing !== void 0 || this.#waiters.size > 0) done(false);
217
+ }, this.#opts.pingTimeoutMs);
218
+ (async () => {
219
+ done(await ping);
220
+ })();
221
+ });
222
+ if (gen !== this.#gen) return;
223
+ if (!ok) {
224
+ this.#abortCurrentPing();
225
+ this.#opts.closeWs();
226
+ }
227
+ }
228
+ async #waitForNativePing(ws) {
229
+ return new Promise((resolve) => {
230
+ let settled = false;
231
+ const finish = (alive) => {
232
+ if (settled) return;
233
+ settled = true;
234
+ this.#nativePing = void 0;
235
+ ws.off?.("pong", onPong);
236
+ resolve(alive);
237
+ };
238
+ const onPong = () => finish(true);
239
+ this.#nativePing = { abort: () => finish(false) };
240
+ if (typeof ws.on === "function" && typeof ws.off === "function") ws.on("pong", onPong);
241
+ else if (typeof ws.once === "function") ws.once("pong", onPong);
242
+ else ws.on?.("pong", onPong);
243
+ try {
244
+ if (typeof ws.ping === "function") ws.ping();
245
+ else finish(false);
246
+ } catch {
247
+ finish(false);
248
+ }
249
+ });
250
+ }
251
+ async #waitForDummyPing() {
252
+ return new Promise((resolve) => {
253
+ const id = this.#opts.nextSubId("__ping__");
254
+ this.#waiters.set(id, { resolve });
255
+ try {
256
+ this.#opts.send([
257
+ "REQ",
258
+ id,
259
+ PING_DUMMY_FILTER
260
+ ]);
261
+ } catch {
262
+ this.#waiters.delete(id);
263
+ resolve(false);
264
+ }
265
+ });
266
+ }
267
+ };
268
+ //#endregion
49
269
  //#region src/relay/subscription.ts
50
270
  var Subscription = class {
51
- sendClose;
52
271
  id;
53
272
  filters;
54
273
  handlers;
@@ -61,9 +280,10 @@ var Subscription = class {
61
280
  lastCreatedAt;
62
281
  /** Event ids at `lastCreatedAt` (same-second reconnect dedup). Not all seen ids. */
63
282
  idsAtWatermark = /* @__PURE__ */ new Set();
283
+ #sendClose;
64
284
  #abort;
65
285
  constructor(filters, opts, sendClose) {
66
- this.sendClose = sendClose;
286
+ this.#sendClose = sendClose;
67
287
  this.id = createSubscriptionId(opts.id);
68
288
  this.filters = filters;
69
289
  this.closeOnEose = opts.closeOnEose === true;
@@ -87,8 +307,8 @@ var Subscription = class {
87
307
  if (this.closed) return;
88
308
  this.closed = true;
89
309
  this.#abort?.();
90
- this.sendClose(this.id);
91
- this.handlers.onclose?.(reason);
310
+ this.#sendClose(this.id);
311
+ invokeSafely(() => this.handlers.onclose?.(reason));
92
312
  }
93
313
  /** Advance the reconnect watermark after a verified EVENT. */
94
314
  noteVerified(event) {
@@ -101,8 +321,8 @@ var Subscription = class {
101
321
  if (event.created_at === this.lastCreatedAt) this.idsAtWatermark.add(event.id);
102
322
  }
103
323
  /**
104
- * Filters for re-REQ. Original `filters` stay unchanged.
105
- * NIP-01 `since` is inclusive — never `lastCreatedAt + 1`.
324
+ * Filters for re-REQ. Original `filters` stay unchanged. NIP-01 `since` is inclusive — never
325
+ * `lastCreatedAt + 1`.
106
326
  */
107
327
  replayFilters() {
108
328
  const since = this.lastCreatedAt;
@@ -120,65 +340,62 @@ function subscriptionToAsyncIterable(start, opts) {
120
340
  let error;
121
341
  let wake;
122
342
  let closer;
343
+ let localClose = false;
123
344
  const notify = () => {
124
345
  wake?.();
125
346
  wake = void 0;
126
347
  };
348
+ const waitForWake = async () => new Promise((resolve) => {
349
+ wake = resolve;
350
+ });
351
+ const closeLocal = (reason) => {
352
+ localClose = true;
353
+ closer?.close(reason);
354
+ done = true;
355
+ notify();
356
+ };
127
357
  closer = start({
128
358
  onevent(event) {
129
359
  queue.push(event);
130
360
  notify();
131
361
  },
132
362
  oneose() {
133
- if (opts?.includeEose === false) {
134
- done = true;
135
- closer?.close("eose");
136
- notify();
137
- }
363
+ if (opts?.includeEose === false) closeLocal("eose");
138
364
  },
139
365
  onclose(reason) {
140
- if (reason && reason !== "eose" && reason !== "closed by client" && reason !== "aborted") error = new RelayClosedError(reason);
366
+ if (opts?.signal?.aborted === true) localClose = true;
367
+ if (!localClose && reason) error = new RelayClosedError(reason);
141
368
  done = true;
142
369
  notify();
143
370
  }
144
371
  });
145
372
  if (opts?.signal) {
146
- if (opts.signal.aborted) {
147
- closer.close("aborted");
148
- done = true;
149
- } else opts.signal.addEventListener("abort", () => {
150
- closer?.close("aborted");
151
- done = true;
152
- notify();
153
- }, { once: true });
373
+ if (opts.signal.aborted) closeLocal("aborted");
374
+ else opts.signal.addEventListener("abort", () => closeLocal("aborted"), { once: true });
154
375
  }
155
376
  return {
156
377
  close(reason) {
157
- closer?.close(reason);
158
- done = true;
159
- notify();
378
+ closeLocal(reason ?? "closed by client");
160
379
  },
161
380
  [Symbol.asyncIterator]() {
162
381
  return {
163
382
  async next() {
164
383
  while (true) {
165
- if (queue.length > 0) return {
166
- value: queue.shift(),
384
+ const value = queue.shift();
385
+ if (value !== void 0) return {
386
+ value,
167
387
  done: false
168
388
  };
169
- if (error) throw error;
389
+ if (error !== void 0) throw error;
170
390
  if (done) return {
171
391
  value: void 0,
172
392
  done: true
173
393
  };
174
- await new Promise((resolve) => {
175
- wake = resolve;
176
- });
394
+ await waitForWake();
177
395
  }
178
396
  },
179
397
  async return() {
180
- closer?.close("iterator returned");
181
- done = true;
398
+ closeLocal("iterator returned");
182
399
  return {
183
400
  value: void 0,
184
401
  done: true
@@ -190,18 +407,6 @@ function subscriptionToAsyncIterable(start, opts) {
190
407
  }
191
408
  //#endregion
192
409
  //#region src/relay/subscribe.ts
193
- function captureListenerError(errors, fn) {
194
- try {
195
- fn();
196
- } catch (err) {
197
- errors.push(err);
198
- }
199
- }
200
- function flushListenerErrors(errors) {
201
- if (errors.length === 0) return;
202
- if (errors.length === 1) throw errors[0];
203
- throw new AggregateError(errors);
204
- }
205
410
  function openExclusive(ctx, filters, opts) {
206
411
  const sub = new Subscription(filters, opts, (id) => {
207
412
  ctx.subs.delete(id);
@@ -267,7 +472,7 @@ function subscribeLive(ctx, filters, opts) {
267
472
  if (group.sub.eosed) queueMicrotask(() => {
268
473
  if (handle.closed || handle.eosed) return;
269
474
  handle.eosed = true;
270
- handle.handlers.oneose?.();
475
+ invokeSafely(() => handle.handlers.oneose?.());
271
476
  });
272
477
  return handle;
273
478
  }
@@ -290,70 +495,54 @@ function detachLive(ctx, fp, handle) {
290
495
  function endLiveGroup(ctx, fp, opts) {
291
496
  const group = ctx.liveByFp.get(fp);
292
497
  if (!group) return;
293
- const id = group.sub.id;
498
+ const { id } = group.sub;
294
499
  const remaining = [...group.attachments];
295
500
  group.attachments.clear();
296
501
  forgetLiveGroup(ctx, group);
297
502
  if (opts.sendClose) try {
298
503
  if (ctx.connected()) ctx.send(["CLOSE", id]);
299
504
  } catch {}
300
- const errors = [];
301
- for (const att of remaining) captureListenerError(errors, () => {
505
+ for (const att of remaining) invokeSafely(() => {
302
506
  att.close(opts.reason);
303
507
  });
304
- flushListenerErrors(errors);
305
508
  }
306
509
  function deliverLiveEvent(ctx, group, event) {
307
- const sub = group.sub;
510
+ const { sub } = group;
308
511
  const attachments = [...group.attachments];
309
- const errors = [];
310
- for (const att of attachments) captureListenerError(errors, () => {
512
+ for (const att of attachments) invokeSafely(() => {
311
513
  att.handlers.receivedEvent?.(event.id);
312
514
  });
313
- if (sub.idsAtWatermark.has(event.id)) {
314
- flushListenerErrors(errors);
315
- return;
316
- }
515
+ if (sub.idsAtWatermark.has(event.id)) return;
317
516
  const recipients = [];
318
517
  for (const att of attachments) {
319
518
  if (att.closed) continue;
320
519
  let skip = false;
321
- captureListenerError(errors, () => {
520
+ invokeSafely(() => {
322
521
  skip = Boolean(att.handlers.alreadyHaveEvent?.(event.id));
323
522
  });
324
523
  if (!skip) recipients.push(att);
325
524
  }
326
- if (recipients.length === 0) {
327
- flushListenerErrors(errors);
328
- return;
329
- }
330
- if (!ctx.acceptEvent(event)) {
331
- flushListenerErrors(errors);
332
- return;
333
- }
525
+ if (recipients.length === 0) return;
526
+ if (!ctx.acceptEvent(event)) return;
334
527
  sub.noteVerified(event);
335
528
  for (const att of recipients) att.noteVerified(event);
336
- for (const att of recipients) captureListenerError(errors, () => {
529
+ for (const att of recipients) invokeSafely(() => {
337
530
  att.handlers.onevent?.(event);
338
531
  });
339
- flushListenerErrors(errors);
340
532
  }
341
533
  function deliverLiveEose(group) {
342
- const attachments = Array.from(group.attachments);
343
- const errors = [];
534
+ const attachments = [...group.attachments];
344
535
  for (const att of attachments) {
345
536
  if (att.closed || att.eosed) continue;
346
537
  att.eosed = true;
347
- captureListenerError(errors, () => {
538
+ invokeSafely(() => {
348
539
  att.handlers.oneose?.();
349
540
  });
350
541
  }
351
- flushListenerErrors(errors);
352
542
  }
353
543
  function closeAllSubscriptions(ctx, reason) {
354
- const errors = [];
355
- const fps = Array.from(ctx.liveByFp.keys());
356
- for (const fp of fps) captureListenerError(errors, () => {
544
+ const fps = [...ctx.liveByFp.keys()];
545
+ for (const fp of fps) invokeSafely(() => {
357
546
  endLiveGroup(ctx, fp, {
358
547
  sendClose: false,
359
548
  reason
@@ -361,14 +550,13 @@ function closeAllSubscriptions(ctx, reason) {
361
550
  });
362
551
  for (const sub of ctx.subs.values()) if (!sub.closed) {
363
552
  sub.closed = true;
364
- captureListenerError(errors, () => {
553
+ invokeSafely(() => {
365
554
  sub.handlers.onclose?.(reason);
366
555
  });
367
556
  }
368
557
  ctx.subs.clear();
369
558
  ctx.liveByFp.clear();
370
559
  ctx.liveBySubId.clear();
371
- flushListenerErrors(errors);
372
560
  }
373
561
  function dropSubscription(ctx, sub, reason) {
374
562
  const group = ctx.liveBySubId.get(sub.id);
@@ -381,7 +569,9 @@ function dropSubscription(ctx, sub, reason) {
381
569
  }
382
570
  ctx.subs.delete(sub.id);
383
571
  sub.closed = true;
384
- sub.handlers.onclose?.(reason);
572
+ invokeSafely(() => {
573
+ sub.handlers.onclose?.(reason);
574
+ });
385
575
  }
386
576
  function onSubEvent(ctx, subId, event) {
387
577
  const sub = ctx.subs.get(subId);
@@ -391,12 +581,20 @@ function onSubEvent(ctx, subId, event) {
391
581
  deliverLiveEvent(ctx, group, event);
392
582
  return;
393
583
  }
394
- sub.handlers.receivedEvent?.(event.id);
584
+ invokeSafely(() => {
585
+ sub.handlers.receivedEvent?.(event.id);
586
+ });
395
587
  if (sub.idsAtWatermark.has(event.id)) return;
396
- if (sub.handlers.alreadyHaveEvent?.(event.id)) return;
588
+ let have = false;
589
+ invokeSafely(() => {
590
+ have = Boolean(sub.handlers.alreadyHaveEvent?.(event.id));
591
+ });
592
+ if (have) return;
397
593
  if (!ctx.acceptEvent(event)) return;
398
594
  sub.noteVerified(event);
399
- sub.handlers.onevent?.(event);
595
+ invokeSafely(() => {
596
+ sub.handlers.onevent?.(event);
597
+ });
400
598
  }
401
599
  function onSubEose(ctx, subId) {
402
600
  const sub = ctx.subs.get(subId);
@@ -406,7 +604,9 @@ function onSubEose(ctx, subId) {
406
604
  const group = ctx.liveBySubId.get(subId);
407
605
  if (group) deliverLiveEose(group);
408
606
  else {
409
- sub.handlers.oneose?.();
607
+ invokeSafely(() => {
608
+ sub.handlers.oneose?.();
609
+ });
410
610
  if (sub.closeOnEose) sub.close("eose");
411
611
  }
412
612
  }
@@ -437,6 +637,7 @@ function streamFilters(subscribe, filters, opts) {
437
637
  }), { signal: opts?.signal });
438
638
  }
439
639
  async function fetchFilters(subscribe, filters, opts) {
640
+ throwIfAborted(opts.signal);
440
641
  const events = [];
441
642
  const seen = /* @__PURE__ */ new Set();
442
643
  await new Promise((resolve, reject) => {
@@ -445,9 +646,12 @@ async function fetchFilters(subscribe, filters, opts) {
445
646
  if (settled) return;
446
647
  settled = true;
447
648
  clearTimeout(timer);
448
- sub.close(err ? err.message : "fetch complete");
449
- if (err) reject(err);
450
- else resolve();
649
+ let reason = "aborted";
650
+ if (err instanceof Error) reason = err.message;
651
+ else if (err === void 0) reason = "fetch complete";
652
+ sub.close(reason);
653
+ if (err === void 0) resolve();
654
+ else reject(err);
451
655
  };
452
656
  const timer = setTimeout(() => done(), opts.timeoutMs);
453
657
  const sub = subscribe(filters, {
@@ -463,10 +667,10 @@ async function fetchFilters(subscribe, filters, opts) {
463
667
  done();
464
668
  },
465
669
  onclose() {
466
- if (!settled) done();
670
+ if (!settled) done(opts.signal?.aborted === true ? abortReason(opts.signal) : void 0);
467
671
  }
468
672
  });
469
- opts.signal?.addEventListener("abort", () => done(new RelayConnectionError("fetch aborted", opts.url)), { once: true });
673
+ opts.signal?.addEventListener("abort", () => done(opts.signal ? abortReason(opts.signal) : void 0), { once: true });
470
674
  });
471
675
  return events;
472
676
  }
@@ -474,7 +678,9 @@ function armEoseTimeout(sub, eoseTimeoutMs) {
474
678
  const timer = setTimeout(() => {
475
679
  if (sub.eosed || sub.closed) return;
476
680
  sub.eosed = true;
477
- sub.handlers.oneose?.();
681
+ invokeSafely(() => {
682
+ sub.handlers.oneose?.();
683
+ });
478
684
  }, eoseTimeoutMs);
479
685
  const prevClose = sub.handlers.onclose;
480
686
  sub.handlers.onclose = (reason) => {
@@ -488,205 +694,8 @@ function armEoseTimeout(sub, eoseTimeoutMs) {
488
694
  };
489
695
  }
490
696
  //#endregion
491
- //#region src/relay/neg-session.ts
492
- function createNegSession() {
493
- return {
494
- queue: [],
495
- waiter: void 0,
496
- error: void 0
497
- };
498
- }
499
- function pushNegMsg(session, hex) {
500
- if (session.waiter) {
501
- const waiter = session.waiter;
502
- session.waiter = void 0;
503
- waiter.resolve(hex);
504
- } else session.queue.push(hex);
505
- }
506
- function failNegSession(session, err) {
507
- session.error = err;
508
- if (session.waiter) {
509
- const waiter = session.waiter;
510
- session.waiter = void 0;
511
- waiter.reject(err);
512
- }
513
- }
514
- function failNegErr(session, reason) {
515
- failNegSession(session, new Nip77Error(reason));
516
- }
517
- /** Wire queue/timeout/abort around `runNegSession`. Not a second session class. */
518
- async function runWiredNegSession(opts) {
519
- const { session, storage, filter, id, timeoutMs, signal, send, url } = opts;
520
- const deadline = Date.now() + timeoutMs;
521
- const timedOut = () => new RelayPublishError("negentropy timed out", url);
522
- const remainingMs = () => deadline - Date.now();
523
- const next = () => {
524
- if (session.error) return Promise.reject(session.error);
525
- if (remainingMs() <= 0) return Promise.reject(timedOut());
526
- const queued = session.queue.shift();
527
- if (queued !== void 0) return Promise.resolve(queued);
528
- return new Promise((resolve, reject) => {
529
- const timer = setTimeout(() => {
530
- session.waiter = void 0;
531
- reject(timedOut());
532
- }, remainingMs());
533
- const fail = (err) => {
534
- clearTimeout(timer);
535
- session.waiter = void 0;
536
- reject(err);
537
- };
538
- const onAbort = () => fail(new RelayConnectionError("negentropy aborted", url));
539
- session.waiter = {
540
- resolve: (hex) => {
541
- clearTimeout(timer);
542
- signal?.removeEventListener("abort", onAbort);
543
- resolve(hex);
544
- },
545
- reject: (err) => fail(err)
546
- };
547
- signal?.addEventListener("abort", onAbort, { once: true });
548
- });
549
- };
550
- return await runNegSession({
551
- storage,
552
- openingSend: (hex) => {
553
- send([
554
- "NEG-OPEN",
555
- id,
556
- filter,
557
- hex
558
- ]);
559
- },
560
- msgSend: (hex) => {
561
- send([
562
- "NEG-MSG",
563
- id,
564
- hex
565
- ]);
566
- },
567
- next
568
- });
569
- }
570
- const PING_DUMMY_FILTER = {
571
- ids: ["a".repeat(64)],
572
- limit: 0
573
- };
574
- /** Browser WebSocket and `ws` both use 1 for OPEN. */
575
- const WS_OPEN = 1;
576
- /** node `ws` delivers `pong` on the EventEmitter, not via addEventListener. */
577
- function canNativePing(ws) {
578
- return typeof ws.ping === "function" && (typeof ws.once === "function" || typeof ws.on === "function");
579
- }
580
- /** REQ-dummy or native ping/pong keepalive. Does not import class Relay. */
581
- var PingLoop = class {
582
- #opts;
583
- #timer;
584
- #waiters = /* @__PURE__ */ new Map();
585
- #gen = 0;
586
- #nativePing;
587
- constructor(opts) {
588
- this.#opts = opts;
589
- }
590
- start() {
591
- this.stop();
592
- this.#timer = setInterval(() => {
593
- this.#pingpong();
594
- }, this.#opts.pingIntervalMs);
595
- }
596
- stop() {
597
- if (this.#timer !== void 0) {
598
- clearInterval(this.#timer);
599
- this.#timer = void 0;
600
- }
601
- this.#abortCurrentPing();
602
- }
603
- hasWaiter(id) {
604
- return this.#waiters.has(id);
605
- }
606
- finishDummyPing(id) {
607
- const waiter = this.#waiters.get(id);
608
- if (!waiter) return false;
609
- this.#waiters.delete(id);
610
- waiter.resolve(true);
611
- try {
612
- this.#opts.send(["CLOSE", id]);
613
- } catch {}
614
- return true;
615
- }
616
- #abortCurrentPing() {
617
- this.#gen += 1;
618
- const native = this.#nativePing;
619
- this.#nativePing = void 0;
620
- native?.abort();
621
- for (const waiter of this.#waiters.values()) waiter.resolve(false);
622
- this.#waiters.clear();
623
- }
624
- async #pingpong() {
625
- const ws = this.#opts.getWs();
626
- if (!ws || ws.readyState !== WS_OPEN) return;
627
- if (this.#nativePing || this.#waiters.size > 0) return;
628
- const gen = this.#gen;
629
- const ping = canNativePing(ws) ? this.#waitForNativePing(ws) : this.#waitForDummyPing();
630
- const ok = await new Promise((resolve) => {
631
- let settled = false;
632
- const done = (alive) => {
633
- if (settled) return;
634
- settled = true;
635
- clearTimeout(timer);
636
- resolve(alive);
637
- };
638
- const timer = setTimeout(() => {
639
- if (this.#nativePing || this.#waiters.size > 0) done(false);
640
- }, this.#opts.pingTimeoutMs);
641
- ping.then(done);
642
- });
643
- if (gen !== this.#gen) return;
644
- if (!ok) {
645
- this.#abortCurrentPing();
646
- this.#opts.closeWs();
647
- }
648
- }
649
- #waitForNativePing(ws) {
650
- return new Promise((resolve) => {
651
- let settled = false;
652
- const finish = (alive) => {
653
- if (settled) return;
654
- settled = true;
655
- this.#nativePing = void 0;
656
- ws.off?.("pong", onPong);
657
- resolve(alive);
658
- };
659
- const onPong = () => finish(true);
660
- this.#nativePing = { abort: () => finish(false) };
661
- if (typeof ws.on === "function" && typeof ws.off === "function") ws.on("pong", onPong);
662
- else if (typeof ws.once === "function") ws.once("pong", onPong);
663
- else ws.on("pong", onPong);
664
- try {
665
- ws.ping();
666
- } catch {
667
- finish(false);
668
- }
669
- });
670
- }
671
- #waitForDummyPing() {
672
- return new Promise((resolve) => {
673
- const id = this.#opts.nextSubId("__ping__");
674
- this.#waiters.set(id, { resolve });
675
- try {
676
- this.#opts.send([
677
- "REQ",
678
- id,
679
- PING_DUMMY_FILTER
680
- ]);
681
- } catch {
682
- this.#waiters.delete(id);
683
- resolve(false);
684
- }
685
- });
686
- }
687
- };
688
- //#endregion
689
697
  //#region src/relay/relay.ts
698
+ /** Relay lifecycle states. */
690
699
  const RelayStatus = {
691
700
  Initialized: "initialized",
692
701
  Connecting: "connecting",
@@ -703,9 +712,10 @@ const DEFAULT_BACKOFF = [
703
712
  3e4,
704
713
  6e4
705
714
  ];
715
+ const noop = () => {};
706
716
  /**
707
- * Single-relay NIP-01 client.
708
- * Connection lifecycle, REQ/CLOSE, EVENT publish ACK, AUTH, COUNT, NIP-77, optional reconnect.
717
+ * Single-relay NIP-01 client. Connection lifecycle, REQ/CLOSE, EVENT publish ACK, AUTH, COUNT,
718
+ * NIP-77, optional reconnect.
709
719
  */
710
720
  var Relay = class Relay {
711
721
  url;
@@ -734,6 +744,10 @@ var Relay = class Relay {
734
744
  #serial = 0;
735
745
  #challenge;
736
746
  #authedChallenge;
747
+ /** Challenge value already answered on this connection; duplicates are not re-signed. */
748
+ #answeredChallenge;
749
+ /** Settled OK verdict for `#answeredChallenge`; set only once the relay replies. */
750
+ #answeredResult;
737
751
  #authPromise;
738
752
  #authSigner;
739
753
  #intentionalClose = false;
@@ -744,12 +758,12 @@ var Relay = class Relay {
744
758
  #deathHandled = false;
745
759
  #enablePing;
746
760
  #ping;
747
- onnotice = null;
748
- onclose = null;
761
+ onnotice;
762
+ onclose;
749
763
  /** Fired when the relay sends a NIP-42 AUTH challenge. */
750
- onauth = null;
764
+ onauth;
751
765
  /** Fired after a successful reconnect (not the initial connect). */
752
- onreconnect = null;
766
+ onreconnect;
753
767
  constructor(url, opts = {}) {
754
768
  this.url = normalizeURL(url);
755
769
  this.#WS = opts.websocketImplementation ?? getWebSocketImplementation();
@@ -805,6 +819,10 @@ var Relay = class Relay {
805
819
  get subscriptionCount() {
806
820
  return this.#subs.size;
807
821
  }
822
+ /** One-shot requests still awaiting a relay reply (EVENT ACK, COUNT, NEG). */
823
+ get inFlightCount() {
824
+ return this.#publishes.size + this.#counts.size + this.#neg.size;
825
+ }
808
826
  static async connect(url, opts) {
809
827
  const relay = new Relay(url, opts);
810
828
  await relay.connect({ signal: opts?.signal });
@@ -812,7 +830,11 @@ var Relay = class Relay {
812
830
  }
813
831
  async connect(opts) {
814
832
  if (this.#connected) return;
815
- if (this.#connecting) return await this.#connecting;
833
+ if (this.#connecting) {
834
+ await raceSignal(this.#connecting, opts?.signal);
835
+ return;
836
+ }
837
+ throwIfAborted(opts?.signal);
816
838
  const gen = ++this.#gen;
817
839
  this.#intentionalClose = false;
818
840
  this.#skipReconnect = false;
@@ -821,84 +843,64 @@ var Relay = class Relay {
821
843
  this.#status = RelayStatus.Connecting;
822
844
  const timeoutMs = opts?.timeoutMs ?? this.#connectTimeoutMs;
823
845
  const isReconnect = this.#reconnectAttempts > 0;
824
- let resolveConnect = () => {};
825
- let rejectConnect = (_err) => {};
846
+ let resolveConnect = noop;
847
+ let rejectConnect = noop;
826
848
  let settled = false;
827
- let timer;
828
849
  let ws;
829
- let handlers;
830
850
  const connecting = new Promise((resolve, reject) => {
831
851
  resolveConnect = resolve;
832
852
  rejectConnect = reject;
833
853
  });
834
854
  const release = () => {
835
- if (!ws) return;
836
- if (handlers) {
837
- try {
838
- ws.removeEventListener("open", handlers.onOpen);
839
- ws.removeEventListener("error", handlers.onError);
840
- ws.removeEventListener("close", handlers.onClose);
841
- ws.removeEventListener("message", handlers.onMessage);
842
- } catch {}
843
- if (this.#socketHandlers === handlers) this.#socketHandlers = void 0;
844
- }
855
+ if (ws === void 0) return;
856
+ try {
857
+ ws.removeEventListener("open", handlers.onOpen);
858
+ ws.removeEventListener("error", handlers.onError);
859
+ ws.removeEventListener("close", handlers.onClose);
860
+ ws.removeEventListener("message", handlers.onMessage);
861
+ } catch {}
862
+ if (this.#socketHandlers === handlers) this.#socketHandlers = void 0;
845
863
  try {
846
864
  if (ws.readyState !== this.#WS.CLOSED && ws.readyState !== this.#WS.CLOSING) ws.close();
847
865
  } catch {}
848
866
  if (this.#ws === ws) this.#ws = void 0;
849
867
  };
850
- const onAbort = () => {
851
- if (gen !== this.#gen) {
852
- release();
853
- return;
854
- }
855
- this.#intentionalClose = true;
856
- this.#skipReconnect = true;
857
- release();
858
- finish(new RelayConnectionError("connection aborted", this.url));
859
- if (gen === this.#gen) this.#handleSocketDeath("connection aborted", {
860
- fromConnectAttempt: true,
861
- gen
862
- });
863
- };
864
868
  const finish = (err) => {
865
869
  if (settled) return;
866
870
  settled = true;
867
- if (timer !== void 0) clearTimeout(timer);
871
+ clearTimeout(timer);
868
872
  if (this.#connectTimer === timer) this.#connectTimer = void 0;
869
- opts?.signal?.removeEventListener("abort", onAbort);
870
873
  if (this.#connectFinish === finish) this.#connectFinish = void 0;
871
874
  if (this.#connecting === connecting) this.#connecting = void 0;
872
- if (err) rejectConnect(err instanceof Error ? err : new RelayConnectionError("connection failed", this.url));
873
- else resolveConnect();
875
+ if (err === void 0) resolveConnect();
876
+ else rejectConnect(err);
874
877
  };
875
878
  this.#connecting = connecting;
876
879
  this.#connectFinish = finish;
877
- timer = setTimeout(() => {
880
+ const timer = setTimeout(() => {
878
881
  if (gen !== this.#gen) {
879
882
  release();
880
883
  return;
881
884
  }
882
885
  if (!isReconnect && !this.#enableReconnect) this.#skipReconnect = true;
883
886
  release();
884
- finish(new RelayConnectionError("connection timed out", this.url));
887
+ finish(new RelayTimeoutError("connection timed out", this.url));
885
888
  if (gen === this.#gen) this.#handleSocketDeath("connection timed out", {
886
889
  fromConnectAttempt: true,
887
890
  gen
888
891
  });
889
892
  }, timeoutMs);
890
893
  this.#connectTimer = timer;
891
- opts?.signal?.addEventListener("abort", onAbort, { once: true });
892
894
  try {
893
895
  ws = new this.#WS(this.url);
894
- } catch (err) {
896
+ } catch (error) {
895
897
  if (!isReconnect && !this.#enableReconnect) this.#skipReconnect = true;
896
- finish(err);
898
+ finish(error);
897
899
  if (gen === this.#gen) this.#handleSocketDeath("connection failed", {
898
900
  fromConnectAttempt: true,
899
901
  gen
900
902
  });
901
- await connecting;
903
+ await raceSignal(connecting, opts?.signal);
902
904
  return;
903
905
  }
904
906
  this.#ws = ws;
@@ -914,6 +916,8 @@ var Relay = class Relay {
914
916
  this.#challenge = void 0;
915
917
  this.#authPromise = void 0;
916
918
  this.#authedChallenge = void 0;
919
+ this.#answeredChallenge = void 0;
920
+ this.#answeredResult = void 0;
917
921
  if (!resubscribeAll(this.#live)) {
918
922
  this.#connected = false;
919
923
  this.#status = RelayStatus.Disconnected;
@@ -926,7 +930,7 @@ var Relay = class Relay {
926
930
  this.#reconnectAttempts = 0;
927
931
  if (this.#enablePing) this.#ping.start();
928
932
  else this.#ping.stop();
929
- if (wasReconnect) this.onreconnect?.();
933
+ if (wasReconnect) invokeSafely(() => this.onreconnect?.());
930
934
  finish();
931
935
  };
932
936
  const onError = () => {
@@ -964,7 +968,7 @@ var Relay = class Relay {
964
968
  const data = typeof ev === "object" && ev !== null && "data" in ev ? ev.data : ev;
965
969
  this.#onMessage(String(data));
966
970
  };
967
- handlers = {
971
+ const handlers = {
968
972
  onOpen,
969
973
  onError,
970
974
  onClose,
@@ -976,7 +980,7 @@ var Relay = class Relay {
976
980
  ws.addEventListener("error", onError);
977
981
  ws.addEventListener("close", onClose);
978
982
  ws.addEventListener("message", onMessage);
979
- await connecting;
983
+ await raceSignal(connecting, opts?.signal);
980
984
  }
981
985
  #detachSocketHandlers() {
982
986
  const h = this.#socketHandlers;
@@ -1011,7 +1015,7 @@ var Relay = class Relay {
1011
1015
  this.#detachSocketHandlers();
1012
1016
  this.#teardownSocket();
1013
1017
  this.#connected = false;
1014
- this.onclose?.();
1018
+ invokeSafely(() => this.onclose?.());
1015
1019
  }
1016
1020
  }
1017
1021
  #teardownSocket() {
@@ -1044,9 +1048,7 @@ var Relay = class Relay {
1044
1048
  for (const session of this.#neg.values()) failNegSession(session, err);
1045
1049
  this.#neg.clear();
1046
1050
  }
1047
- /**
1048
- * Unexpected socket death. Keep subscriptions if reconnecting.
1049
- */
1051
+ /** Unexpected socket death. Keep subscriptions if reconnecting. */
1050
1052
  #handleSocketDeath(reason, opts) {
1051
1053
  if (opts.gen !== this.#gen) return;
1052
1054
  if (this.#deathHandled) return;
@@ -1064,9 +1066,9 @@ var Relay = class Relay {
1064
1066
  return;
1065
1067
  }
1066
1068
  if (!this.#intentionalClose) this.#status = RelayStatus.Disconnected;
1067
- if (!opts.fromConnectAttempt || this.#subs.size > 0) {
1069
+ if (opts.fromConnectAttempt !== true || this.#subs.size > 0) {
1068
1070
  closeAllSubscriptions(this.#live, reason);
1069
- if (!this.#intentionalClose) this.onclose?.();
1071
+ if (!this.#intentionalClose) invokeSafely(() => this.onclose?.());
1070
1072
  }
1071
1073
  this.#status = RelayStatus.Closed;
1072
1074
  }
@@ -1078,9 +1080,13 @@ var Relay = class Relay {
1078
1080
  this.#reconnectTimer = setTimeout(() => {
1079
1081
  this.#reconnectTimer = void 0;
1080
1082
  if (this.#intentionalClose || this.#connected) return;
1081
- this.connect().catch(() => {
1082
- if (this.#enableReconnect && !this.#intentionalClose && !this.#skipReconnect && this.#subs.size > 0 && !this.#connected) this.#scheduleReconnect();
1083
- });
1083
+ (async () => {
1084
+ try {
1085
+ await this.connect();
1086
+ } catch {
1087
+ if (this.#enableReconnect && !this.#intentionalClose && !this.#skipReconnect && this.#subs.size > 0 && !this.#connected) this.#scheduleReconnect();
1088
+ }
1089
+ })();
1084
1090
  }, delay);
1085
1091
  }
1086
1092
  #send(message) {
@@ -1135,7 +1141,7 @@ var Relay = class Relay {
1135
1141
  const [, eventId, ok, message] = msg;
1136
1142
  const waiter = this.#publishes.get(eventId);
1137
1143
  if (!waiter) return;
1138
- if (!ok && isAuthRequired(message) && waiter.event && !waiter.authRetried && this.#authSigner) {
1144
+ if (!ok && isAuthRequired(message) && waiter.event !== void 0 && waiter.authRetried !== true && this.#authSigner) {
1139
1145
  waiter.authRetried = true;
1140
1146
  this.#authThenRepublish(waiter, eventId, message);
1141
1147
  return;
@@ -1169,14 +1175,23 @@ var Relay = class Relay {
1169
1175
  failNegErr(session, reason);
1170
1176
  break;
1171
1177
  }
1172
- case "NOTICE":
1173
- this.onnotice?.(msg[1]);
1178
+ case "NOTICE": {
1179
+ const [, notice] = msg;
1180
+ invokeSafely(() => this.onnotice?.(notice));
1174
1181
  break;
1175
- case "AUTH":
1176
- this.#challenge = msg[1];
1177
- this.#authPromise = void 0;
1178
- this.#authedChallenge = void 0;
1179
- this.onauth?.(msg[1]);
1182
+ }
1183
+ case "AUTH": {
1184
+ const [, authChallenge] = msg;
1185
+ if (authChallenge !== this.#challenge) {
1186
+ this.#authPromise = void 0;
1187
+ this.#authedChallenge = void 0;
1188
+ this.#answeredChallenge = void 0;
1189
+ this.#answeredResult = void 0;
1190
+ }
1191
+ this.#challenge = authChallenge;
1192
+ invokeSafely(() => this.onauth?.(authChallenge));
1193
+ break;
1194
+ }
1180
1195
  }
1181
1196
  }
1182
1197
  /** Low-level REQ with callbacks. Survives reconnect when enableReconnect is on. */
@@ -1191,27 +1206,25 @@ var Relay = class Relay {
1191
1206
  if (this.#verifyDead) return false;
1192
1207
  try {
1193
1208
  return this.#verify(event);
1194
- } catch (e) {
1195
- if (e instanceof WasmVerifyPoisonedError || e instanceof WebAssembly.RuntimeError) {
1209
+ } catch (error) {
1210
+ if (error instanceof WasmVerifyPoisonedError || error instanceof WebAssembly.RuntimeError) {
1196
1211
  this.#verifyDead = true;
1197
- this.onnotice?.("verify-poisoned: wasm instance aborted");
1212
+ invokeSafely(() => this.onnotice?.("verify-poisoned: wasm instance aborted"));
1198
1213
  return false;
1199
1214
  }
1200
- throw e;
1215
+ throw error;
1201
1216
  }
1202
1217
  }
1203
1218
  /** AsyncIterable of events for filters until the subscription is closed. */
1204
1219
  stream(filters, opts) {
1205
1220
  return streamFilters((f, o) => this.subscribe(f, o), filters, opts);
1206
1221
  }
1207
- /**
1208
- * One-shot query: collect events until EOSE or timeout, then close.
1209
- */
1222
+ /** One-shot query: collect events until EOSE or timeout, then close. */
1210
1223
  async fetch(filters, opts) {
1211
1224
  if (filters.length === 0) throw new MessageError("REQ requires at least one filter");
1212
- filters = canonicalizeFilters(filters);
1225
+ const canonical = canonicalizeFilters(filters);
1213
1226
  if (!this.#connected) await this.connect({ signal: opts?.signal });
1214
- return fetchFilters((f, o) => this.subscribe(f, o), filters, {
1227
+ return fetchFilters((f, o) => this.subscribe(f, o), canonical, {
1215
1228
  timeoutMs: opts?.timeoutMs ?? 4400,
1216
1229
  signal: opts?.signal,
1217
1230
  id: opts?.id,
@@ -1225,7 +1238,7 @@ var Relay = class Relay {
1225
1238
  return await new Promise((resolve, reject) => {
1226
1239
  const timer = setTimeout(() => {
1227
1240
  this.#publishes.delete(event.id);
1228
- reject(new RelayPublishError("publish timed out", this.url));
1241
+ reject(new RelayTimeoutError("publish timed out", this.url));
1229
1242
  }, timeoutMs);
1230
1243
  this.#publishes.set(event.id, {
1231
1244
  resolve,
@@ -1236,25 +1249,25 @@ var Relay = class Relay {
1236
1249
  });
1237
1250
  try {
1238
1251
  this.#send(["EVENT", event]);
1239
- } catch (err) {
1252
+ } catch (error) {
1240
1253
  clearTimeout(timer);
1241
1254
  this.#publishes.delete(event.id);
1242
- reject(err instanceof Error ? err : new RelayPublishError("publish failed", this.url));
1255
+ reject(error instanceof Error ? error : new RelayPublishError("publish failed", this.url));
1243
1256
  }
1244
1257
  });
1245
1258
  }
1246
1259
  /**
1247
- * NIP-45 COUNT: ask the relay how many events match `filters`.
1248
- * Resolves with the COUNT payload (count / optional approximate / optional hll).
1260
+ * NIP-45 COUNT: ask the relay how many events match `filters`. Resolves with the COUNT payload
1261
+ * (count / optional approximate / optional hll).
1249
1262
  */
1250
1263
  async count(filters, opts) {
1251
1264
  if (!this.#connected) throw new RelayClosedError("not connected", this.url);
1252
1265
  if (filters.length === 0) throw new RelayError("COUNT requires at least one filter", this.url);
1253
- if (opts?.signal?.aborted) throw new RelayConnectionError("count aborted", this.url);
1254
- filters = canonicalizeFilters(filters);
1255
- const id = opts?.id !== void 0 ? createSubscriptionId(opts.id) : this.nextSubId("count");
1266
+ throwIfAborted(opts?.signal);
1267
+ const canonical = canonicalizeFilters(filters);
1268
+ const id = opts?.id === void 0 ? this.nextSubId("count") : createSubscriptionId(opts.id);
1256
1269
  const timeoutMs = opts?.timeoutMs ?? this.#publishTimeoutMs;
1257
- return await new Promise((resolve, reject) => {
1270
+ return new Promise((resolve, reject) => {
1258
1271
  const cleanup = () => {
1259
1272
  opts?.signal?.removeEventListener("abort", onAbort);
1260
1273
  };
@@ -1274,13 +1287,15 @@ var Relay = class Relay {
1274
1287
  reject(err);
1275
1288
  },
1276
1289
  timer: void 0,
1277
- filters: [...filters],
1290
+ filters: [...canonical],
1278
1291
  authRetried: false,
1279
1292
  timeoutMs
1280
1293
  };
1281
- const onAbort = () => waiter.reject(new RelayConnectionError("count aborted", this.url));
1294
+ const onAbort = () => {
1295
+ if (opts?.signal) waiter.reject(abortReason(opts.signal));
1296
+ };
1282
1297
  waiter.timer = setTimeout(() => {
1283
- waiter.reject(new RelayPublishError("count timed out", this.url));
1298
+ waiter.reject(new RelayTimeoutError("count timed out", this.url));
1284
1299
  }, timeoutMs);
1285
1300
  this.#counts.set(id, waiter);
1286
1301
  opts?.signal?.addEventListener("abort", onAbort, { once: true });
@@ -1288,41 +1303,56 @@ var Relay = class Relay {
1288
1303
  this.#send([
1289
1304
  "COUNT",
1290
1305
  id,
1291
- ...filters
1306
+ ...canonical
1292
1307
  ]);
1293
- } catch (err) {
1294
- waiter.reject(err instanceof Error ? err : new RelayPublishError("count failed", this.url));
1308
+ } catch (error) {
1309
+ waiter.reject(error instanceof Error ? error : new RelayPublishError("count failed", this.url));
1295
1310
  }
1296
1311
  });
1297
1312
  }
1298
- /**
1299
- * NIP-42 AUTH: sign the current challenge and wait for OK.
1300
- */
1313
+ /** NIP-42 AUTH: sign the current challenge and wait for OK. */
1301
1314
  async auth(sign, opts) {
1302
1315
  const challenge = this.#challenge;
1303
- if (!challenge) throw new RelayError("no AUTH challenge received from relay", this.url);
1316
+ if (challenge === void 0 || challenge === "") throw new RelayError("no AUTH challenge received from relay", this.url);
1304
1317
  if (this.#authPromise) return this.#authPromise;
1318
+ if (this.#answeredChallenge === challenge && this.#answeredResult !== void 0) return this.#answeredResult;
1305
1319
  const pending = (async () => {
1306
- const event = await sign(makeAuthEvent(this.url, challenge));
1320
+ const template = makeAuthEvent(this.url, challenge);
1321
+ let event;
1322
+ try {
1323
+ event = await sign(template);
1324
+ } catch (error) {
1325
+ if (error instanceof NoSignerError) return {
1326
+ ok: false,
1327
+ message: "auth: no signer"
1328
+ };
1329
+ throw error;
1330
+ }
1307
1331
  if (!this.#connected) throw new RelayClosedError("not connected", this.url);
1308
1332
  const timeoutMs = opts?.timeoutMs ?? this.#publishTimeoutMs;
1309
- return await new Promise((resolve, reject) => {
1333
+ return new Promise((resolve, reject) => {
1310
1334
  const timer = setTimeout(() => {
1311
1335
  this.#publishes.delete(event.id);
1312
- reject(new RelayPublishError("auth timed out", this.url));
1336
+ reject(new RelayTimeoutError("auth timed out", this.url));
1313
1337
  }, timeoutMs);
1314
1338
  this.#publishes.set(event.id, {
1315
- resolve,
1339
+ resolve: (result) => {
1340
+ if (this.#challenge === challenge) {
1341
+ this.#answeredChallenge = challenge;
1342
+ this.#answeredResult = result;
1343
+ }
1344
+ resolve(result);
1345
+ },
1316
1346
  reject,
1317
1347
  timer,
1318
1348
  timeoutMs
1319
1349
  });
1320
1350
  try {
1321
1351
  this.#send(["AUTH", event]);
1322
- } catch (err) {
1352
+ } catch (error) {
1323
1353
  clearTimeout(timer);
1324
1354
  this.#publishes.delete(event.id);
1325
- reject(err instanceof Error ? err : new RelayPublishError("auth send failed", this.url));
1355
+ reject(error instanceof Error ? error : new RelayPublishError("auth send failed", this.url));
1326
1356
  }
1327
1357
  });
1328
1358
  })();
@@ -1335,17 +1365,32 @@ var Relay = class Relay {
1335
1365
  if (this.#authPromise === pending) this.#authPromise = void 0;
1336
1366
  }
1337
1367
  }
1368
+ /**
1369
+ * Clear a cached AUTH rejection for the current challenge so a later `auth()` signs again. A
1370
+ * successful auth and in-flight answers are kept. When a challenge is pending and unanswered,
1371
+ * `onauth` is re-fired so the pool's automatic auth can run once more.
1372
+ */
1373
+ resetAuth() {
1374
+ if (this.#answeredResult !== void 0 && !this.#answeredResult.ok) {
1375
+ this.#answeredChallenge = void 0;
1376
+ this.#answeredResult = void 0;
1377
+ }
1378
+ if (this.#challenge !== void 0 && this.#challenge !== this.#authedChallenge) {
1379
+ const challenge = this.#challenge;
1380
+ invokeSafely(() => this.onauth?.(challenge));
1381
+ }
1382
+ }
1338
1383
  async #ensureAuthed() {
1339
1384
  const signer = this.#authSigner;
1340
1385
  if (!signer) return false;
1341
1386
  for (let i = 0; i < 3; i++) {
1342
- if (!this.#challenge) return false;
1387
+ if (this.#challenge === void 0 || this.#challenge === "") return false;
1343
1388
  if (this.#authedChallenge === this.#challenge) return true;
1344
1389
  const signed = this.#challenge;
1345
1390
  const result = await this.auth(signer);
1346
1391
  if (this.#authedChallenge === this.#challenge) return true;
1347
1392
  if (!result.ok) {
1348
- if (!this.#challenge || this.#challenge === signed) return false;
1393
+ if (this.#challenge === void 0 || this.#challenge === "" || this.#challenge === signed) return false;
1349
1394
  continue;
1350
1395
  }
1351
1396
  }
@@ -1393,16 +1438,16 @@ var Relay = class Relay {
1393
1438
  return;
1394
1439
  }
1395
1440
  waiter.timer = setTimeout(() => {
1396
- waiter.reject(new RelayPublishError("count timed out", this.url));
1441
+ waiter.reject(new RelayTimeoutError("count timed out", this.url));
1397
1442
  }, waiter.timeoutMs);
1398
1443
  this.#send([
1399
1444
  "COUNT",
1400
1445
  id,
1401
1446
  ...waiter.filters
1402
1447
  ]);
1403
- } catch (err) {
1448
+ } catch (error) {
1404
1449
  if (!this.#counts.has(id)) return;
1405
- waiter.reject(err instanceof Error ? err : new RelayClosedError(reason || "COUNT closed", this.url));
1450
+ waiter.reject(error instanceof Error ? error : new RelayClosedError(reason || "COUNT closed", this.url));
1406
1451
  }
1407
1452
  }
1408
1453
  async #authThenRepublish(waiter, eventId, message) {
@@ -1441,7 +1486,7 @@ var Relay = class Relay {
1441
1486
  }
1442
1487
  waiter.timer = setTimeout(() => {
1443
1488
  this.#publishes.delete(eventId);
1444
- waiter.reject(new RelayPublishError("publish timed out", this.url));
1489
+ waiter.reject(new RelayTimeoutError("publish timed out", this.url));
1445
1490
  }, waiter.timeoutMs);
1446
1491
  this.#send(["EVENT", waiter.event]);
1447
1492
  } catch {
@@ -1453,19 +1498,17 @@ var Relay = class Relay {
1453
1498
  }
1454
1499
  }
1455
1500
  /**
1456
- * NIP-77: run Negentropy reconciliation against this relay.
1457
- * Returns the local-only (`have`) and remote-only (`need`) event ids.
1458
- * Does not upload or download events.
1501
+ * NIP-77: run Negentropy reconciliation against this relay. Returns the local-only (`have`) and
1502
+ * remote-only (`need`) event ids. Does not upload or download events.
1459
1503
  *
1460
- * `timeoutMs` is a single wall-clock deadline for the whole session
1461
- * (`NEG-OPEN` through the last `NEG-MSG`), not a per-message budget.
1462
- * Default: {@link RelayOptions.publishTimeoutMs}.
1504
+ * `timeoutMs` is a single wall-clock deadline for the whole session (`NEG-OPEN` through the last
1505
+ * `NEG-MSG`), not a per-message budget. Default: {@link RelayOptions.publishTimeoutMs}.
1463
1506
  */
1464
1507
  async negReconcile(filter, storage, opts) {
1465
1508
  if (!this.#connected) throw new RelayClosedError("not connected", this.url);
1466
- if (opts?.signal?.aborted) throw new RelayConnectionError("negentropy aborted", this.url);
1467
- filter = canonicalizeFilter(filter);
1468
- const id = opts?.id !== void 0 ? assertSubscriptionId(opts.id) : this.nextSubId("neg");
1509
+ throwIfAborted(opts?.signal);
1510
+ const canonical = canonicalizeFilter(filter);
1511
+ const id = opts?.id === void 0 ? this.nextSubId("neg") : assertSubscriptionId(opts.id);
1469
1512
  const timeoutMs = opts?.timeoutMs ?? this.#publishTimeoutMs;
1470
1513
  const prev = this.#neg.get(id);
1471
1514
  if (prev) failNegSession(prev, new Nip77Error("closed: replaced by new NEG-OPEN"));
@@ -1475,7 +1518,7 @@ var Relay = class Relay {
1475
1518
  return await runWiredNegSession({
1476
1519
  session,
1477
1520
  storage,
1478
- filter,
1521
+ filter: canonical,
1479
1522
  id,
1480
1523
  timeoutMs,
1481
1524
  signal: opts?.signal,
@@ -1500,9 +1543,8 @@ var Relay = class Relay {
1500
1543
  //#endregion
1501
1544
  //#region src/relay/fan-in.ts
1502
1545
  /**
1503
- * Aggregate EOSE/close across routed jobs. One `seen` set.
1504
- * `pending` counts URL list entries (duplicates included).
1505
- * `pendingEose` counts unique `jobIndex:url` keys.
1546
+ * Aggregate EOSE/close across routed jobs. One `seen` set. `pending` counts URL list entries
1547
+ * (duplicates included). `pendingEose` counts unique `jobIndex:url` keys.
1506
1548
  */
1507
1549
  function fanIn(pool, jobs, opts = {}) {
1508
1550
  const seen = /* @__PURE__ */ new Set();
@@ -1521,7 +1563,7 @@ function fanIn(pool, jobs, opts = {}) {
1521
1563
  clearTimeout(eoseTimer);
1522
1564
  eoseTimer = void 0;
1523
1565
  }
1524
- opts.oneose?.();
1566
+ invokeSafely(() => opts.oneose?.());
1525
1567
  };
1526
1568
  const markEose = (jobIndex, url) => {
1527
1569
  const key = `${jobIndex}:${url}`;
@@ -1541,24 +1583,39 @@ function fanIn(pool, jobs, opts = {}) {
1541
1583
  if (closed) return;
1542
1584
  settleClose();
1543
1585
  for (const c of closers) c.close(reason);
1544
- opts.onclose?.(reason ?? "closed by client");
1586
+ invokeSafely(() => opts.onclose?.(reason ?? "closed by client"));
1545
1587
  };
1546
- if (opts.signal?.aborted) {
1588
+ if (opts.signal?.aborted === true) {
1547
1589
  closed = true;
1548
- opts.onclose?.("aborted");
1590
+ invokeSafely(() => opts.onclose?.("aborted"));
1549
1591
  return { close: closeAll };
1550
1592
  }
1551
1593
  opts.signal?.addEventListener("abort", () => closeAll("aborted"), { once: true });
1594
+ const failUrl = (jobIndex, key) => {
1595
+ markEose(jobIndex, key);
1596
+ pending -= 1;
1597
+ if (pending <= 0 && closers.length === 0 && !closed) {
1598
+ settleClose();
1599
+ invokeSafely(() => opts.onclose?.("all relays failed"));
1600
+ }
1601
+ };
1552
1602
  const attach = (relay, job, jobIndex) => {
1553
1603
  if (closed) return;
1604
+ const received = opts.receivedEvent;
1554
1605
  const sub = relay.subscribe([...job.filters], {
1555
1606
  id: jobs.length === 1 ? job.id : void 0,
1556
1607
  closeOnEose: opts.closeOnEose,
1557
- alreadyHaveEvent: (id) => Boolean(opts.alreadyHaveEvent?.(id) || seen.has(id)),
1558
- receivedEvent: opts.receivedEvent,
1608
+ alreadyHaveEvent: (id) => {
1609
+ let have = seen.has(id);
1610
+ invokeSafely(() => {
1611
+ have = have || Boolean(opts.alreadyHaveEvent?.(id));
1612
+ });
1613
+ return have;
1614
+ },
1615
+ receivedEvent: received === void 0 ? void 0 : (id) => invokeSafely(() => received(id, relay.url)),
1559
1616
  onevent: (event) => {
1560
1617
  seen.add(event.id);
1561
- opts.onevent?.(event);
1618
+ invokeSafely(() => opts.onevent?.(event, relay.url));
1562
1619
  },
1563
1620
  oneose: () => markEose(jobIndex, relay.url),
1564
1621
  onclose: (reason) => {
@@ -1566,14 +1623,14 @@ function fanIn(pool, jobs, opts = {}) {
1566
1623
  pending -= 1;
1567
1624
  if (pending <= 0 && !closed) {
1568
1625
  settleClose();
1569
- opts.onclose?.(reason);
1626
+ invokeSafely(() => opts.onclose?.(reason));
1570
1627
  }
1571
1628
  }
1572
1629
  });
1573
1630
  closers.push(sub);
1574
1631
  };
1575
- for (let jobIndex = 0; jobIndex < jobs.length; jobIndex++) {
1576
- const job = jobs[jobIndex];
1632
+ for (const [jobIndex, job] of jobs.entries()) {
1633
+ const jobUrls = /* @__PURE__ */ new Set();
1577
1634
  for (const url of job.urls) {
1578
1635
  let key;
1579
1636
  try {
@@ -1581,42 +1638,43 @@ function fanIn(pool, jobs, opts = {}) {
1581
1638
  } catch {
1582
1639
  key = url;
1583
1640
  }
1641
+ if (jobUrls.has(key)) continue;
1642
+ jobUrls.add(key);
1584
1643
  pending += 1;
1585
1644
  const eoseKey = `${jobIndex}:${key}`;
1586
1645
  if (!eoseAttempted.has(eoseKey)) {
1587
1646
  eoseAttempted.add(eoseKey);
1588
1647
  pendingEose += 1;
1589
1648
  }
1590
- const failUrl = () => {
1591
- markEose(jobIndex, key);
1592
- pending -= 1;
1593
- if (pending <= 0 && closers.length === 0 && !closed) {
1594
- settleClose();
1595
- opts.onclose?.("all relays failed");
1596
- }
1597
- };
1598
1649
  const tryAttach = (relay) => {
1599
1650
  try {
1600
1651
  attach(relay, job, jobIndex);
1601
- } catch (err) {
1602
- if (err instanceof RelayClosedError) {
1603
- failUrl();
1652
+ } catch (error) {
1653
+ if (error instanceof RelayClosedError) {
1654
+ failUrl(jobIndex, key);
1604
1655
  return;
1605
1656
  }
1606
- throw err;
1657
+ throw error;
1607
1658
  }
1608
1659
  };
1609
- pool.ensureRelay(url, {
1610
- signal: opts.signal,
1611
- timeoutMs: opts.connectTimeoutMs
1612
- }).then((relay) => tryAttach(relay), () => {
1613
- const relay = pool.getRelay(key);
1614
- if (relay) {
1615
- tryAttach(relay);
1660
+ (async () => {
1661
+ let relay;
1662
+ try {
1663
+ relay = await pool.ensureRelay(key, {
1664
+ signal: opts.signal,
1665
+ timeoutMs: opts.connectTimeoutMs
1666
+ });
1667
+ } catch {
1668
+ const existing = pool.getRelay(key);
1669
+ if (existing !== void 0) {
1670
+ tryAttach(existing);
1671
+ return;
1672
+ }
1673
+ failUrl(jobIndex, key);
1616
1674
  return;
1617
1675
  }
1618
- failUrl();
1619
- });
1676
+ tryAttach(relay);
1677
+ })();
1620
1678
  }
1621
1679
  }
1622
1680
  if (opts.eoseTimeoutMs !== void 0 && pending > 0) eoseTimer = setTimeout(() => {
@@ -1626,31 +1684,53 @@ function fanIn(pool, jobs, opts = {}) {
1626
1684
  if (pending === 0) queueMicrotask(() => {
1627
1685
  if (closed) return;
1628
1686
  settleClose();
1629
- opts.onclose?.("no relays");
1687
+ invokeSafely(() => opts.onclose?.("no relays"));
1630
1688
  });
1631
1689
  return { close: closeAll };
1632
1690
  }
1633
1691
  async function fetchRouted(pool, jobs, opts = {}) {
1692
+ throwIfAborted(opts.signal);
1634
1693
  const byId = /* @__PURE__ */ new Map();
1635
- await Promise.all(jobs.flatMap((job) => job.urls.map(async (url) => {
1636
- try {
1637
- const batch = await (await pool.ensureRelay(url, {
1638
- signal: opts.signal,
1639
- timeoutMs: opts.connectTimeoutMs
1640
- })).fetch([...job.filters], {
1641
- timeoutMs: opts.timeoutMs,
1642
- signal: opts.signal
1643
- });
1694
+ await Promise.all(jobs.flatMap((job) => {
1695
+ const urls = [];
1696
+ for (const raw of job.urls) {
1697
+ let key;
1698
+ try {
1699
+ key = normalizeURL(raw);
1700
+ } catch {
1701
+ key = raw;
1702
+ }
1703
+ if (!urls.includes(key)) urls.push(key);
1704
+ }
1705
+ return urls.map(async (url) => {
1706
+ let batch;
1707
+ let relayUrl;
1708
+ try {
1709
+ const relay = await pool.ensureRelay(url, {
1710
+ signal: opts.signal,
1711
+ timeoutMs: opts.connectTimeoutMs
1712
+ });
1713
+ relayUrl = relay.url;
1714
+ batch = await relay.fetch([...job.filters], {
1715
+ timeoutMs: opts.timeoutMs,
1716
+ signal: opts.signal
1717
+ });
1718
+ } catch {
1719
+ if (opts.signal?.aborted === true) throw abortReason(opts.signal);
1720
+ return;
1721
+ }
1644
1722
  for (const event of batch) if (!byId.has(event.id)) byId.set(event.id, event);
1645
- } catch {}
1646
- })));
1723
+ for (const event of batch) invokeSafely(() => opts.onevent?.(event, relayUrl));
1724
+ });
1725
+ }));
1647
1726
  return [...byId.values()];
1648
1727
  }
1649
1728
  //#endregion
1650
1729
  //#region src/relay/pool.ts
1651
- /**
1652
- * Multi-relay coordinator: connection reuse, cross-relay event dedup, fan-out publish.
1653
- */
1730
+ function isIdle(relay) {
1731
+ return relay.status !== RelayStatus.Connecting && relay.subscriptionCount === 0 && relay.inFlightCount === 0;
1732
+ }
1733
+ /** Multi-relay coordinator: connection reuse, cross-relay event dedup, fan-out publish. */
1654
1734
  var Pool = class {
1655
1735
  #relays = /* @__PURE__ */ new Map();
1656
1736
  #opts;
@@ -1659,10 +1739,12 @@ var Pool = class {
1659
1739
  #idleTimer;
1660
1740
  #allowInsecure;
1661
1741
  #trustedInsecure;
1742
+ #pinned;
1662
1743
  constructor(opts = {}) {
1663
1744
  this.#opts = opts;
1664
- this.#allowInsecure = opts.allowInsecure ?? true;
1745
+ this.#allowInsecure = opts.allowInsecure ?? false;
1665
1746
  this.#trustedInsecure = new Set((opts.trustedInsecureUrls ?? []).map(normalizeURL));
1747
+ this.#pinned = new Set((opts.pinnedUrls ?? []).map(normalizeURL));
1666
1748
  this.#idleTimeoutMs = opts.idleTimeoutMs ?? 0;
1667
1749
  if (this.#idleTimeoutMs > 0) this.#idleTimer = setInterval(() => this.cleanIdleRelays(), opts.idleCleanupIntervalMs ?? 3e4);
1668
1750
  }
@@ -1672,19 +1754,29 @@ var Pool = class {
1672
1754
  setTrustedInsecureUrls(urls) {
1673
1755
  this.#trustedInsecure = new Set(urls.map(normalizeURL));
1674
1756
  }
1757
+ setPinnedUrls(urls) {
1758
+ this.#pinned = new Set(urls.map(normalizeURL));
1759
+ }
1760
+ /**
1761
+ * Drop cached AUTH rejections and re-fire pending challenges on every pooled relay, so a later
1762
+ * `setSigner` takes effect without reconnect.
1763
+ */
1764
+ resetAuth() {
1765
+ for (const relay of this.#relays.values()) relay.resetAuth();
1766
+ }
1675
1767
  cleanIdleRelays() {
1676
1768
  if (this.#idleTimeoutMs <= 0) return;
1677
1769
  const now = Date.now();
1678
1770
  const idle = [];
1679
1771
  for (const [url, relay] of this.#relays) {
1680
- if (relay.subscriptionCount > 0) continue;
1772
+ if (this.#pinned.has(url) || !isIdle(relay)) continue;
1681
1773
  const last = this.#lastActivity.get(url) ?? 0;
1682
1774
  if (!relay.connected || now - last >= this.#idleTimeoutMs) idle.push(url);
1683
1775
  }
1684
1776
  for (const url of this.#lastActivity.keys()) if (!this.#relays.has(url)) this.#lastActivity.delete(url);
1685
1777
  if (idle.length === 0) return;
1686
1778
  this.close(idle);
1687
- this.#opts.onIdleRelaysClosed?.(idle);
1779
+ invokeSafely(() => this.#opts.onIdleRelaysClosed?.(idle));
1688
1780
  }
1689
1781
  #touch(url) {
1690
1782
  this.#lastActivity.set(url, Date.now());
@@ -1700,11 +1792,35 @@ var Pool = class {
1700
1792
  clearInterval(this.#idleTimer);
1701
1793
  this.#idleTimer = void 0;
1702
1794
  }
1795
+ /**
1796
+ * Soft cap: closing the least-recently-used idle non-pinned relay when a new non-pinned relay
1797
+ * would push the count past `maxRelays`. Busy relays are never evicted; with none idle the cap is
1798
+ * exceeded rather than failing.
1799
+ */
1800
+ #enforceMaxRelays(incoming) {
1801
+ const cap = this.#opts.maxRelays;
1802
+ if (cap === void 0 || this.#pinned.has(incoming)) return;
1803
+ let count = 0;
1804
+ let oldestUrl;
1805
+ let oldestAt = Number.POSITIVE_INFINITY;
1806
+ for (const [url, relay] of this.#relays) {
1807
+ if (this.#pinned.has(url)) continue;
1808
+ count += 1;
1809
+ if (!isIdle(relay)) continue;
1810
+ const at = this.#lastActivity.get(url) ?? 0;
1811
+ if (at < oldestAt) {
1812
+ oldestAt = at;
1813
+ oldestUrl = url;
1814
+ }
1815
+ }
1816
+ if (count >= cap && oldestUrl !== void 0) this.close([oldestUrl]);
1817
+ }
1703
1818
  async ensureRelay(url, opts) {
1704
1819
  const norm = normalizeURL(url);
1705
1820
  this.#rejectInsecure(url, norm);
1706
1821
  let relay = this.#relays.get(norm);
1707
1822
  if (!relay) {
1823
+ this.#enforceMaxRelays(norm);
1708
1824
  const signFn = this.#opts.automaticallyAuth?.(norm) ?? void 0;
1709
1825
  const created = new Relay(norm, {
1710
1826
  websocketImplementation: this.#opts.websocketImplementation,
@@ -1723,7 +1839,11 @@ var Pool = class {
1723
1839
  this.#lastActivity.delete(norm);
1724
1840
  };
1725
1841
  if (signFn) created.onauth = () => {
1726
- created.auth(signFn).catch(() => {});
1842
+ (async () => {
1843
+ try {
1844
+ await created.auth(signFn);
1845
+ } catch {}
1846
+ })();
1727
1847
  };
1728
1848
  this.#relays.set(norm, created);
1729
1849
  relay = created;
@@ -1734,12 +1854,12 @@ var Pool = class {
1734
1854
  signal: opts?.signal,
1735
1855
  timeoutMs: opts?.timeoutMs ?? this.#opts.connectTimeoutMs
1736
1856
  });
1737
- } catch (err) {
1738
- if (!this.#opts.enableReconnect) {
1857
+ } catch (error) {
1858
+ if (this.#opts.enableReconnect !== true) {
1739
1859
  this.#relays.delete(norm);
1740
1860
  this.#lastActivity.delete(norm);
1741
1861
  }
1742
- throw err;
1862
+ throw error;
1743
1863
  }
1744
1864
  return relay;
1745
1865
  }
@@ -1759,16 +1879,16 @@ var Pool = class {
1759
1879
  }
1760
1880
  }
1761
1881
  /**
1762
- * Subscribe across relays. Deduplicates by event id.
1763
- * Returns a closer; callbacks receive every new event once.
1882
+ * Subscribe across relays. Deduplicates by event id. Returns a closer; callbacks receive every
1883
+ * new event once.
1764
1884
  */
1765
1885
  subscribe(relays, filters, opts = {}) {
1766
1886
  if (filters.length === 0) throw new MessageError("REQ requires at least one filter");
1767
1887
  if (opts.id !== void 0) assertSubscriptionId(opts.id);
1768
- filters = canonicalizeFilters(filters);
1888
+ const canonical = canonicalizeFilters(filters);
1769
1889
  return fanIn(this, [{
1770
1890
  urls: relays,
1771
- filters,
1891
+ filters: canonical,
1772
1892
  id: opts.id
1773
1893
  }], {
1774
1894
  onevent: opts.onevent,
@@ -1791,7 +1911,8 @@ var Pool = class {
1791
1911
  }], {
1792
1912
  timeoutMs: opts?.timeoutMs,
1793
1913
  signal: opts?.signal,
1794
- connectTimeoutMs: this.#opts.connectTimeoutMs
1914
+ connectTimeoutMs: this.#opts.connectTimeoutMs,
1915
+ onevent: opts?.onevent
1795
1916
  });
1796
1917
  }
1797
1918
  /** Publish to all listed relays; returns per-relay outcomes. */
@@ -1805,10 +1926,10 @@ var Pool = class {
1805
1926
  url: relay.url,
1806
1927
  result
1807
1928
  };
1808
- } catch (err) {
1929
+ } catch (error) {
1809
1930
  return {
1810
1931
  url,
1811
- error: err instanceof Error ? err.message : String(err)
1932
+ error: error instanceof Error ? error.message : String(error)
1812
1933
  };
1813
1934
  }
1814
1935
  }));
@@ -1827,11 +1948,12 @@ var Pool = class {
1827
1948
  }));
1828
1949
  }
1829
1950
  /**
1830
- * NIP-45 COUNT across relays. Per-relay outcomes; failures do not throw.
1831
- * Counts are not summed — each relay reports independently (may overlap).
1951
+ * NIP-45 COUNT across relays. Per-relay outcomes; failures do not throw. Counts are not summed —
1952
+ * each relay reports independently (may overlap).
1832
1953
  */
1833
1954
  async count(relays, filters, opts) {
1834
- filters = canonicalizeFilters(filters);
1955
+ throwIfAborted(opts?.signal);
1956
+ const canonical = canonicalizeFilters(filters);
1835
1957
  return await Promise.all(relays.map(async (url) => {
1836
1958
  try {
1837
1959
  const relay = await this.ensureRelay(url, {
@@ -1839,20 +1961,22 @@ var Pool = class {
1839
1961
  timeoutMs: this.#opts.connectTimeoutMs
1840
1962
  });
1841
1963
  this.#touch(relay.url);
1842
- const payload = await relay.count(filters, {
1964
+ const payload = await relay.count(canonical, {
1843
1965
  timeoutMs: opts?.timeoutMs,
1844
1966
  signal: opts?.signal
1845
1967
  });
1846
- return {
1968
+ const out = {
1847
1969
  url: relay.url,
1848
- count: payload.count,
1849
- approximate: payload.approximate,
1850
- hll: payload.hll
1970
+ count: payload.count
1851
1971
  };
1852
- } catch (err) {
1972
+ if (payload.approximate !== void 0) out.approximate = payload.approximate;
1973
+ if (payload.hll !== void 0) out.hll = payload.hll;
1974
+ return out;
1975
+ } catch (error) {
1976
+ if (opts?.signal?.aborted === true) throw abortReason(opts.signal);
1853
1977
  return {
1854
1978
  url,
1855
- error: err instanceof Error ? err.message : String(err)
1979
+ error: error instanceof Error ? error.message : String(error)
1856
1980
  };
1857
1981
  }
1858
1982
  }));
@@ -1876,6 +2000,6 @@ var Pool = class {
1876
2000
  }
1877
2001
  };
1878
2002
  //#endregion
1879
- export { RelayStatus as a, isInsecureRelayUrl as c, RelayClosedError as d, RelayConnectionError as f, Relay as i, getWebSocketImplementation as l, RelayPublishError as m, fanIn as n, Subscription as o, RelayError as p, fetchRouted as r, subscriptionToAsyncIterable as s, Pool as t, useWebSocketImplementation as u };
2003
+ export { RelayStatus as a, isInsecureRelayUrl as c, RelayClosedError as d, RelayConnectionError as f, RelayTimeoutError as h, Relay as i, getWebSocketImplementation as l, RelayPublishError as m, fanIn as n, Subscription as o, RelayError as p, fetchRouted as r, subscriptionToAsyncIterable as s, Pool as t, useWebSocketImplementation as u };
1880
2004
 
1881
- //# sourceMappingURL=pool-C_j1OmBB.mjs.map
2005
+ //# sourceMappingURL=pool-XmAGaufS.mjs.map