@qntx/nostr 0.1.0 → 0.2.1

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 (186) hide show
  1. package/README.md +16 -0
  2. package/dist/abort-Ba8tDrOI.mjs +38 -0
  3. package/dist/abort-Ba8tDrOI.mjs.map +1 -0
  4. package/dist/{builder-BphJa-Y-.d.mts → builder-BWn55Paf.d.mts} +20 -14
  5. package/dist/{builder-q0VsvnYE.mjs → builder-CyrGKRVP.mjs} +32 -16
  6. package/dist/builder-CyrGKRVP.mjs.map +1 -0
  7. package/dist/{client-Bd8B1mYm.mjs → client-5DhgYV7P.mjs} +372 -195
  8. package/dist/client-5DhgYV7P.mjs.map +1 -0
  9. package/dist/client.d.mts +1 -1
  10. package/dist/client.mjs +1 -1
  11. package/dist/core.d.mts +8 -8
  12. package/dist/core.mjs +8 -9
  13. package/dist/error-BW7re1UN.d.mts +9 -0
  14. package/dist/error-B__OMBRY.mjs +14 -0
  15. package/dist/error-B__OMBRY.mjs.map +1 -0
  16. package/dist/{error-WEFS-R-4.d.mts → error-CR8Urc84.d.mts} +20 -8
  17. package/dist/{error-CwBxle6N.mjs → error-DrEIM7FO.mjs} +5 -3
  18. package/dist/error-DrEIM7FO.mjs.map +1 -0
  19. package/dist/{event-CzoQEFMF.mjs → event-D8Nfoh4K.mjs} +40 -32
  20. package/dist/event-D8Nfoh4K.mjs.map +1 -0
  21. package/dist/{event-B5D2jxt_.d.mts → event-fU3Y01PR.d.mts} +23 -20
  22. package/dist/{filter-BxwjEkZp.mjs → filter-BQl3GlHt.mjs} +42 -35
  23. package/dist/filter-BQl3GlHt.mjs.map +1 -0
  24. package/dist/filter-C0IMByE3.d.mts +37 -0
  25. package/dist/{gossip-C5RVB1UB.d.mts → gossip-BRQwIM_q.d.mts} +25 -23
  26. package/dist/{gossip-3xkRRCm_.mjs → gossip-oloDMWEj.mjs} +52 -34
  27. package/dist/gossip-oloDMWEj.mjs.map +1 -0
  28. package/dist/gossip.d.mts +1 -1
  29. package/dist/gossip.mjs +1 -1
  30. package/dist/{http-6vevbBlu.d.mts → http-C4HwIu4B.d.mts} +7 -7
  31. package/dist/{http-D2rhokZo.mjs → http-CuazNRIP.mjs} +10 -6
  32. package/dist/http-CuazNRIP.mjs.map +1 -0
  33. package/dist/index-B-CGNDPS.d.mts +207 -0
  34. package/dist/index-BIhSADwK.d.mts +339 -0
  35. package/dist/{index-BYMnhRIC.d.mts → index-Ba7bJ7DD.d.mts} +68 -50
  36. package/dist/{index-DaRyjo7O.d.mts → index-Boe8FshM.d.mts} +13 -7
  37. package/dist/index-D-TK_KJG.d.mts +84 -0
  38. package/dist/index-D1bqFHKB.d.mts +188 -0
  39. package/dist/index.d.mts +22 -20
  40. package/dist/index.mjs +19 -18
  41. package/dist/{key-Dn-aX4x4.mjs → key-Bbtys6o7.mjs} +22 -14
  42. package/dist/key-Bbtys6o7.mjs.map +1 -0
  43. package/dist/{key-BiGxGLhg.d.mts → key-BlsYhN1V.d.mts} +9 -6
  44. package/dist/{kind-BLl4cLOc.mjs → kind-Cxwhhkx8.mjs} +2 -1
  45. package/dist/kind-Cxwhhkx8.mjs.map +1 -0
  46. package/dist/{loaders-BNQszDAn.mjs → loaders-o6YXB_Ri.mjs} +329 -305
  47. package/dist/loaders-o6YXB_Ri.mjs.map +1 -0
  48. package/dist/loaders.d.mts +3 -3
  49. package/dist/loaders.mjs +2 -2
  50. package/dist/memory-C2EVx0_O.mjs +46 -0
  51. package/dist/memory-C2EVx0_O.mjs.map +1 -0
  52. package/dist/{memory-BCmhZPek.mjs → memory-index-Do1mz0WF.mjs} +212 -82
  53. package/dist/memory-index-Do1mz0WF.mjs.map +1 -0
  54. package/dist/{message-C_Uj-Y-c.mjs → message-DcY6Fi9o.mjs} +38 -24
  55. package/dist/message-DcY6Fi9o.mjs.map +1 -0
  56. package/dist/{message-DJC-Jz6T.d.mts → message-iGPIqnn0.d.mts} +15 -8
  57. package/dist/{nip17-BWq-Kfq5.d.mts → nip17-DNEkz8rM.d.mts} +18 -17
  58. package/dist/{nip19-D7905oIv.d.mts → nip19-BevCJZsp.d.mts} +23 -3
  59. package/dist/{nip46-C_DoxiS_.d.mts → nip46-DDV4dC56.d.mts} +13 -12
  60. package/dist/{nip59-DljW_sYh.d.mts → nip59-zQgdLcEr.d.mts} +28 -25
  61. package/dist/{nip65-Di5hZRuV.d.mts → nip65-DBfh2smt.d.mts} +8 -3
  62. package/dist/{nip77-ByxlgoQO.d.mts → nip77-DH4RhS5r.d.mts} +9 -8
  63. package/dist/nips/blossom.d.mts +37 -37
  64. package/dist/nips/blossom.mjs +67 -52
  65. package/dist/nips/blossom.mjs.map +1 -1
  66. package/dist/nips/nip04.d.mts +2 -3
  67. package/dist/nips/nip04.mjs +6 -10
  68. package/dist/nips/nip04.mjs.map +1 -1
  69. package/dist/nips/nip05.d.mts +37 -43
  70. package/dist/nips/nip05.mjs +57 -57
  71. package/dist/nips/nip05.mjs.map +1 -1
  72. package/dist/nips/nip10.d.mts +18 -19
  73. package/dist/nips/nip10.mjs +76 -61
  74. package/dist/nips/nip10.mjs.map +1 -1
  75. package/dist/nips/nip11.d.mts +9 -8
  76. package/dist/nips/nip11.mjs +14 -11
  77. package/dist/nips/nip11.mjs.map +1 -1
  78. package/dist/nips/nip13.d.mts +9 -9
  79. package/dist/nips/nip13.mjs +15 -17
  80. package/dist/nips/nip13.mjs.map +1 -1
  81. package/dist/nips/nip17.d.mts +1 -1
  82. package/dist/nips/nip17.mjs +17 -12
  83. package/dist/nips/nip17.mjs.map +1 -1
  84. package/dist/nips/nip19.d.mts +1 -1
  85. package/dist/nips/nip19.mjs +52 -29
  86. package/dist/nips/nip19.mjs.map +1 -1
  87. package/dist/nips/nip21.d.mts +5 -6
  88. package/dist/nips/nip21.mjs +8 -7
  89. package/dist/nips/nip21.mjs.map +1 -1
  90. package/dist/nips/nip27.d.mts +7 -8
  91. package/dist/nips/nip27.mjs +26 -25
  92. package/dist/nips/nip27.mjs.map +1 -1
  93. package/dist/nips/nip42.d.mts +5 -6
  94. package/dist/nips/nip42.mjs +3 -3
  95. package/dist/nips/nip42.mjs.map +1 -1
  96. package/dist/nips/nip44.d.mts +24 -7
  97. package/dist/nips/nip44.mjs +25 -15
  98. package/dist/nips/nip44.mjs.map +1 -1
  99. package/dist/nips/nip46.d.mts +1 -1
  100. package/dist/nips/nip46.mjs +52 -49
  101. package/dist/nips/nip46.mjs.map +1 -1
  102. package/dist/nips/nip49.d.mts +8 -7
  103. package/dist/nips/nip49.mjs +16 -12
  104. package/dist/nips/nip49.mjs.map +1 -1
  105. package/dist/nips/nip51.d.mts +25 -27
  106. package/dist/nips/nip51.mjs +47 -36
  107. package/dist/nips/nip51.mjs.map +1 -1
  108. package/dist/nips/nip57.d.mts +12 -13
  109. package/dist/nips/nip57.mjs +35 -38
  110. package/dist/nips/nip57.mjs.map +1 -1
  111. package/dist/nips/nip59.d.mts +1 -1
  112. package/dist/nips/nip59.mjs +14 -18
  113. package/dist/nips/nip59.mjs.map +1 -1
  114. package/dist/nips/nip65.d.mts +1 -1
  115. package/dist/nips/nip65.mjs +11 -8
  116. package/dist/nips/nip65.mjs.map +1 -1
  117. package/dist/nips/nip77.d.mts +1 -1
  118. package/dist/nips/nip77.mjs +56 -34
  119. package/dist/nips/nip77.mjs.map +1 -1
  120. package/dist/nips/nip96.d.mts +21 -12
  121. package/dist/nips/nip96.mjs +59 -36
  122. package/dist/nips/nip96.mjs.map +1 -1
  123. package/dist/nips/nip98.d.mts +12 -12
  124. package/dist/nips/nip98.mjs +18 -19
  125. package/dist/nips/nip98.mjs.map +1 -1
  126. package/dist/pool-B7p_reAm.d.mts +333 -0
  127. package/dist/{pool-C_j1OmBB.mjs → pool-C2xOcsun.mjs} +792 -568
  128. package/dist/pool-C2xOcsun.mjs.map +1 -0
  129. package/dist/reactive-DDlThDSZ.mjs +324 -0
  130. package/dist/reactive-DDlThDSZ.mjs.map +1 -0
  131. package/dist/reactive-DFpqtIf_.d.mts +66 -0
  132. package/dist/relay.d.mts +5 -4
  133. package/dist/relay.mjs +2 -2
  134. package/dist/report-CHld4uWj.mjs +32 -0
  135. package/dist/report-CHld4uWj.mjs.map +1 -0
  136. package/dist/{signer-CZ6K6FUc.mjs → signer-CKR9EHce.mjs} +107 -72
  137. package/dist/signer-CKR9EHce.mjs.map +1 -0
  138. package/dist/signer.d.mts +3 -3
  139. package/dist/signer.mjs +3 -2
  140. package/dist/storage-xXxLGCVb.mjs +1376 -0
  141. package/dist/storage-xXxLGCVb.mjs.map +1 -0
  142. package/dist/storage.d.mts +4 -4
  143. package/dist/storage.mjs +5 -4
  144. package/dist/store.d.mts +2 -0
  145. package/dist/store.mjs +2 -0
  146. package/dist/testing.d.mts +135 -0
  147. package/dist/testing.mjs +963 -0
  148. package/dist/testing.mjs.map +1 -0
  149. package/dist/types-2pCUprRU.d.mts +47 -0
  150. package/dist/types-CRQEk0Oi.d.mts +19 -0
  151. package/dist/util-LfQirgDk.mjs +113 -0
  152. package/dist/util-LfQirgDk.mjs.map +1 -0
  153. package/dist/wasm.d.mts +4 -4
  154. package/dist/wasm.mjs +54 -55
  155. package/dist/wasm.mjs.map +1 -1
  156. package/dist/websocket-CmeqvAkp.d.mts +27 -0
  157. package/package.json +93 -39
  158. package/dist/builder-q0VsvnYE.mjs.map +0 -1
  159. package/dist/client-Bd8B1mYm.mjs.map +0 -1
  160. package/dist/error-B49rbDzT.d.mts +0 -7
  161. package/dist/error-CwBxle6N.mjs.map +0 -1
  162. package/dist/error-DZEs5OuZ.mjs +0 -24
  163. package/dist/error-DZEs5OuZ.mjs.map +0 -1
  164. package/dist/event-CzoQEFMF.mjs.map +0 -1
  165. package/dist/filter-BxwjEkZp.mjs.map +0 -1
  166. package/dist/filter-DUEKP3q1.d.mts +0 -40
  167. package/dist/gossip-3xkRRCm_.mjs.map +0 -1
  168. package/dist/http-D2rhokZo.mjs.map +0 -1
  169. package/dist/index-C0C4e-Gn.d.mts +0 -55
  170. package/dist/index-DgOugrpF.d.mts +0 -275
  171. package/dist/index-EwL6fHLo.d.mts +0 -267
  172. package/dist/index-k2YhCgsh.d.mts +0 -57
  173. package/dist/key-Dn-aX4x4.mjs.map +0 -1
  174. package/dist/kind-BLl4cLOc.mjs.map +0 -1
  175. package/dist/loaders-BNQszDAn.mjs.map +0 -1
  176. package/dist/memory-BCmhZPek.mjs.map +0 -1
  177. package/dist/message-C_Uj-Y-c.mjs.map +0 -1
  178. package/dist/pool-C_j1OmBB.mjs.map +0 -1
  179. package/dist/pool-DhRi7l-b.d.mts +0 -297
  180. package/dist/signer-CZ6K6FUc.mjs.map +0 -1
  181. package/dist/storage-DOy0iLRE.mjs +0 -870
  182. package/dist/storage-DOy0iLRE.mjs.map +0 -1
  183. package/dist/types-BzyADqnw.d.mts +0 -19
  184. package/dist/types-Dn24TPFs.d.mts +0 -43
  185. package/dist/util-6tfLQ9JF.mjs +0 -73
  186. package/dist/util-6tfLQ9JF.mjs.map +0 -1
@@ -1,26 +1,57 @@
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
+ };
36
+ /**
37
+ * A relay was suspended for delivering more events that fail id/signature verification than its
38
+ * {@link PoolOptions.invalidEventPolicy} allows. Carries `until`, the epoch-ms time when the relay
39
+ * may connect again.
40
+ */
41
+ var RelaySuspendedError = class extends RelayError {
42
+ name = "RelaySuspendedError";
43
+ /** Epoch milliseconds when the suspension lifts. */
44
+ until;
45
+ constructor(url, until) {
46
+ super("relay suspended for delivering invalid events", url);
47
+ this.until = until;
48
+ }
49
+ };
19
50
  //#endregion
20
51
  //#region src/relay/websocket.ts
21
52
  let impl;
22
53
  try {
23
- if (typeof globalThis.WebSocket !== "undefined") impl = globalThis.WebSocket;
54
+ if (globalThis.WebSocket !== void 0) impl = globalThis.WebSocket;
24
55
  } catch {}
25
56
  /** Inject a WebSocket implementation (required on Node unless undici/global is present). */
26
57
  function useWebSocketImplementation(websocketImplementation) {
@@ -33,8 +64,8 @@ function getWebSocketImplementation() {
33
64
  //#endregion
34
65
  //#region src/relay/url.ts
35
66
  /**
36
- * True for `ws:` / `http:` URLs that are not `.onion`.
37
- * Local-network `ws://` is still insecure; allow those via a trusted set.
67
+ * True for `ws:` / `http:` URLs that are not `.onion`. Local-network `ws://` is still insecure;
68
+ * allow those via a trusted set.
38
69
  */
39
70
  function isInsecureRelayUrl(url) {
40
71
  try {
@@ -46,9 +77,211 @@ function isInsecureRelayUrl(url) {
46
77
  }
47
78
  }
48
79
  //#endregion
80
+ //#region src/relay/neg-session.ts
81
+ function createNegSession() {
82
+ return {
83
+ queue: [],
84
+ waiter: void 0,
85
+ error: void 0
86
+ };
87
+ }
88
+ function pushNegMsg(session, hex) {
89
+ if (session.waiter) {
90
+ const { waiter } = session;
91
+ session.waiter = void 0;
92
+ waiter.resolve(hex);
93
+ } else session.queue.push(hex);
94
+ }
95
+ function failNegSession(session, err) {
96
+ session.error = err;
97
+ if (session.waiter) {
98
+ const { waiter } = session;
99
+ session.waiter = void 0;
100
+ waiter.reject(err);
101
+ }
102
+ }
103
+ function failNegErr(session, reason) {
104
+ failNegSession(session, new Nip77Error(reason));
105
+ }
106
+ /** Wire queue/timeout/abort around `runNegSession`. Not a second session class. */
107
+ async function runWiredNegSession(opts) {
108
+ const { session, storage, filter, id, timeoutMs, signal, send, url } = opts;
109
+ const deadline = Date.now() + timeoutMs;
110
+ const timedOut = () => new RelayTimeoutError("negentropy timed out", url);
111
+ const remainingMs = () => deadline - Date.now();
112
+ const next = async () => {
113
+ if (session.error !== void 0) throw session.error;
114
+ if (remainingMs() <= 0) throw timedOut();
115
+ const queued = session.queue.shift();
116
+ if (queued !== void 0) return queued;
117
+ return new Promise((resolve, reject) => {
118
+ const timer = setTimeout(() => {
119
+ session.waiter = void 0;
120
+ reject(timedOut());
121
+ }, remainingMs());
122
+ const fail = (err) => {
123
+ clearTimeout(timer);
124
+ session.waiter = void 0;
125
+ reject(err);
126
+ };
127
+ const onAbort = () => {
128
+ if (signal) fail(abortReason(signal));
129
+ };
130
+ session.waiter = {
131
+ resolve: (hex) => {
132
+ clearTimeout(timer);
133
+ signal?.removeEventListener("abort", onAbort);
134
+ resolve(hex);
135
+ },
136
+ reject: (err) => fail(err)
137
+ };
138
+ signal?.addEventListener("abort", onAbort, { once: true });
139
+ });
140
+ };
141
+ return runNegSession({
142
+ storage,
143
+ openingSend: (hex) => {
144
+ send([
145
+ "NEG-OPEN",
146
+ id,
147
+ filter,
148
+ hex
149
+ ]);
150
+ },
151
+ msgSend: (hex) => {
152
+ send([
153
+ "NEG-MSG",
154
+ id,
155
+ hex
156
+ ]);
157
+ },
158
+ next
159
+ });
160
+ }
161
+ const PING_DUMMY_FILTER = {
162
+ ids: ["a".repeat(64)],
163
+ limit: 0
164
+ };
165
+ /** Browser WebSocket and `ws` both use 1 for OPEN. */
166
+ const WS_OPEN = 1;
167
+ /** Node `ws` delivers `pong` on the EventEmitter, not via addEventListener. */
168
+ function canNativePing(ws) {
169
+ return typeof ws.ping === "function" && (typeof ws.once === "function" || typeof ws.on === "function");
170
+ }
171
+ /** REQ-dummy or native ping/pong keepalive. Does not import class Relay. */
172
+ var PingLoop = class {
173
+ #opts;
174
+ #timer;
175
+ #waiters = /* @__PURE__ */ new Map();
176
+ #gen = 0;
177
+ #nativePing;
178
+ constructor(opts) {
179
+ this.#opts = opts;
180
+ }
181
+ start() {
182
+ this.stop();
183
+ this.#timer = setInterval(() => {
184
+ this.#pingpong();
185
+ }, this.#opts.pingIntervalMs);
186
+ }
187
+ stop() {
188
+ if (this.#timer !== void 0) {
189
+ clearInterval(this.#timer);
190
+ this.#timer = void 0;
191
+ }
192
+ this.#abortCurrentPing();
193
+ }
194
+ hasWaiter(id) {
195
+ return this.#waiters.has(id);
196
+ }
197
+ finishDummyPing(id) {
198
+ const waiter = this.#waiters.get(id);
199
+ if (!waiter) return false;
200
+ this.#waiters.delete(id);
201
+ waiter.resolve(true);
202
+ try {
203
+ this.#opts.send(["CLOSE", id]);
204
+ } catch {}
205
+ return true;
206
+ }
207
+ #abortCurrentPing() {
208
+ this.#gen += 1;
209
+ const native = this.#nativePing;
210
+ this.#nativePing = void 0;
211
+ native?.abort();
212
+ for (const waiter of this.#waiters.values()) waiter.resolve(false);
213
+ this.#waiters.clear();
214
+ }
215
+ async #pingpong() {
216
+ const ws = this.#opts.getWs();
217
+ if (!ws || ws.readyState !== WS_OPEN) return;
218
+ if (this.#nativePing !== void 0 || this.#waiters.size > 0) return;
219
+ const gen = this.#gen;
220
+ const ping = canNativePing(ws) ? this.#waitForNativePing(ws) : this.#waitForDummyPing();
221
+ const ok = await new Promise((resolve) => {
222
+ let settled = false;
223
+ const done = (alive) => {
224
+ if (settled) return;
225
+ settled = true;
226
+ clearTimeout(timer);
227
+ resolve(alive);
228
+ };
229
+ const timer = setTimeout(() => {
230
+ if (this.#nativePing !== void 0 || this.#waiters.size > 0) done(false);
231
+ }, this.#opts.pingTimeoutMs);
232
+ (async () => {
233
+ done(await ping);
234
+ })();
235
+ });
236
+ if (gen !== this.#gen) return;
237
+ if (!ok) {
238
+ this.#abortCurrentPing();
239
+ this.#opts.closeWs();
240
+ }
241
+ }
242
+ async #waitForNativePing(ws) {
243
+ return new Promise((resolve) => {
244
+ let settled = false;
245
+ const finish = (alive) => {
246
+ if (settled) return;
247
+ settled = true;
248
+ this.#nativePing = void 0;
249
+ ws.off?.("pong", onPong);
250
+ resolve(alive);
251
+ };
252
+ const onPong = () => finish(true);
253
+ this.#nativePing = { abort: () => finish(false) };
254
+ if (typeof ws.on === "function" && typeof ws.off === "function") ws.on("pong", onPong);
255
+ else if (typeof ws.once === "function") ws.once("pong", onPong);
256
+ else ws.on?.("pong", onPong);
257
+ try {
258
+ if (typeof ws.ping === "function") ws.ping();
259
+ else finish(false);
260
+ } catch {
261
+ finish(false);
262
+ }
263
+ });
264
+ }
265
+ async #waitForDummyPing() {
266
+ return new Promise((resolve) => {
267
+ const id = this.#opts.nextSubId("__ping__");
268
+ this.#waiters.set(id, { resolve });
269
+ try {
270
+ this.#opts.send([
271
+ "REQ",
272
+ id,
273
+ PING_DUMMY_FILTER
274
+ ]);
275
+ } catch {
276
+ this.#waiters.delete(id);
277
+ resolve(false);
278
+ }
279
+ });
280
+ }
281
+ };
282
+ //#endregion
49
283
  //#region src/relay/subscription.ts
50
284
  var Subscription = class {
51
- sendClose;
52
285
  id;
53
286
  filters;
54
287
  handlers;
@@ -61,9 +294,10 @@ var Subscription = class {
61
294
  lastCreatedAt;
62
295
  /** Event ids at `lastCreatedAt` (same-second reconnect dedup). Not all seen ids. */
63
296
  idsAtWatermark = /* @__PURE__ */ new Set();
297
+ #sendClose;
64
298
  #abort;
65
299
  constructor(filters, opts, sendClose) {
66
- this.sendClose = sendClose;
300
+ this.#sendClose = sendClose;
67
301
  this.id = createSubscriptionId(opts.id);
68
302
  this.filters = filters;
69
303
  this.closeOnEose = opts.closeOnEose === true;
@@ -87,8 +321,8 @@ var Subscription = class {
87
321
  if (this.closed) return;
88
322
  this.closed = true;
89
323
  this.#abort?.();
90
- this.sendClose(this.id);
91
- this.handlers.onclose?.(reason);
324
+ this.#sendClose(this.id);
325
+ invokeSafely(() => this.handlers.onclose?.(reason));
92
326
  }
93
327
  /** Advance the reconnect watermark after a verified EVENT. */
94
328
  noteVerified(event) {
@@ -101,8 +335,8 @@ var Subscription = class {
101
335
  if (event.created_at === this.lastCreatedAt) this.idsAtWatermark.add(event.id);
102
336
  }
103
337
  /**
104
- * Filters for re-REQ. Original `filters` stay unchanged.
105
- * NIP-01 `since` is inclusive — never `lastCreatedAt + 1`.
338
+ * Filters for re-REQ. Original `filters` stay unchanged. NIP-01 `since` is inclusive — never
339
+ * `lastCreatedAt + 1`.
106
340
  */
107
341
  replayFilters() {
108
342
  const since = this.lastCreatedAt;
@@ -120,65 +354,62 @@ function subscriptionToAsyncIterable(start, opts) {
120
354
  let error;
121
355
  let wake;
122
356
  let closer;
357
+ let localClose = false;
123
358
  const notify = () => {
124
359
  wake?.();
125
360
  wake = void 0;
126
361
  };
362
+ const waitForWake = async () => new Promise((resolve) => {
363
+ wake = resolve;
364
+ });
365
+ const closeLocal = (reason) => {
366
+ localClose = true;
367
+ closer?.close(reason);
368
+ done = true;
369
+ notify();
370
+ };
127
371
  closer = start({
128
372
  onevent(event) {
129
373
  queue.push(event);
130
374
  notify();
131
375
  },
132
376
  oneose() {
133
- if (opts?.includeEose === false) {
134
- done = true;
135
- closer?.close("eose");
136
- notify();
137
- }
377
+ if (opts?.includeEose === false) closeLocal("eose");
138
378
  },
139
379
  onclose(reason) {
140
- if (reason && reason !== "eose" && reason !== "closed by client" && reason !== "aborted") error = new RelayClosedError(reason);
380
+ if (opts?.signal?.aborted === true) localClose = true;
381
+ if (!localClose && reason) error = new RelayClosedError(reason);
141
382
  done = true;
142
383
  notify();
143
384
  }
144
385
  });
145
386
  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 });
387
+ if (opts.signal.aborted) closeLocal("aborted");
388
+ else opts.signal.addEventListener("abort", () => closeLocal("aborted"), { once: true });
154
389
  }
155
390
  return {
156
391
  close(reason) {
157
- closer?.close(reason);
158
- done = true;
159
- notify();
392
+ closeLocal(reason ?? "closed by client");
160
393
  },
161
394
  [Symbol.asyncIterator]() {
162
395
  return {
163
396
  async next() {
164
397
  while (true) {
165
- if (queue.length > 0) return {
166
- value: queue.shift(),
398
+ const value = queue.shift();
399
+ if (value !== void 0) return {
400
+ value,
167
401
  done: false
168
402
  };
169
- if (error) throw error;
403
+ if (error !== void 0) throw error;
170
404
  if (done) return {
171
405
  value: void 0,
172
406
  done: true
173
407
  };
174
- await new Promise((resolve) => {
175
- wake = resolve;
176
- });
408
+ await waitForWake();
177
409
  }
178
410
  },
179
411
  async return() {
180
- closer?.close("iterator returned");
181
- done = true;
412
+ closeLocal("iterator returned");
182
413
  return {
183
414
  value: void 0,
184
415
  done: true
@@ -190,18 +421,6 @@ function subscriptionToAsyncIterable(start, opts) {
190
421
  }
191
422
  //#endregion
192
423
  //#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
424
  function openExclusive(ctx, filters, opts) {
206
425
  const sub = new Subscription(filters, opts, (id) => {
207
426
  ctx.subs.delete(id);
@@ -267,7 +486,7 @@ function subscribeLive(ctx, filters, opts) {
267
486
  if (group.sub.eosed) queueMicrotask(() => {
268
487
  if (handle.closed || handle.eosed) return;
269
488
  handle.eosed = true;
270
- handle.handlers.oneose?.();
489
+ invokeSafely(() => handle.handlers.oneose?.());
271
490
  });
272
491
  return handle;
273
492
  }
@@ -290,70 +509,54 @@ function detachLive(ctx, fp, handle) {
290
509
  function endLiveGroup(ctx, fp, opts) {
291
510
  const group = ctx.liveByFp.get(fp);
292
511
  if (!group) return;
293
- const id = group.sub.id;
512
+ const { id } = group.sub;
294
513
  const remaining = [...group.attachments];
295
514
  group.attachments.clear();
296
515
  forgetLiveGroup(ctx, group);
297
516
  if (opts.sendClose) try {
298
517
  if (ctx.connected()) ctx.send(["CLOSE", id]);
299
518
  } catch {}
300
- const errors = [];
301
- for (const att of remaining) captureListenerError(errors, () => {
519
+ for (const att of remaining) invokeSafely(() => {
302
520
  att.close(opts.reason);
303
521
  });
304
- flushListenerErrors(errors);
305
522
  }
306
523
  function deliverLiveEvent(ctx, group, event) {
307
- const sub = group.sub;
524
+ const { sub } = group;
308
525
  const attachments = [...group.attachments];
309
- const errors = [];
310
- for (const att of attachments) captureListenerError(errors, () => {
526
+ for (const att of attachments) invokeSafely(() => {
311
527
  att.handlers.receivedEvent?.(event.id);
312
528
  });
313
- if (sub.idsAtWatermark.has(event.id)) {
314
- flushListenerErrors(errors);
315
- return;
316
- }
529
+ if (sub.idsAtWatermark.has(event.id)) return;
317
530
  const recipients = [];
318
531
  for (const att of attachments) {
319
532
  if (att.closed) continue;
320
533
  let skip = false;
321
- captureListenerError(errors, () => {
534
+ invokeSafely(() => {
322
535
  skip = Boolean(att.handlers.alreadyHaveEvent?.(event.id));
323
536
  });
324
537
  if (!skip) recipients.push(att);
325
538
  }
326
- if (recipients.length === 0) {
327
- flushListenerErrors(errors);
328
- return;
329
- }
330
- if (!ctx.acceptEvent(event)) {
331
- flushListenerErrors(errors);
332
- return;
333
- }
539
+ if (recipients.length === 0) return;
540
+ if (!ctx.acceptEvent(event)) return;
334
541
  sub.noteVerified(event);
335
542
  for (const att of recipients) att.noteVerified(event);
336
- for (const att of recipients) captureListenerError(errors, () => {
543
+ for (const att of recipients) invokeSafely(() => {
337
544
  att.handlers.onevent?.(event);
338
545
  });
339
- flushListenerErrors(errors);
340
546
  }
341
547
  function deliverLiveEose(group) {
342
- const attachments = Array.from(group.attachments);
343
- const errors = [];
548
+ const attachments = [...group.attachments];
344
549
  for (const att of attachments) {
345
550
  if (att.closed || att.eosed) continue;
346
551
  att.eosed = true;
347
- captureListenerError(errors, () => {
552
+ invokeSafely(() => {
348
553
  att.handlers.oneose?.();
349
554
  });
350
555
  }
351
- flushListenerErrors(errors);
352
556
  }
353
557
  function closeAllSubscriptions(ctx, reason) {
354
- const errors = [];
355
- const fps = Array.from(ctx.liveByFp.keys());
356
- for (const fp of fps) captureListenerError(errors, () => {
558
+ const fps = [...ctx.liveByFp.keys()];
559
+ for (const fp of fps) invokeSafely(() => {
357
560
  endLiveGroup(ctx, fp, {
358
561
  sendClose: false,
359
562
  reason
@@ -361,14 +564,13 @@ function closeAllSubscriptions(ctx, reason) {
361
564
  });
362
565
  for (const sub of ctx.subs.values()) if (!sub.closed) {
363
566
  sub.closed = true;
364
- captureListenerError(errors, () => {
567
+ invokeSafely(() => {
365
568
  sub.handlers.onclose?.(reason);
366
569
  });
367
570
  }
368
571
  ctx.subs.clear();
369
572
  ctx.liveByFp.clear();
370
573
  ctx.liveBySubId.clear();
371
- flushListenerErrors(errors);
372
574
  }
373
575
  function dropSubscription(ctx, sub, reason) {
374
576
  const group = ctx.liveBySubId.get(sub.id);
@@ -381,7 +583,9 @@ function dropSubscription(ctx, sub, reason) {
381
583
  }
382
584
  ctx.subs.delete(sub.id);
383
585
  sub.closed = true;
384
- sub.handlers.onclose?.(reason);
586
+ invokeSafely(() => {
587
+ sub.handlers.onclose?.(reason);
588
+ });
385
589
  }
386
590
  function onSubEvent(ctx, subId, event) {
387
591
  const sub = ctx.subs.get(subId);
@@ -391,302 +595,121 @@ function onSubEvent(ctx, subId, event) {
391
595
  deliverLiveEvent(ctx, group, event);
392
596
  return;
393
597
  }
394
- sub.handlers.receivedEvent?.(event.id);
395
- if (sub.idsAtWatermark.has(event.id)) return;
396
- if (sub.handlers.alreadyHaveEvent?.(event.id)) return;
397
- if (!ctx.acceptEvent(event)) return;
398
- sub.noteVerified(event);
399
- sub.handlers.onevent?.(event);
400
- }
401
- function onSubEose(ctx, subId) {
402
- const sub = ctx.subs.get(subId);
403
- if (!sub || sub.closed) return;
404
- if (sub.eosed) return;
405
- sub.eosed = true;
406
- const group = ctx.liveBySubId.get(subId);
407
- if (group) deliverLiveEose(group);
408
- else {
409
- sub.handlers.oneose?.();
410
- if (sub.closeOnEose) sub.close("eose");
411
- }
412
- }
413
- function resubscribeAll(ctx) {
414
- for (const sub of ctx.subs.values()) {
415
- if (sub.closed) continue;
416
- sub.eosed = false;
417
- sub.authRetried = false;
418
- const group = ctx.liveBySubId.get(sub.id);
419
- if (group) for (const att of group.attachments) att.eosed = false;
420
- try {
421
- ctx.send([
422
- "REQ",
423
- sub.id,
424
- ...sub.replayFilters()
425
- ]);
426
- } catch {
427
- return false;
428
- }
429
- }
430
- return true;
431
- }
432
- function streamFilters(subscribe, filters, opts) {
433
- return subscriptionToAsyncIterable((handlers) => subscribe(filters, {
434
- ...handlers,
435
- id: opts?.id,
436
- signal: opts?.signal
437
- }), { signal: opts?.signal });
438
- }
439
- async function fetchFilters(subscribe, filters, opts) {
440
- const events = [];
441
- const seen = /* @__PURE__ */ new Set();
442
- await new Promise((resolve, reject) => {
443
- let settled = false;
444
- const done = (err) => {
445
- if (settled) return;
446
- settled = true;
447
- clearTimeout(timer);
448
- sub.close(err ? err.message : "fetch complete");
449
- if (err) reject(err);
450
- else resolve();
451
- };
452
- const timer = setTimeout(() => done(), opts.timeoutMs);
453
- const sub = subscribe(filters, {
454
- id: opts.id,
455
- signal: opts.signal,
456
- closeOnEose: true,
457
- onevent(event) {
458
- if (seen.has(event.id)) return;
459
- seen.add(event.id);
460
- events.push(event);
461
- },
462
- oneose() {
463
- done();
464
- },
465
- onclose() {
466
- if (!settled) done();
467
- }
468
- });
469
- opts.signal?.addEventListener("abort", () => done(new RelayConnectionError("fetch aborted", opts.url)), { once: true });
470
- });
471
- return events;
472
- }
473
- function armEoseTimeout(sub, eoseTimeoutMs) {
474
- const timer = setTimeout(() => {
475
- if (sub.eosed || sub.closed) return;
476
- sub.eosed = true;
477
- sub.handlers.oneose?.();
478
- }, eoseTimeoutMs);
479
- const prevClose = sub.handlers.onclose;
480
- sub.handlers.onclose = (reason) => {
481
- clearTimeout(timer);
482
- prevClose?.(reason);
483
- };
484
- const prevEose = sub.handlers.oneose;
485
- sub.handlers.oneose = () => {
486
- clearTimeout(timer);
487
- prevEose?.();
488
- };
489
- }
490
- //#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);
598
+ invokeSafely(() => {
599
+ sub.handlers.receivedEvent?.(event.id);
600
+ });
601
+ if (sub.idsAtWatermark.has(event.id)) return;
602
+ let have = false;
603
+ invokeSafely(() => {
604
+ have = Boolean(sub.handlers.alreadyHaveEvent?.(event.id));
605
+ });
606
+ if (have) return;
607
+ if (!ctx.acceptEvent(event)) return;
608
+ sub.noteVerified(event);
609
+ invokeSafely(() => {
610
+ sub.handlers.onevent?.(event);
611
+ });
612
+ }
613
+ function onSubEose(ctx, subId) {
614
+ const sub = ctx.subs.get(subId);
615
+ if (!sub || sub.closed) return;
616
+ if (sub.eosed) return;
617
+ sub.eosed = true;
618
+ const group = ctx.liveBySubId.get(subId);
619
+ if (group) deliverLiveEose(group);
620
+ else {
621
+ invokeSafely(() => {
622
+ sub.handlers.oneose?.();
642
623
  });
643
- if (gen !== this.#gen) return;
644
- if (!ok) {
645
- this.#abortCurrentPing();
646
- this.#opts.closeWs();
624
+ if (sub.closeOnEose) sub.close("eose");
625
+ }
626
+ }
627
+ function resubscribeAll(ctx) {
628
+ for (const sub of ctx.subs.values()) {
629
+ if (sub.closed) continue;
630
+ sub.eosed = false;
631
+ sub.authRetried = false;
632
+ const group = ctx.liveBySubId.get(sub.id);
633
+ if (group) for (const att of group.attachments) att.eosed = false;
634
+ try {
635
+ ctx.send([
636
+ "REQ",
637
+ sub.id,
638
+ ...sub.replayFilters()
639
+ ]);
640
+ } catch {
641
+ return false;
647
642
  }
648
643
  }
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);
644
+ return true;
645
+ }
646
+ function streamFilters(subscribe, filters, opts) {
647
+ return subscriptionToAsyncIterable((handlers) => subscribe(filters, {
648
+ ...handlers,
649
+ id: opts?.id,
650
+ signal: opts?.signal
651
+ }), { signal: opts?.signal });
652
+ }
653
+ async function fetchFilters(subscribe, filters, opts) {
654
+ throwIfAborted(opts.signal);
655
+ const events = [];
656
+ const seen = /* @__PURE__ */ new Set();
657
+ await new Promise((resolve, reject) => {
658
+ let settled = false;
659
+ const done = (err) => {
660
+ if (settled) return;
661
+ settled = true;
662
+ clearTimeout(timer);
663
+ let reason = "aborted";
664
+ if (err instanceof Error) reason = err.message;
665
+ else if (err === void 0) reason = "fetch complete";
666
+ sub.close(reason);
667
+ if (err === void 0) resolve();
668
+ else reject(err);
669
+ };
670
+ const timer = setTimeout(() => done(), opts.timeoutMs);
671
+ const sub = subscribe(filters, {
672
+ id: opts.id,
673
+ signal: opts.signal,
674
+ closeOnEose: true,
675
+ onevent(event) {
676
+ if (seen.has(event.id)) return;
677
+ seen.add(event.id);
678
+ events.push(event);
679
+ },
680
+ oneose() {
681
+ done();
682
+ },
683
+ onclose() {
684
+ if (!settled) done(opts.signal?.aborted === true ? abortReason(opts.signal) : void 0);
668
685
  }
669
686
  });
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
- }
687
+ opts.signal?.addEventListener("abort", () => done(opts.signal ? abortReason(opts.signal) : void 0), { once: true });
688
+ });
689
+ return events;
690
+ }
691
+ function armEoseTimeout(sub, eoseTimeoutMs) {
692
+ const timer = setTimeout(() => {
693
+ if (sub.eosed || sub.closed) return;
694
+ sub.eosed = true;
695
+ invokeSafely(() => {
696
+ sub.handlers.oneose?.();
685
697
  });
686
- }
687
- };
698
+ }, eoseTimeoutMs);
699
+ const prevClose = sub.handlers.onclose;
700
+ sub.handlers.onclose = (reason) => {
701
+ clearTimeout(timer);
702
+ prevClose?.(reason);
703
+ };
704
+ const prevEose = sub.handlers.oneose;
705
+ sub.handlers.oneose = () => {
706
+ clearTimeout(timer);
707
+ prevEose?.();
708
+ };
709
+ }
688
710
  //#endregion
689
711
  //#region src/relay/relay.ts
712
+ /** Relay lifecycle states. */
690
713
  const RelayStatus = {
691
714
  Initialized: "initialized",
692
715
  Connecting: "connecting",
@@ -703,9 +726,10 @@ const DEFAULT_BACKOFF = [
703
726
  3e4,
704
727
  6e4
705
728
  ];
729
+ const noop = () => {};
706
730
  /**
707
- * Single-relay NIP-01 client.
708
- * Connection lifecycle, REQ/CLOSE, EVENT publish ACK, AUTH, COUNT, NIP-77, optional reconnect.
731
+ * Single-relay NIP-01 client. Connection lifecycle, REQ/CLOSE, EVENT publish ACK, AUTH, COUNT,
732
+ * NIP-77, optional reconnect.
709
733
  */
710
734
  var Relay = class Relay {
711
735
  url;
@@ -734,6 +758,10 @@ var Relay = class Relay {
734
758
  #serial = 0;
735
759
  #challenge;
736
760
  #authedChallenge;
761
+ /** Challenge value already answered on this connection; duplicates are not re-signed. */
762
+ #answeredChallenge;
763
+ /** Settled OK verdict for `#answeredChallenge`; set only once the relay replies. */
764
+ #answeredResult;
737
765
  #authPromise;
738
766
  #authSigner;
739
767
  #intentionalClose = false;
@@ -744,12 +772,17 @@ var Relay = class Relay {
744
772
  #deathHandled = false;
745
773
  #enablePing;
746
774
  #ping;
747
- onnotice = null;
748
- onclose = null;
775
+ onnotice;
776
+ onclose;
749
777
  /** Fired when the relay sends a NIP-42 AUTH challenge. */
750
- onauth = null;
778
+ onauth;
751
779
  /** Fired after a successful reconnect (not the initial connect). */
752
- onreconnect = null;
780
+ onreconnect;
781
+ /**
782
+ * Fired when a delivered EVENT fails id/signature verification, just before the event is dropped.
783
+ * A poisoned verifier dropping events does not fire it.
784
+ */
785
+ oninvalidevent;
753
786
  constructor(url, opts = {}) {
754
787
  this.url = normalizeURL(url);
755
788
  this.#WS = opts.websocketImplementation ?? getWebSocketImplementation();
@@ -805,6 +838,10 @@ var Relay = class Relay {
805
838
  get subscriptionCount() {
806
839
  return this.#subs.size;
807
840
  }
841
+ /** One-shot requests still awaiting a relay reply (EVENT ACK, COUNT, NEG). */
842
+ get inFlightCount() {
843
+ return this.#publishes.size + this.#counts.size + this.#neg.size;
844
+ }
808
845
  static async connect(url, opts) {
809
846
  const relay = new Relay(url, opts);
810
847
  await relay.connect({ signal: opts?.signal });
@@ -812,7 +849,11 @@ var Relay = class Relay {
812
849
  }
813
850
  async connect(opts) {
814
851
  if (this.#connected) return;
815
- if (this.#connecting) return await this.#connecting;
852
+ if (this.#connecting) {
853
+ await raceSignal(this.#connecting, opts?.signal);
854
+ return;
855
+ }
856
+ throwIfAborted(opts?.signal);
816
857
  const gen = ++this.#gen;
817
858
  this.#intentionalClose = false;
818
859
  this.#skipReconnect = false;
@@ -821,84 +862,64 @@ var Relay = class Relay {
821
862
  this.#status = RelayStatus.Connecting;
822
863
  const timeoutMs = opts?.timeoutMs ?? this.#connectTimeoutMs;
823
864
  const isReconnect = this.#reconnectAttempts > 0;
824
- let resolveConnect = () => {};
825
- let rejectConnect = (_err) => {};
865
+ let resolveConnect = noop;
866
+ let rejectConnect = noop;
826
867
  let settled = false;
827
- let timer;
828
868
  let ws;
829
- let handlers;
830
869
  const connecting = new Promise((resolve, reject) => {
831
870
  resolveConnect = resolve;
832
871
  rejectConnect = reject;
833
872
  });
834
873
  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
- }
874
+ if (ws === void 0) return;
875
+ try {
876
+ ws.removeEventListener("open", handlers.onOpen);
877
+ ws.removeEventListener("error", handlers.onError);
878
+ ws.removeEventListener("close", handlers.onClose);
879
+ ws.removeEventListener("message", handlers.onMessage);
880
+ } catch {}
881
+ if (this.#socketHandlers === handlers) this.#socketHandlers = void 0;
845
882
  try {
846
883
  if (ws.readyState !== this.#WS.CLOSED && ws.readyState !== this.#WS.CLOSING) ws.close();
847
884
  } catch {}
848
885
  if (this.#ws === ws) this.#ws = void 0;
849
886
  };
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
887
  const finish = (err) => {
865
888
  if (settled) return;
866
889
  settled = true;
867
- if (timer !== void 0) clearTimeout(timer);
890
+ clearTimeout(timer);
868
891
  if (this.#connectTimer === timer) this.#connectTimer = void 0;
869
- opts?.signal?.removeEventListener("abort", onAbort);
870
892
  if (this.#connectFinish === finish) this.#connectFinish = void 0;
871
893
  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();
894
+ if (err === void 0) resolveConnect();
895
+ else rejectConnect(err);
874
896
  };
875
897
  this.#connecting = connecting;
876
898
  this.#connectFinish = finish;
877
- timer = setTimeout(() => {
899
+ const timer = setTimeout(() => {
878
900
  if (gen !== this.#gen) {
879
901
  release();
880
902
  return;
881
903
  }
882
904
  if (!isReconnect && !this.#enableReconnect) this.#skipReconnect = true;
883
905
  release();
884
- finish(new RelayConnectionError("connection timed out", this.url));
906
+ finish(new RelayTimeoutError("connection timed out", this.url));
885
907
  if (gen === this.#gen) this.#handleSocketDeath("connection timed out", {
886
908
  fromConnectAttempt: true,
887
909
  gen
888
910
  });
889
911
  }, timeoutMs);
890
912
  this.#connectTimer = timer;
891
- opts?.signal?.addEventListener("abort", onAbort, { once: true });
892
913
  try {
893
914
  ws = new this.#WS(this.url);
894
- } catch (err) {
915
+ } catch (error) {
895
916
  if (!isReconnect && !this.#enableReconnect) this.#skipReconnect = true;
896
- finish(err);
917
+ finish(error);
897
918
  if (gen === this.#gen) this.#handleSocketDeath("connection failed", {
898
919
  fromConnectAttempt: true,
899
920
  gen
900
921
  });
901
- await connecting;
922
+ await raceSignal(connecting, opts?.signal);
902
923
  return;
903
924
  }
904
925
  this.#ws = ws;
@@ -914,6 +935,8 @@ var Relay = class Relay {
914
935
  this.#challenge = void 0;
915
936
  this.#authPromise = void 0;
916
937
  this.#authedChallenge = void 0;
938
+ this.#answeredChallenge = void 0;
939
+ this.#answeredResult = void 0;
917
940
  if (!resubscribeAll(this.#live)) {
918
941
  this.#connected = false;
919
942
  this.#status = RelayStatus.Disconnected;
@@ -926,7 +949,7 @@ var Relay = class Relay {
926
949
  this.#reconnectAttempts = 0;
927
950
  if (this.#enablePing) this.#ping.start();
928
951
  else this.#ping.stop();
929
- if (wasReconnect) this.onreconnect?.();
952
+ if (wasReconnect) invokeSafely(() => this.onreconnect?.());
930
953
  finish();
931
954
  };
932
955
  const onError = () => {
@@ -964,7 +987,7 @@ var Relay = class Relay {
964
987
  const data = typeof ev === "object" && ev !== null && "data" in ev ? ev.data : ev;
965
988
  this.#onMessage(String(data));
966
989
  };
967
- handlers = {
990
+ const handlers = {
968
991
  onOpen,
969
992
  onError,
970
993
  onClose,
@@ -976,7 +999,7 @@ var Relay = class Relay {
976
999
  ws.addEventListener("error", onError);
977
1000
  ws.addEventListener("close", onClose);
978
1001
  ws.addEventListener("message", onMessage);
979
- await connecting;
1002
+ await raceSignal(connecting, opts?.signal);
980
1003
  }
981
1004
  #detachSocketHandlers() {
982
1005
  const h = this.#socketHandlers;
@@ -1011,8 +1034,32 @@ var Relay = class Relay {
1011
1034
  this.#detachSocketHandlers();
1012
1035
  this.#teardownSocket();
1013
1036
  this.#connected = false;
1014
- this.onclose?.();
1037
+ invokeSafely(() => this.onclose?.());
1038
+ }
1039
+ }
1040
+ /**
1041
+ * Sever the socket while keeping every subscription: unlike {@link close}, open REQ state is
1042
+ * preserved and reconnect stays suppressed — the next {@link connect} re-subscribes all of them.
1043
+ * {@link Pool} uses this to suspend a relay without tearing down its live subscriptions.
1044
+ */
1045
+ disconnect() {
1046
+ this.#gen += 1;
1047
+ this.#status = RelayStatus.Disconnected;
1048
+ this.#intentionalClose = true;
1049
+ this.#skipReconnect = true;
1050
+ this.#clearReconnectTimer();
1051
+ if (this.#connectTimer !== void 0) {
1052
+ clearTimeout(this.#connectTimer);
1053
+ this.#connectTimer = void 0;
1015
1054
  }
1055
+ this.#ping.stop();
1056
+ this.#connectFinish?.(new RelayClosedError("relay disconnected", this.url));
1057
+ this.#rejectPublishes(new RelayClosedError("relay disconnected", this.url));
1058
+ this.#rejectCounts(new RelayClosedError("relay disconnected", this.url));
1059
+ this.#rejectNeg(new RelayClosedError("relay disconnected", this.url));
1060
+ this.#detachSocketHandlers();
1061
+ this.#teardownSocket();
1062
+ this.#connected = false;
1016
1063
  }
1017
1064
  #teardownSocket() {
1018
1065
  try {
@@ -1044,9 +1091,7 @@ var Relay = class Relay {
1044
1091
  for (const session of this.#neg.values()) failNegSession(session, err);
1045
1092
  this.#neg.clear();
1046
1093
  }
1047
- /**
1048
- * Unexpected socket death. Keep subscriptions if reconnecting.
1049
- */
1094
+ /** Unexpected socket death. Keep subscriptions if reconnecting. */
1050
1095
  #handleSocketDeath(reason, opts) {
1051
1096
  if (opts.gen !== this.#gen) return;
1052
1097
  if (this.#deathHandled) return;
@@ -1064,9 +1109,9 @@ var Relay = class Relay {
1064
1109
  return;
1065
1110
  }
1066
1111
  if (!this.#intentionalClose) this.#status = RelayStatus.Disconnected;
1067
- if (!opts.fromConnectAttempt || this.#subs.size > 0) {
1112
+ if (opts.fromConnectAttempt !== true || this.#subs.size > 0) {
1068
1113
  closeAllSubscriptions(this.#live, reason);
1069
- if (!this.#intentionalClose) this.onclose?.();
1114
+ if (!this.#intentionalClose) invokeSafely(() => this.onclose?.());
1070
1115
  }
1071
1116
  this.#status = RelayStatus.Closed;
1072
1117
  }
@@ -1078,9 +1123,13 @@ var Relay = class Relay {
1078
1123
  this.#reconnectTimer = setTimeout(() => {
1079
1124
  this.#reconnectTimer = void 0;
1080
1125
  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
- });
1126
+ (async () => {
1127
+ try {
1128
+ await this.connect();
1129
+ } catch {
1130
+ if (this.#enableReconnect && !this.#intentionalClose && !this.#skipReconnect && this.#subs.size > 0 && !this.#connected) this.#scheduleReconnect();
1131
+ }
1132
+ })();
1084
1133
  }, delay);
1085
1134
  }
1086
1135
  #send(message) {
@@ -1135,7 +1184,7 @@ var Relay = class Relay {
1135
1184
  const [, eventId, ok, message] = msg;
1136
1185
  const waiter = this.#publishes.get(eventId);
1137
1186
  if (!waiter) return;
1138
- if (!ok && isAuthRequired(message) && waiter.event && !waiter.authRetried && this.#authSigner) {
1187
+ if (!ok && isAuthRequired(message) && waiter.event !== void 0 && waiter.authRetried !== true && this.#authSigner) {
1139
1188
  waiter.authRetried = true;
1140
1189
  this.#authThenRepublish(waiter, eventId, message);
1141
1190
  return;
@@ -1169,14 +1218,23 @@ var Relay = class Relay {
1169
1218
  failNegErr(session, reason);
1170
1219
  break;
1171
1220
  }
1172
- case "NOTICE":
1173
- this.onnotice?.(msg[1]);
1221
+ case "NOTICE": {
1222
+ const [, notice] = msg;
1223
+ invokeSafely(() => this.onnotice?.(notice));
1224
+ break;
1225
+ }
1226
+ case "AUTH": {
1227
+ const [, authChallenge] = msg;
1228
+ if (authChallenge !== this.#challenge) {
1229
+ this.#authPromise = void 0;
1230
+ this.#authedChallenge = void 0;
1231
+ this.#answeredChallenge = void 0;
1232
+ this.#answeredResult = void 0;
1233
+ }
1234
+ this.#challenge = authChallenge;
1235
+ invokeSafely(() => this.onauth?.(authChallenge));
1174
1236
  break;
1175
- case "AUTH":
1176
- this.#challenge = msg[1];
1177
- this.#authPromise = void 0;
1178
- this.#authedChallenge = void 0;
1179
- this.onauth?.(msg[1]);
1237
+ }
1180
1238
  }
1181
1239
  }
1182
1240
  /** Low-level REQ with callbacks. Survives reconnect when enableReconnect is on. */
@@ -1190,28 +1248,28 @@ var Relay = class Relay {
1190
1248
  #acceptEvent(event) {
1191
1249
  if (this.#verifyDead) return false;
1192
1250
  try {
1193
- return this.#verify(event);
1194
- } catch (e) {
1195
- if (e instanceof WasmVerifyPoisonedError || e instanceof WebAssembly.RuntimeError) {
1251
+ const ok = this.#verify(event);
1252
+ if (!ok) invokeSafely(() => this.oninvalidevent?.());
1253
+ return ok;
1254
+ } catch (error) {
1255
+ if (error instanceof WasmVerifyPoisonedError || error instanceof WebAssembly.RuntimeError) {
1196
1256
  this.#verifyDead = true;
1197
- this.onnotice?.("verify-poisoned: wasm instance aborted");
1257
+ invokeSafely(() => this.onnotice?.("verify-poisoned: wasm instance aborted"));
1198
1258
  return false;
1199
1259
  }
1200
- throw e;
1260
+ throw error;
1201
1261
  }
1202
1262
  }
1203
1263
  /** AsyncIterable of events for filters until the subscription is closed. */
1204
1264
  stream(filters, opts) {
1205
1265
  return streamFilters((f, o) => this.subscribe(f, o), filters, opts);
1206
1266
  }
1207
- /**
1208
- * One-shot query: collect events until EOSE or timeout, then close.
1209
- */
1267
+ /** One-shot query: collect events until EOSE or timeout, then close. */
1210
1268
  async fetch(filters, opts) {
1211
1269
  if (filters.length === 0) throw new MessageError("REQ requires at least one filter");
1212
- filters = canonicalizeFilters(filters);
1270
+ const canonical = canonicalizeFilters(filters);
1213
1271
  if (!this.#connected) await this.connect({ signal: opts?.signal });
1214
- return fetchFilters((f, o) => this.subscribe(f, o), filters, {
1272
+ return fetchFilters((f, o) => this.subscribe(f, o), canonical, {
1215
1273
  timeoutMs: opts?.timeoutMs ?? 4400,
1216
1274
  signal: opts?.signal,
1217
1275
  id: opts?.id,
@@ -1225,7 +1283,7 @@ var Relay = class Relay {
1225
1283
  return await new Promise((resolve, reject) => {
1226
1284
  const timer = setTimeout(() => {
1227
1285
  this.#publishes.delete(event.id);
1228
- reject(new RelayPublishError("publish timed out", this.url));
1286
+ reject(new RelayTimeoutError("publish timed out", this.url));
1229
1287
  }, timeoutMs);
1230
1288
  this.#publishes.set(event.id, {
1231
1289
  resolve,
@@ -1236,25 +1294,25 @@ var Relay = class Relay {
1236
1294
  });
1237
1295
  try {
1238
1296
  this.#send(["EVENT", event]);
1239
- } catch (err) {
1297
+ } catch (error) {
1240
1298
  clearTimeout(timer);
1241
1299
  this.#publishes.delete(event.id);
1242
- reject(err instanceof Error ? err : new RelayPublishError("publish failed", this.url));
1300
+ reject(error instanceof Error ? error : new RelayPublishError("publish failed", this.url));
1243
1301
  }
1244
1302
  });
1245
1303
  }
1246
1304
  /**
1247
- * NIP-45 COUNT: ask the relay how many events match `filters`.
1248
- * Resolves with the COUNT payload (count / optional approximate / optional hll).
1305
+ * NIP-45 COUNT: ask the relay how many events match `filters`. Resolves with the COUNT payload
1306
+ * (count / optional approximate / optional hll).
1249
1307
  */
1250
1308
  async count(filters, opts) {
1251
1309
  if (!this.#connected) throw new RelayClosedError("not connected", this.url);
1252
1310
  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");
1311
+ throwIfAborted(opts?.signal);
1312
+ const canonical = canonicalizeFilters(filters);
1313
+ const id = opts?.id === void 0 ? this.nextSubId("count") : createSubscriptionId(opts.id);
1256
1314
  const timeoutMs = opts?.timeoutMs ?? this.#publishTimeoutMs;
1257
- return await new Promise((resolve, reject) => {
1315
+ return new Promise((resolve, reject) => {
1258
1316
  const cleanup = () => {
1259
1317
  opts?.signal?.removeEventListener("abort", onAbort);
1260
1318
  };
@@ -1274,13 +1332,15 @@ var Relay = class Relay {
1274
1332
  reject(err);
1275
1333
  },
1276
1334
  timer: void 0,
1277
- filters: [...filters],
1335
+ filters: [...canonical],
1278
1336
  authRetried: false,
1279
1337
  timeoutMs
1280
1338
  };
1281
- const onAbort = () => waiter.reject(new RelayConnectionError("count aborted", this.url));
1339
+ const onAbort = () => {
1340
+ if (opts?.signal) waiter.reject(abortReason(opts.signal));
1341
+ };
1282
1342
  waiter.timer = setTimeout(() => {
1283
- waiter.reject(new RelayPublishError("count timed out", this.url));
1343
+ waiter.reject(new RelayTimeoutError("count timed out", this.url));
1284
1344
  }, timeoutMs);
1285
1345
  this.#counts.set(id, waiter);
1286
1346
  opts?.signal?.addEventListener("abort", onAbort, { once: true });
@@ -1288,41 +1348,56 @@ var Relay = class Relay {
1288
1348
  this.#send([
1289
1349
  "COUNT",
1290
1350
  id,
1291
- ...filters
1351
+ ...canonical
1292
1352
  ]);
1293
- } catch (err) {
1294
- waiter.reject(err instanceof Error ? err : new RelayPublishError("count failed", this.url));
1353
+ } catch (error) {
1354
+ waiter.reject(error instanceof Error ? error : new RelayPublishError("count failed", this.url));
1295
1355
  }
1296
1356
  });
1297
1357
  }
1298
- /**
1299
- * NIP-42 AUTH: sign the current challenge and wait for OK.
1300
- */
1358
+ /** NIP-42 AUTH: sign the current challenge and wait for OK. */
1301
1359
  async auth(sign, opts) {
1302
1360
  const challenge = this.#challenge;
1303
- if (!challenge) throw new RelayError("no AUTH challenge received from relay", this.url);
1361
+ if (challenge === void 0 || challenge === "") throw new RelayError("no AUTH challenge received from relay", this.url);
1304
1362
  if (this.#authPromise) return this.#authPromise;
1363
+ if (this.#answeredChallenge === challenge && this.#answeredResult !== void 0) return this.#answeredResult;
1305
1364
  const pending = (async () => {
1306
- const event = await sign(makeAuthEvent(this.url, challenge));
1365
+ const template = makeAuthEvent(this.url, challenge);
1366
+ let event;
1367
+ try {
1368
+ event = await sign(template);
1369
+ } catch (error) {
1370
+ if (error instanceof NoSignerError) return {
1371
+ ok: false,
1372
+ message: "auth: no signer"
1373
+ };
1374
+ throw error;
1375
+ }
1307
1376
  if (!this.#connected) throw new RelayClosedError("not connected", this.url);
1308
1377
  const timeoutMs = opts?.timeoutMs ?? this.#publishTimeoutMs;
1309
- return await new Promise((resolve, reject) => {
1378
+ return new Promise((resolve, reject) => {
1310
1379
  const timer = setTimeout(() => {
1311
1380
  this.#publishes.delete(event.id);
1312
- reject(new RelayPublishError("auth timed out", this.url));
1381
+ reject(new RelayTimeoutError("auth timed out", this.url));
1313
1382
  }, timeoutMs);
1314
1383
  this.#publishes.set(event.id, {
1315
- resolve,
1384
+ resolve: (result) => {
1385
+ if (this.#challenge === challenge) {
1386
+ this.#answeredChallenge = challenge;
1387
+ this.#answeredResult = result;
1388
+ }
1389
+ resolve(result);
1390
+ },
1316
1391
  reject,
1317
1392
  timer,
1318
1393
  timeoutMs
1319
1394
  });
1320
1395
  try {
1321
1396
  this.#send(["AUTH", event]);
1322
- } catch (err) {
1397
+ } catch (error) {
1323
1398
  clearTimeout(timer);
1324
1399
  this.#publishes.delete(event.id);
1325
- reject(err instanceof Error ? err : new RelayPublishError("auth send failed", this.url));
1400
+ reject(error instanceof Error ? error : new RelayPublishError("auth send failed", this.url));
1326
1401
  }
1327
1402
  });
1328
1403
  })();
@@ -1335,17 +1410,32 @@ var Relay = class Relay {
1335
1410
  if (this.#authPromise === pending) this.#authPromise = void 0;
1336
1411
  }
1337
1412
  }
1413
+ /**
1414
+ * Clear a cached AUTH rejection for the current challenge so a later `auth()` signs again. A
1415
+ * successful auth and in-flight answers are kept. When a challenge is pending and unanswered,
1416
+ * `onauth` is re-fired so the pool's automatic auth can run once more.
1417
+ */
1418
+ resetAuth() {
1419
+ if (this.#answeredResult !== void 0 && !this.#answeredResult.ok) {
1420
+ this.#answeredChallenge = void 0;
1421
+ this.#answeredResult = void 0;
1422
+ }
1423
+ if (this.#challenge !== void 0 && this.#challenge !== this.#authedChallenge) {
1424
+ const challenge = this.#challenge;
1425
+ invokeSafely(() => this.onauth?.(challenge));
1426
+ }
1427
+ }
1338
1428
  async #ensureAuthed() {
1339
1429
  const signer = this.#authSigner;
1340
1430
  if (!signer) return false;
1341
1431
  for (let i = 0; i < 3; i++) {
1342
- if (!this.#challenge) return false;
1432
+ if (this.#challenge === void 0 || this.#challenge === "") return false;
1343
1433
  if (this.#authedChallenge === this.#challenge) return true;
1344
1434
  const signed = this.#challenge;
1345
1435
  const result = await this.auth(signer);
1346
1436
  if (this.#authedChallenge === this.#challenge) return true;
1347
1437
  if (!result.ok) {
1348
- if (!this.#challenge || this.#challenge === signed) return false;
1438
+ if (this.#challenge === void 0 || this.#challenge === "" || this.#challenge === signed) return false;
1349
1439
  continue;
1350
1440
  }
1351
1441
  }
@@ -1393,16 +1483,16 @@ var Relay = class Relay {
1393
1483
  return;
1394
1484
  }
1395
1485
  waiter.timer = setTimeout(() => {
1396
- waiter.reject(new RelayPublishError("count timed out", this.url));
1486
+ waiter.reject(new RelayTimeoutError("count timed out", this.url));
1397
1487
  }, waiter.timeoutMs);
1398
1488
  this.#send([
1399
1489
  "COUNT",
1400
1490
  id,
1401
1491
  ...waiter.filters
1402
1492
  ]);
1403
- } catch (err) {
1493
+ } catch (error) {
1404
1494
  if (!this.#counts.has(id)) return;
1405
- waiter.reject(err instanceof Error ? err : new RelayClosedError(reason || "COUNT closed", this.url));
1495
+ waiter.reject(error instanceof Error ? error : new RelayClosedError(reason || "COUNT closed", this.url));
1406
1496
  }
1407
1497
  }
1408
1498
  async #authThenRepublish(waiter, eventId, message) {
@@ -1441,7 +1531,7 @@ var Relay = class Relay {
1441
1531
  }
1442
1532
  waiter.timer = setTimeout(() => {
1443
1533
  this.#publishes.delete(eventId);
1444
- waiter.reject(new RelayPublishError("publish timed out", this.url));
1534
+ waiter.reject(new RelayTimeoutError("publish timed out", this.url));
1445
1535
  }, waiter.timeoutMs);
1446
1536
  this.#send(["EVENT", waiter.event]);
1447
1537
  } catch {
@@ -1453,19 +1543,17 @@ var Relay = class Relay {
1453
1543
  }
1454
1544
  }
1455
1545
  /**
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.
1546
+ * NIP-77: run Negentropy reconciliation against this relay. Returns the local-only (`have`) and
1547
+ * remote-only (`need`) event ids. Does not upload or download events.
1459
1548
  *
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}.
1549
+ * `timeoutMs` is a single wall-clock deadline for the whole session (`NEG-OPEN` through the last
1550
+ * `NEG-MSG`), not a per-message budget. Default: {@link RelayOptions.publishTimeoutMs}.
1463
1551
  */
1464
1552
  async negReconcile(filter, storage, opts) {
1465
1553
  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");
1554
+ throwIfAborted(opts?.signal);
1555
+ const canonical = canonicalizeFilter(filter);
1556
+ const id = opts?.id === void 0 ? this.nextSubId("neg") : assertSubscriptionId(opts.id);
1469
1557
  const timeoutMs = opts?.timeoutMs ?? this.#publishTimeoutMs;
1470
1558
  const prev = this.#neg.get(id);
1471
1559
  if (prev) failNegSession(prev, new Nip77Error("closed: replaced by new NEG-OPEN"));
@@ -1475,7 +1563,7 @@ var Relay = class Relay {
1475
1563
  return await runWiredNegSession({
1476
1564
  session,
1477
1565
  storage,
1478
- filter,
1566
+ filter: canonical,
1479
1567
  id,
1480
1568
  timeoutMs,
1481
1569
  signal: opts?.signal,
@@ -1500,9 +1588,8 @@ var Relay = class Relay {
1500
1588
  //#endregion
1501
1589
  //#region src/relay/fan-in.ts
1502
1590
  /**
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.
1591
+ * Aggregate EOSE/close across routed jobs. One `seen` set. `pending` counts URL list entries
1592
+ * (duplicates included). `pendingEose` counts unique `jobIndex:url` keys.
1506
1593
  */
1507
1594
  function fanIn(pool, jobs, opts = {}) {
1508
1595
  const seen = /* @__PURE__ */ new Set();
@@ -1521,7 +1608,7 @@ function fanIn(pool, jobs, opts = {}) {
1521
1608
  clearTimeout(eoseTimer);
1522
1609
  eoseTimer = void 0;
1523
1610
  }
1524
- opts.oneose?.();
1611
+ invokeSafely(() => opts.oneose?.());
1525
1612
  };
1526
1613
  const markEose = (jobIndex, url) => {
1527
1614
  const key = `${jobIndex}:${url}`;
@@ -1541,24 +1628,39 @@ function fanIn(pool, jobs, opts = {}) {
1541
1628
  if (closed) return;
1542
1629
  settleClose();
1543
1630
  for (const c of closers) c.close(reason);
1544
- opts.onclose?.(reason ?? "closed by client");
1631
+ invokeSafely(() => opts.onclose?.(reason ?? "closed by client"));
1545
1632
  };
1546
- if (opts.signal?.aborted) {
1633
+ if (opts.signal?.aborted === true) {
1547
1634
  closed = true;
1548
- opts.onclose?.("aborted");
1635
+ invokeSafely(() => opts.onclose?.("aborted"));
1549
1636
  return { close: closeAll };
1550
1637
  }
1551
1638
  opts.signal?.addEventListener("abort", () => closeAll("aborted"), { once: true });
1639
+ const failUrl = (jobIndex, key) => {
1640
+ markEose(jobIndex, key);
1641
+ pending -= 1;
1642
+ if (pending <= 0 && closers.length === 0 && !closed) {
1643
+ settleClose();
1644
+ invokeSafely(() => opts.onclose?.("all relays failed"));
1645
+ }
1646
+ };
1552
1647
  const attach = (relay, job, jobIndex) => {
1553
1648
  if (closed) return;
1649
+ const received = opts.receivedEvent;
1554
1650
  const sub = relay.subscribe([...job.filters], {
1555
1651
  id: jobs.length === 1 ? job.id : void 0,
1556
1652
  closeOnEose: opts.closeOnEose,
1557
- alreadyHaveEvent: (id) => Boolean(opts.alreadyHaveEvent?.(id) || seen.has(id)),
1558
- receivedEvent: opts.receivedEvent,
1653
+ alreadyHaveEvent: (id) => {
1654
+ let have = seen.has(id);
1655
+ invokeSafely(() => {
1656
+ have = have || Boolean(opts.alreadyHaveEvent?.(id));
1657
+ });
1658
+ return have;
1659
+ },
1660
+ receivedEvent: received === void 0 ? void 0 : (id) => invokeSafely(() => received(id, relay.url)),
1559
1661
  onevent: (event) => {
1560
1662
  seen.add(event.id);
1561
- opts.onevent?.(event);
1663
+ invokeSafely(() => opts.onevent?.(event, relay.url));
1562
1664
  },
1563
1665
  oneose: () => markEose(jobIndex, relay.url),
1564
1666
  onclose: (reason) => {
@@ -1566,14 +1668,14 @@ function fanIn(pool, jobs, opts = {}) {
1566
1668
  pending -= 1;
1567
1669
  if (pending <= 0 && !closed) {
1568
1670
  settleClose();
1569
- opts.onclose?.(reason);
1671
+ invokeSafely(() => opts.onclose?.(reason));
1570
1672
  }
1571
1673
  }
1572
1674
  });
1573
1675
  closers.push(sub);
1574
1676
  };
1575
- for (let jobIndex = 0; jobIndex < jobs.length; jobIndex++) {
1576
- const job = jobs[jobIndex];
1677
+ for (const [jobIndex, job] of jobs.entries()) {
1678
+ const jobUrls = /* @__PURE__ */ new Set();
1577
1679
  for (const url of job.urls) {
1578
1680
  let key;
1579
1681
  try {
@@ -1581,42 +1683,43 @@ function fanIn(pool, jobs, opts = {}) {
1581
1683
  } catch {
1582
1684
  key = url;
1583
1685
  }
1686
+ if (jobUrls.has(key)) continue;
1687
+ jobUrls.add(key);
1584
1688
  pending += 1;
1585
1689
  const eoseKey = `${jobIndex}:${key}`;
1586
1690
  if (!eoseAttempted.has(eoseKey)) {
1587
1691
  eoseAttempted.add(eoseKey);
1588
1692
  pendingEose += 1;
1589
1693
  }
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
1694
  const tryAttach = (relay) => {
1599
1695
  try {
1600
1696
  attach(relay, job, jobIndex);
1601
- } catch (err) {
1602
- if (err instanceof RelayClosedError) {
1603
- failUrl();
1697
+ } catch (error) {
1698
+ if (error instanceof RelayClosedError) {
1699
+ failUrl(jobIndex, key);
1604
1700
  return;
1605
1701
  }
1606
- throw err;
1702
+ throw error;
1607
1703
  }
1608
1704
  };
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);
1705
+ (async () => {
1706
+ let relay;
1707
+ try {
1708
+ relay = await pool.ensureRelay(key, {
1709
+ signal: opts.signal,
1710
+ timeoutMs: opts.connectTimeoutMs
1711
+ });
1712
+ } catch {
1713
+ const existing = pool.getRelay(key);
1714
+ if (existing !== void 0) {
1715
+ tryAttach(existing);
1716
+ return;
1717
+ }
1718
+ failUrl(jobIndex, key);
1616
1719
  return;
1617
1720
  }
1618
- failUrl();
1619
- });
1721
+ tryAttach(relay);
1722
+ })();
1620
1723
  }
1621
1724
  }
1622
1725
  if (opts.eoseTimeoutMs !== void 0 && pending > 0) eoseTimer = setTimeout(() => {
@@ -1626,31 +1729,53 @@ function fanIn(pool, jobs, opts = {}) {
1626
1729
  if (pending === 0) queueMicrotask(() => {
1627
1730
  if (closed) return;
1628
1731
  settleClose();
1629
- opts.onclose?.("no relays");
1732
+ invokeSafely(() => opts.onclose?.("no relays"));
1630
1733
  });
1631
1734
  return { close: closeAll };
1632
1735
  }
1633
1736
  async function fetchRouted(pool, jobs, opts = {}) {
1737
+ throwIfAborted(opts.signal);
1634
1738
  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
- });
1739
+ await Promise.all(jobs.flatMap((job) => {
1740
+ const urls = [];
1741
+ for (const raw of job.urls) {
1742
+ let key;
1743
+ try {
1744
+ key = normalizeURL(raw);
1745
+ } catch {
1746
+ key = raw;
1747
+ }
1748
+ if (!urls.includes(key)) urls.push(key);
1749
+ }
1750
+ return urls.map(async (url) => {
1751
+ let batch;
1752
+ let relayUrl;
1753
+ try {
1754
+ const relay = await pool.ensureRelay(url, {
1755
+ signal: opts.signal,
1756
+ timeoutMs: opts.connectTimeoutMs
1757
+ });
1758
+ relayUrl = relay.url;
1759
+ batch = await relay.fetch([...job.filters], {
1760
+ timeoutMs: opts.timeoutMs,
1761
+ signal: opts.signal
1762
+ });
1763
+ } catch {
1764
+ if (opts.signal?.aborted === true) throw abortReason(opts.signal);
1765
+ return;
1766
+ }
1644
1767
  for (const event of batch) if (!byId.has(event.id)) byId.set(event.id, event);
1645
- } catch {}
1646
- })));
1768
+ for (const event of batch) invokeSafely(() => opts.onevent?.(event, relayUrl));
1769
+ });
1770
+ }));
1647
1771
  return [...byId.values()];
1648
1772
  }
1649
1773
  //#endregion
1650
1774
  //#region src/relay/pool.ts
1651
- /**
1652
- * Multi-relay coordinator: connection reuse, cross-relay event dedup, fan-out publish.
1653
- */
1775
+ function isIdle(relay) {
1776
+ return relay.status !== RelayStatus.Connecting && relay.subscriptionCount === 0 && relay.inFlightCount === 0;
1777
+ }
1778
+ /** Multi-relay coordinator: connection reuse, cross-relay event dedup, fan-out publish. */
1654
1779
  var Pool = class {
1655
1780
  #relays = /* @__PURE__ */ new Map();
1656
1781
  #opts;
@@ -1659,10 +1784,17 @@ var Pool = class {
1659
1784
  #idleTimer;
1660
1785
  #allowInsecure;
1661
1786
  #trustedInsecure;
1787
+ #pinned;
1788
+ /** Per-relay timestamps of verification failures inside the sliding window. */
1789
+ #invalidEvents = /* @__PURE__ */ new Map();
1790
+ /** Normalized URL → epoch ms when its suspension lifts. */
1791
+ #suspendedUntil = /* @__PURE__ */ new Map();
1792
+ #resumeTimers = /* @__PURE__ */ new Map();
1662
1793
  constructor(opts = {}) {
1663
1794
  this.#opts = opts;
1664
- this.#allowInsecure = opts.allowInsecure ?? true;
1795
+ this.#allowInsecure = opts.allowInsecure ?? false;
1665
1796
  this.#trustedInsecure = new Set((opts.trustedInsecureUrls ?? []).map(normalizeURL));
1797
+ this.#pinned = new Set((opts.pinnedUrls ?? []).map(normalizeURL));
1666
1798
  this.#idleTimeoutMs = opts.idleTimeoutMs ?? 0;
1667
1799
  if (this.#idleTimeoutMs > 0) this.#idleTimer = setInterval(() => this.cleanIdleRelays(), opts.idleCleanupIntervalMs ?? 3e4);
1668
1800
  }
@@ -1672,23 +1804,66 @@ var Pool = class {
1672
1804
  setTrustedInsecureUrls(urls) {
1673
1805
  this.#trustedInsecure = new Set(urls.map(normalizeURL));
1674
1806
  }
1807
+ setPinnedUrls(urls) {
1808
+ this.#pinned = new Set(urls.map(normalizeURL));
1809
+ }
1810
+ /**
1811
+ * Drop cached AUTH rejections and re-fire pending challenges on every pooled relay, so a later
1812
+ * `setSigner` takes effect without reconnect.
1813
+ */
1814
+ resetAuth() {
1815
+ for (const relay of this.#relays.values()) relay.resetAuth();
1816
+ }
1675
1817
  cleanIdleRelays() {
1676
1818
  if (this.#idleTimeoutMs <= 0) return;
1677
1819
  const now = Date.now();
1678
1820
  const idle = [];
1679
1821
  for (const [url, relay] of this.#relays) {
1680
- if (relay.subscriptionCount > 0) continue;
1822
+ if (this.#pinned.has(url) || !isIdle(relay)) continue;
1681
1823
  const last = this.#lastActivity.get(url) ?? 0;
1682
1824
  if (!relay.connected || now - last >= this.#idleTimeoutMs) idle.push(url);
1683
1825
  }
1684
1826
  for (const url of this.#lastActivity.keys()) if (!this.#relays.has(url)) this.#lastActivity.delete(url);
1685
1827
  if (idle.length === 0) return;
1686
1828
  this.close(idle);
1687
- this.#opts.onIdleRelaysClosed?.(idle);
1829
+ invokeSafely(() => this.#opts.onIdleRelaysClosed?.(idle));
1688
1830
  }
1689
1831
  #touch(url) {
1690
1832
  this.#lastActivity.set(url, Date.now());
1691
1833
  }
1834
+ /**
1835
+ * Count one verification failure for `norm`; on the `limit + 1`-th inside `windowMs` suspend the
1836
+ * relay: drop its connection (subscriptions kept), record `until`, and fire `onRelaySuspended`.
1837
+ */
1838
+ #noteInvalidEvent(norm) {
1839
+ const policy = this.#opts.invalidEventPolicy;
1840
+ if (policy === void 0) return;
1841
+ const now = Date.now();
1842
+ const window = (this.#invalidEvents.get(norm) ?? []).filter((at) => now - at < policy.windowMs);
1843
+ window.push(now);
1844
+ if (window.length <= policy.limit) {
1845
+ this.#invalidEvents.set(norm, window);
1846
+ return;
1847
+ }
1848
+ this.#invalidEvents.delete(norm);
1849
+ if (this.#suspendedUntil.has(norm)) return;
1850
+ const until = now + policy.cooldownMs;
1851
+ this.#suspendedUntil.set(norm, until);
1852
+ this.#relays.get(norm)?.disconnect();
1853
+ const resume = setTimeout(() => {
1854
+ this.#resumeTimers.delete(norm);
1855
+ this.#suspendedUntil.delete(norm);
1856
+ const relay = this.#relays.get(norm);
1857
+ if (relay === void 0 || relay.connected || relay.subscriptionCount === 0) return;
1858
+ (async () => {
1859
+ try {
1860
+ await relay.connect();
1861
+ } catch {}
1862
+ })();
1863
+ }, policy.cooldownMs);
1864
+ this.#resumeTimers.set(norm, resume);
1865
+ invokeSafely(() => this.#opts.onRelaySuspended?.(norm, until));
1866
+ }
1692
1867
  #rejectInsecure(url, norm) {
1693
1868
  if (this.#allowInsecure) return;
1694
1869
  if (!isInsecureRelayUrl(url)) return;
@@ -1700,11 +1875,40 @@ var Pool = class {
1700
1875
  clearInterval(this.#idleTimer);
1701
1876
  this.#idleTimer = void 0;
1702
1877
  }
1878
+ /**
1879
+ * Soft cap: closing the least-recently-used idle non-pinned relay when a new non-pinned relay
1880
+ * would push the count past `maxRelays`. Busy relays are never evicted; with none idle the cap is
1881
+ * exceeded rather than failing.
1882
+ */
1883
+ #enforceMaxRelays(incoming) {
1884
+ const cap = this.#opts.maxRelays;
1885
+ if (cap === void 0 || this.#pinned.has(incoming)) return;
1886
+ let count = 0;
1887
+ let oldestUrl;
1888
+ let oldestAt = Number.POSITIVE_INFINITY;
1889
+ for (const [url, relay] of this.#relays) {
1890
+ if (this.#pinned.has(url)) continue;
1891
+ count += 1;
1892
+ if (!isIdle(relay)) continue;
1893
+ const at = this.#lastActivity.get(url) ?? 0;
1894
+ if (at < oldestAt) {
1895
+ oldestAt = at;
1896
+ oldestUrl = url;
1897
+ }
1898
+ }
1899
+ if (count >= cap && oldestUrl !== void 0) this.close([oldestUrl]);
1900
+ }
1703
1901
  async ensureRelay(url, opts) {
1704
1902
  const norm = normalizeURL(url);
1705
1903
  this.#rejectInsecure(url, norm);
1904
+ const suspendedUntil = this.#suspendedUntil.get(norm);
1905
+ if (suspendedUntil !== void 0) {
1906
+ if (Date.now() < suspendedUntil) throw new RelaySuspendedError(norm, suspendedUntil);
1907
+ this.#suspendedUntil.delete(norm);
1908
+ }
1706
1909
  let relay = this.#relays.get(norm);
1707
1910
  if (!relay) {
1911
+ this.#enforceMaxRelays(norm);
1708
1912
  const signFn = this.#opts.automaticallyAuth?.(norm) ?? void 0;
1709
1913
  const created = new Relay(norm, {
1710
1914
  websocketImplementation: this.#opts.websocketImplementation,
@@ -1722,8 +1926,15 @@ var Pool = class {
1722
1926
  this.#relays.delete(norm);
1723
1927
  this.#lastActivity.delete(norm);
1724
1928
  };
1929
+ created.oninvalidevent = () => {
1930
+ this.#noteInvalidEvent(norm);
1931
+ };
1725
1932
  if (signFn) created.onauth = () => {
1726
- created.auth(signFn).catch(() => {});
1933
+ (async () => {
1934
+ try {
1935
+ await created.auth(signFn);
1936
+ } catch {}
1937
+ })();
1727
1938
  };
1728
1939
  this.#relays.set(norm, created);
1729
1940
  relay = created;
@@ -1734,18 +1945,22 @@ var Pool = class {
1734
1945
  signal: opts?.signal,
1735
1946
  timeoutMs: opts?.timeoutMs ?? this.#opts.connectTimeoutMs
1736
1947
  });
1737
- } catch (err) {
1738
- if (!this.#opts.enableReconnect) {
1948
+ } catch (error) {
1949
+ if (this.#opts.enableReconnect !== true) {
1739
1950
  this.#relays.delete(norm);
1740
1951
  this.#lastActivity.delete(norm);
1741
1952
  }
1742
- throw err;
1953
+ throw error;
1743
1954
  }
1744
1955
  return relay;
1745
1956
  }
1746
1957
  close(urls) {
1747
1958
  if (!urls) {
1748
1959
  this.#stopIdleCleanup();
1960
+ for (const timer of this.#resumeTimers.values()) clearTimeout(timer);
1961
+ this.#resumeTimers.clear();
1962
+ this.#suspendedUntil.clear();
1963
+ this.#invalidEvents.clear();
1749
1964
  for (const relay of this.#relays.values()) relay.close();
1750
1965
  this.#relays.clear();
1751
1966
  this.#lastActivity.clear();
@@ -1753,22 +1968,27 @@ var Pool = class {
1753
1968
  }
1754
1969
  for (const url of urls) {
1755
1970
  const norm = normalizeURL(url);
1971
+ const timer = this.#resumeTimers.get(norm);
1972
+ if (timer !== void 0) {
1973
+ clearTimeout(timer);
1974
+ this.#resumeTimers.delete(norm);
1975
+ }
1756
1976
  this.#relays.get(norm)?.close();
1757
1977
  this.#relays.delete(norm);
1758
1978
  this.#lastActivity.delete(norm);
1759
1979
  }
1760
1980
  }
1761
1981
  /**
1762
- * Subscribe across relays. Deduplicates by event id.
1763
- * Returns a closer; callbacks receive every new event once.
1982
+ * Subscribe across relays. Deduplicates by event id. Returns a closer; callbacks receive every
1983
+ * new event once.
1764
1984
  */
1765
1985
  subscribe(relays, filters, opts = {}) {
1766
1986
  if (filters.length === 0) throw new MessageError("REQ requires at least one filter");
1767
1987
  if (opts.id !== void 0) assertSubscriptionId(opts.id);
1768
- filters = canonicalizeFilters(filters);
1988
+ const canonical = canonicalizeFilters(filters);
1769
1989
  return fanIn(this, [{
1770
1990
  urls: relays,
1771
- filters,
1991
+ filters: canonical,
1772
1992
  id: opts.id
1773
1993
  }], {
1774
1994
  onevent: opts.onevent,
@@ -1791,7 +2011,8 @@ var Pool = class {
1791
2011
  }], {
1792
2012
  timeoutMs: opts?.timeoutMs,
1793
2013
  signal: opts?.signal,
1794
- connectTimeoutMs: this.#opts.connectTimeoutMs
2014
+ connectTimeoutMs: this.#opts.connectTimeoutMs,
2015
+ onevent: opts?.onevent
1795
2016
  });
1796
2017
  }
1797
2018
  /** Publish to all listed relays; returns per-relay outcomes. */
@@ -1805,10 +2026,10 @@ var Pool = class {
1805
2026
  url: relay.url,
1806
2027
  result
1807
2028
  };
1808
- } catch (err) {
2029
+ } catch (error) {
1809
2030
  return {
1810
2031
  url,
1811
- error: err instanceof Error ? err.message : String(err)
2032
+ error: error instanceof Error ? error.message : String(error)
1812
2033
  };
1813
2034
  }
1814
2035
  }));
@@ -1827,11 +2048,12 @@ var Pool = class {
1827
2048
  }));
1828
2049
  }
1829
2050
  /**
1830
- * NIP-45 COUNT across relays. Per-relay outcomes; failures do not throw.
1831
- * Counts are not summed — each relay reports independently (may overlap).
2051
+ * NIP-45 COUNT across relays. Per-relay outcomes; failures do not throw. Counts are not summed —
2052
+ * each relay reports independently (may overlap).
1832
2053
  */
1833
2054
  async count(relays, filters, opts) {
1834
- filters = canonicalizeFilters(filters);
2055
+ throwIfAborted(opts?.signal);
2056
+ const canonical = canonicalizeFilters(filters);
1835
2057
  return await Promise.all(relays.map(async (url) => {
1836
2058
  try {
1837
2059
  const relay = await this.ensureRelay(url, {
@@ -1839,20 +2061,22 @@ var Pool = class {
1839
2061
  timeoutMs: this.#opts.connectTimeoutMs
1840
2062
  });
1841
2063
  this.#touch(relay.url);
1842
- const payload = await relay.count(filters, {
2064
+ const payload = await relay.count(canonical, {
1843
2065
  timeoutMs: opts?.timeoutMs,
1844
2066
  signal: opts?.signal
1845
2067
  });
1846
- return {
2068
+ const out = {
1847
2069
  url: relay.url,
1848
- count: payload.count,
1849
- approximate: payload.approximate,
1850
- hll: payload.hll
2070
+ count: payload.count
1851
2071
  };
1852
- } catch (err) {
2072
+ if (payload.approximate !== void 0) out.approximate = payload.approximate;
2073
+ if (payload.hll !== void 0) out.hll = payload.hll;
2074
+ return out;
2075
+ } catch (error) {
2076
+ if (opts?.signal?.aborted === true) throw abortReason(opts.signal);
1853
2077
  return {
1854
2078
  url,
1855
- error: err instanceof Error ? err.message : String(err)
2079
+ error: error instanceof Error ? error.message : String(error)
1856
2080
  };
1857
2081
  }
1858
2082
  }));
@@ -1876,6 +2100,6 @@ var Pool = class {
1876
2100
  }
1877
2101
  };
1878
2102
  //#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 };
2103
+ export { RelayStatus as a, isInsecureRelayUrl as c, RelayClosedError as d, RelayConnectionError as f, RelayTimeoutError as g, RelaySuspendedError 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
2104
 
1881
- //# sourceMappingURL=pool-C_j1OmBB.mjs.map
2105
+ //# sourceMappingURL=pool-C2xOcsun.mjs.map