@qntx/nostr 0.6.0 → 0.7.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 (177) hide show
  1. package/dist/{abort-Ba8tDrOI.mjs → abort-DBq2STPj.mjs} +13 -6
  2. package/dist/abort-DBq2STPj.mjs.map +1 -0
  3. package/dist/{builder-9tjI6WQ3.mjs → builder-CdLE4J6O.mjs} +11 -17
  4. package/dist/builder-CdLE4J6O.mjs.map +1 -0
  5. package/dist/{builder-sSflu6nB.d.mts → builder-DgkqDylK.d.mts} +5 -6
  6. package/dist/{client-CadO5fAX.mjs → client-CILhdfsF.mjs} +104 -314
  7. package/dist/client-CILhdfsF.mjs.map +1 -0
  8. package/dist/client.d.mts +2 -2
  9. package/dist/client.mjs +2 -2
  10. package/dist/collections-DtwMc1NU.mjs +33 -0
  11. package/dist/collections-DtwMc1NU.mjs.map +1 -0
  12. package/dist/core.d.mts +9 -8
  13. package/dist/core.mjs +7 -7
  14. package/dist/{error-BW7re1UN.d.mts → error-BD_0QFKT.d.mts} +2 -2
  15. package/dist/{error-CR8Urc84.d.mts → error-BquXDmIN.d.mts} +4 -4
  16. package/dist/{error-DrEIM7FO.mjs → error-L_QdmSnV.mjs} +2 -2
  17. package/dist/{error-DrEIM7FO.mjs.map → error-L_QdmSnV.mjs.map} +1 -1
  18. package/dist/{error-B__OMBRY.mjs → error-gtJvV6YS.mjs} +2 -2
  19. package/dist/{error-B__OMBRY.mjs.map → error-gtJvV6YS.mjs.map} +1 -1
  20. package/dist/{event-fU3Y01PR.d.mts → event-DJeb_DIL.d.mts} +4 -2
  21. package/dist/{event-D8Nfoh4K.mjs → event-DinQMmJ1.mjs} +16 -10
  22. package/dist/event-DinQMmJ1.mjs.map +1 -0
  23. package/dist/{filter-C0IMByE3.d.mts → filter-jI_9htF7.d.mts} +2 -2
  24. package/dist/{gossip-DpIvkBuj.d.mts → gossip-C7HLW5kI.d.mts} +35 -26
  25. package/dist/{gossip-oloDMWEj.mjs → gossip-CcTp9VpU.mjs} +23 -33
  26. package/dist/gossip-CcTp9VpU.mjs.map +1 -0
  27. package/dist/gossip.d.mts +1 -1
  28. package/dist/gossip.mjs +1 -1
  29. package/dist/{index-CvkvqB16.d.mts → index-DU9tRFTZ.d.mts} +7 -20
  30. package/dist/{index-q4MDce_r.d.mts → index-DiXwNn2M.d.mts} +6 -8
  31. package/dist/{index-CNl2q67H.d.mts → index-DocgD9K0.d.mts} +92 -167
  32. package/dist/{index-D1bqFHKB.d.mts → index-InQZpHyQ.d.mts} +18 -18
  33. package/dist/{index-MxC28S4P.d.mts → index-Tx15HGAD.d.mts} +26 -28
  34. package/dist/{index-lvXeER5t.d.mts → index-iLT8UzY3.d.mts} +19 -37
  35. package/dist/index.d.mts +23 -21
  36. package/dist/index.mjs +21 -19
  37. package/dist/{instance-DQE8BoWt.mjs → instance-BcKzxHIP.mjs} +26 -27
  38. package/dist/instance-BcKzxHIP.mjs.map +1 -0
  39. package/dist/{instance-CIca-ynn.d.mts → instance-DjsL8h84.d.mts} +5 -5
  40. package/dist/{key-Cv4_pN3x.d.mts → key-D_5OgGT7.d.mts} +11 -7
  41. package/dist/{key-Bps-LqX7.mjs → key-bshqfmZL.mjs} +13 -9
  42. package/dist/key-bshqfmZL.mjs.map +1 -0
  43. package/dist/{loaders-o6YXB_Ri.mjs → loaders-DD0XAGzw.mjs} +73 -82
  44. package/dist/loaders-DD0XAGzw.mjs.map +1 -0
  45. package/dist/loaders.d.mts +3 -3
  46. package/dist/loaders.mjs +2 -2
  47. package/dist/{memory-C2EVx0_O.mjs → memory-D_-5-T31.mjs} +2 -2
  48. package/dist/memory-D_-5-T31.mjs.map +1 -0
  49. package/dist/{memory-index-Do1mz0WF.mjs → memory-index-C6rujtoc.mjs} +30 -247
  50. package/dist/memory-index-C6rujtoc.mjs.map +1 -0
  51. package/dist/message-CWLcFTQ1.mjs +194 -0
  52. package/dist/message-CWLcFTQ1.mjs.map +1 -0
  53. package/dist/{message-iGPIqnn0.d.mts → message-CbBZOhq3.d.mts} +3 -3
  54. package/dist/{nip17-CKIufV98.d.mts → nip17-cTDFfFVt.d.mts} +5 -6
  55. package/dist/{nip19-BevCJZsp.d.mts → nip19-D1OnJ192.d.mts} +3 -9
  56. package/dist/{nip46-DDV4dC56.d.mts → nip46-DrsPI8EX.d.mts} +3 -5
  57. package/dist/nip51-DoPUvQ--.d.mts +87 -0
  58. package/dist/{nip59-zQgdLcEr.d.mts → nip59-BFIihLYY.d.mts} +3 -4
  59. package/dist/nip65-52HiwiyD.d.mts +23 -0
  60. package/dist/{nip77-DH4RhS5r.d.mts → nip77-C9QcZn7b.d.mts} +3 -4
  61. package/dist/nips/blossom.d.mts +12 -8
  62. package/dist/nips/blossom.mjs +30 -9
  63. package/dist/nips/blossom.mjs.map +1 -1
  64. package/dist/nips/nip04.mjs +17 -7
  65. package/dist/nips/nip04.mjs.map +1 -1
  66. package/dist/nips/nip05.d.mts +2 -3
  67. package/dist/nips/nip05.mjs +1 -4
  68. package/dist/nips/nip05.mjs.map +1 -1
  69. package/dist/nips/nip10.d.mts +3 -3
  70. package/dist/nips/nip10.mjs +3 -3
  71. package/dist/nips/nip11.d.mts +1 -1
  72. package/dist/nips/nip11.mjs +4 -4
  73. package/dist/nips/nip11.mjs.map +1 -1
  74. package/dist/nips/nip13.d.mts +7 -7
  75. package/dist/nips/nip13.mjs +32 -10
  76. package/dist/nips/nip13.mjs.map +1 -1
  77. package/dist/nips/nip17.d.mts +1 -1
  78. package/dist/nips/nip17.mjs +13 -33
  79. package/dist/nips/nip17.mjs.map +1 -1
  80. package/dist/nips/nip19.d.mts +2 -2
  81. package/dist/nips/nip19.mjs +16 -28
  82. package/dist/nips/nip19.mjs.map +1 -1
  83. package/dist/nips/nip21.d.mts +6 -1
  84. package/dist/nips/nip21.mjs +15 -5
  85. package/dist/nips/nip21.mjs.map +1 -1
  86. package/dist/nips/nip27.d.mts +4 -4
  87. package/dist/nips/nip27.mjs.map +1 -1
  88. package/dist/nips/nip42.d.mts +1 -1
  89. package/dist/nips/nip42.mjs +2 -1
  90. package/dist/nips/nip42.mjs.map +1 -1
  91. package/dist/nips/nip44.mjs +1 -1
  92. package/dist/nips/nip46.d.mts +2 -2
  93. package/dist/nips/nip46.mjs +15 -19
  94. package/dist/nips/nip46.mjs.map +1 -1
  95. package/dist/nips/nip49.d.mts +5 -5
  96. package/dist/nips/nip49.mjs +4 -4
  97. package/dist/nips/nip49.mjs.map +1 -1
  98. package/dist/nips/nip51.d.mts +2 -86
  99. package/dist/nips/nip51.mjs +7 -23
  100. package/dist/nips/nip51.mjs.map +1 -1
  101. package/dist/nips/nip57.d.mts +7 -5
  102. package/dist/nips/nip57.mjs +12 -17
  103. package/dist/nips/nip57.mjs.map +1 -1
  104. package/dist/nips/nip59.d.mts +1 -1
  105. package/dist/nips/nip59.mjs +16 -16
  106. package/dist/nips/nip59.mjs.map +1 -1
  107. package/dist/nips/nip65.d.mts +2 -2
  108. package/dist/nips/nip65.mjs +14 -37
  109. package/dist/nips/nip65.mjs.map +1 -1
  110. package/dist/nips/nip77.d.mts +1 -1
  111. package/dist/nips/nip77.mjs +33 -32
  112. package/dist/nips/nip77.mjs.map +1 -1
  113. package/dist/nips/nip98.d.mts +5 -5
  114. package/dist/nips/nip98.mjs +8 -8
  115. package/dist/nips/nip98.mjs.map +1 -1
  116. package/dist/{pool-DBIYMtwy.d.mts → pool-SzJKPIa3.d.mts} +96 -91
  117. package/dist/pool-YHRZgcQ3.mjs +596 -0
  118. package/dist/pool-YHRZgcQ3.mjs.map +1 -0
  119. package/dist/put-DS0yzVJx.mjs +187 -0
  120. package/dist/put-DS0yzVJx.mjs.map +1 -0
  121. package/dist/{reactive-7aqeip4A.d.mts → reactive-BJ1ggn4d.d.mts} +7 -22
  122. package/dist/{reactive-BfhPp9a3.mjs → reactive-BJRoVHxU.mjs} +80 -65
  123. package/dist/reactive-BJRoVHxU.mjs.map +1 -0
  124. package/dist/{pool-As20OGxB.mjs → relay-CvxPiQ_m.mjs} +178 -721
  125. package/dist/relay-CvxPiQ_m.mjs.map +1 -0
  126. package/dist/relay.d.mts +4 -4
  127. package/dist/relay.mjs +3 -2
  128. package/dist/serial-Bzpw-xz3.mjs +21 -0
  129. package/dist/serial-Bzpw-xz3.mjs.map +1 -0
  130. package/dist/{signer-CYQutDzD.mjs → signer-yW3slDC2.mjs} +12 -14
  131. package/dist/signer-yW3slDC2.mjs.map +1 -0
  132. package/dist/signer.d.mts +2 -2
  133. package/dist/signer.mjs +2 -2
  134. package/dist/{storage-Dky3wuoH.mjs → storage-CZ52ifcQ.mjs} +17 -42
  135. package/dist/storage-CZ52ifcQ.mjs.map +1 -0
  136. package/dist/storage.d.mts +3 -3
  137. package/dist/storage.mjs +4 -4
  138. package/dist/store.d.mts +1 -1
  139. package/dist/store.mjs +1 -1
  140. package/dist/testing.d.mts +18 -18
  141. package/dist/testing.mjs +9 -9
  142. package/dist/testing.mjs.map +1 -1
  143. package/dist/{types-2pCUprRU.d.mts → types-Cm-XMgtH.d.mts} +6 -6
  144. package/dist/{types-CRQEk0Oi.d.mts → types-DNmiscfd.d.mts} +2 -2
  145. package/dist/{util-LfQirgDk.mjs → util-yFYzlbrm.mjs} +29 -7
  146. package/dist/util-yFYzlbrm.mjs.map +1 -0
  147. package/dist/{verifier-t5WoWbtY.mjs → verifier-BTLOpo1n.mjs} +3 -3
  148. package/dist/verifier-BTLOpo1n.mjs.map +1 -0
  149. package/dist/verifier-CgTFpgsC.d.mts +22 -0
  150. package/dist/wasm.browser.d.mts +3 -3
  151. package/dist/wasm.browser.mjs +3 -3
  152. package/dist/wasm.d.mts +3 -3
  153. package/dist/wasm.mjs +3 -3
  154. package/dist/wasm.mjs.map +1 -1
  155. package/package.json +5 -9
  156. package/dist/abort-Ba8tDrOI.mjs.map +0 -1
  157. package/dist/builder-9tjI6WQ3.mjs.map +0 -1
  158. package/dist/client-CadO5fAX.mjs.map +0 -1
  159. package/dist/event-D8Nfoh4K.mjs.map +0 -1
  160. package/dist/gossip-oloDMWEj.mjs.map +0 -1
  161. package/dist/instance-DQE8BoWt.mjs.map +0 -1
  162. package/dist/key-Bps-LqX7.mjs.map +0 -1
  163. package/dist/loaders-o6YXB_Ri.mjs.map +0 -1
  164. package/dist/memory-C2EVx0_O.mjs.map +0 -1
  165. package/dist/memory-index-Do1mz0WF.mjs.map +0 -1
  166. package/dist/message-DcY6Fi9o.mjs +0 -193
  167. package/dist/message-DcY6Fi9o.mjs.map +0 -1
  168. package/dist/nip65-CjgmdpPB.d.mts +0 -26
  169. package/dist/nips/nip96.d.mts +0 -40
  170. package/dist/nips/nip96.mjs +0 -118
  171. package/dist/nips/nip96.mjs.map +0 -1
  172. package/dist/pool-As20OGxB.mjs.map +0 -1
  173. package/dist/reactive-BfhPp9a3.mjs.map +0 -1
  174. package/dist/signer-CYQutDzD.mjs.map +0 -1
  175. package/dist/storage-Dky3wuoH.mjs.map +0 -1
  176. package/dist/util-LfQirgDk.mjs.map +0 -1
  177. package/dist/verifier-t5WoWbtY.mjs.map +0 -1
@@ -1,11 +1,11 @@
1
- import { g as NostrError, h as MessageError, l as normalizeURL, v as WasmVerifyPoisonedError } from "./util-LfQirgDk.mjs";
2
- import { n as verifyEvent } from "./verifier-t5WoWbtY.mjs";
1
+ import { _ as MessageError, b as WasmPoisonedError, u as normalizeURL, v as NostrError } from "./util-yFYzlbrm.mjs";
2
+ import { n as verifyEvent } from "./verifier-BTLOpo1n.mjs";
3
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";
4
+ import { n as createSubscriptionId, r as encodeClientMessage, s as parseRelayMessage, t as assertSubscriptionId } from "./message-CWLcFTQ1.mjs";
5
+ import { t as NoSignerError } from "./error-gtJvV6YS.mjs";
6
6
  import { t as invokeSafely } from "./report-CHld4uWj.mjs";
7
7
  import { isAuthRequired, makeAuthEvent } from "./nips/nip42.mjs";
8
- import { n as raceSignal, r as throwIfAborted, t as abortReason } from "./abort-Ba8tDrOI.mjs";
8
+ import { i as throwIfAborted, n as onAbort, r as raceSignal, t as abortReason } from "./abort-DBq2STPj.mjs";
9
9
  import { Nip77Error, runNegSession } from "./nips/nip77.mjs";
10
10
  //#region src/relay/error.ts
11
11
  /** Base class for relay errors; carries the relay URL when known. */
@@ -62,21 +62,6 @@ function getWebSocketImplementation() {
62
62
  return impl;
63
63
  }
64
64
  //#endregion
65
- //#region src/relay/url.ts
66
- /**
67
- * True for `ws:` / `http:` URLs that are not `.onion`. Local-network `ws://` is still insecure;
68
- * allow those via a trusted set.
69
- */
70
- function isInsecureRelayUrl(url) {
71
- try {
72
- const parsed = new URL(url);
73
- if (parsed.hostname.endsWith(".onion")) return false;
74
- return parsed.protocol === "ws:" || parsed.protocol === "http:";
75
- } catch {
76
- return false;
77
- }
78
- }
79
- //#endregion
80
65
  //#region src/relay/neg-session.ts
81
66
  function createNegSession() {
82
67
  return {
@@ -117,25 +102,26 @@ async function runWiredNegSession(opts) {
117
102
  return new Promise((resolve, reject) => {
118
103
  const timer = setTimeout(() => {
119
104
  session.waiter = void 0;
105
+ disposeAbort();
120
106
  reject(timedOut());
121
107
  }, remainingMs());
122
108
  const fail = (err) => {
123
109
  clearTimeout(timer);
124
110
  session.waiter = void 0;
111
+ disposeAbort();
125
112
  reject(err);
126
113
  };
127
- const onAbort = () => {
128
- if (signal) fail(abortReason(signal));
129
- };
130
114
  session.waiter = {
131
115
  resolve: (hex) => {
132
116
  clearTimeout(timer);
133
- signal?.removeEventListener("abort", onAbort);
117
+ disposeAbort();
134
118
  resolve(hex);
135
119
  },
136
120
  reject: (err) => fail(err)
137
121
  };
138
- signal?.addEventListener("abort", onAbort, { once: true });
122
+ const disposeAbort = onAbort(signal, () => {
123
+ if (signal) fail(abortReason(signal));
124
+ });
139
125
  });
140
126
  };
141
127
  return runNegSession({
@@ -310,17 +296,17 @@ var Subscription = class {
310
296
  };
311
297
  if (opts.signal) {
312
298
  if (opts.signal.aborted) this.close("aborted");
313
- else {
314
- const onAbort = () => this.close("aborted");
315
- opts.signal.addEventListener("abort", onAbort, { once: true });
316
- this.#abort = () => opts.signal?.removeEventListener("abort", onAbort);
317
- }
299
+ else this.#abort = onAbort(opts.signal, () => this.close("aborted"));
318
300
  }
319
301
  }
302
+ /** Drop the signal listener. Terminal paths that mark `closed` without `close()` run this too. */
303
+ dispose() {
304
+ this.#abort?.();
305
+ }
320
306
  close(reason = "closed by client") {
321
307
  if (this.closed) return;
322
308
  this.closed = true;
323
- this.#abort?.();
309
+ this.dispose();
324
310
  this.#sendClose(this.id);
325
311
  invokeSafely(() => this.handlers.onclose?.(reason));
326
312
  }
@@ -355,6 +341,7 @@ function subscriptionToAsyncIterable(start, opts) {
355
341
  let wake;
356
342
  let closer;
357
343
  let localClose = false;
344
+ let disposeAbort;
358
345
  const notify = () => {
359
346
  wake?.();
360
347
  wake = void 0;
@@ -364,6 +351,7 @@ function subscriptionToAsyncIterable(start, opts) {
364
351
  });
365
352
  const closeLocal = (reason) => {
366
353
  localClose = true;
354
+ disposeAbort?.();
367
355
  closer?.close(reason);
368
356
  done = true;
369
357
  notify();
@@ -385,7 +373,10 @@ function subscriptionToAsyncIterable(start, opts) {
385
373
  });
386
374
  if (opts?.signal) {
387
375
  if (opts.signal.aborted) closeLocal("aborted");
388
- else opts.signal.addEventListener("abort", () => closeLocal("aborted"), { once: true });
376
+ else {
377
+ disposeAbort = onAbort(opts.signal, () => closeLocal("aborted"));
378
+ if (done) disposeAbort();
379
+ }
389
380
  }
390
381
  return {
391
382
  close(reason) {
@@ -421,6 +412,7 @@ function subscriptionToAsyncIterable(start, opts) {
421
412
  }
422
413
  //#endregion
423
414
  //#region src/relay/subscribe.ts
415
+ /** REQ subscription runtime: exclusive one-shot, live coalescing, dispatch, reconnect replay. */
424
416
  function openExclusive(ctx, filters, opts) {
425
417
  const sub = new Subscription(filters, opts, (id) => {
426
418
  ctx.subs.delete(id);
@@ -495,6 +487,7 @@ function forgetLiveGroup(ctx, group) {
495
487
  ctx.liveBySubId.delete(group.sub.id);
496
488
  ctx.subs.delete(group.sub.id);
497
489
  group.sub.closed = true;
490
+ group.sub.dispose();
498
491
  }
499
492
  function detachLive(ctx, group, handle) {
500
493
  if (!group.attachments.delete(handle)) return;
@@ -560,6 +553,7 @@ function closeAllSubscriptions(ctx, reason) {
560
553
  });
561
554
  for (const sub of ctx.subs.values()) if (!sub.closed) {
562
555
  sub.closed = true;
556
+ sub.dispose();
563
557
  invokeSafely(() => {
564
558
  sub.handlers.onclose?.(reason);
565
559
  });
@@ -579,6 +573,7 @@ function dropSubscription(ctx, sub, reason) {
579
573
  }
580
574
  ctx.subs.delete(sub.id);
581
575
  sub.closed = true;
576
+ sub.dispose();
582
577
  invokeSafely(() => {
583
578
  sub.handlers.onclose?.(reason);
584
579
  });
@@ -656,6 +651,7 @@ async function fetchFilters(subscribe, filters, opts) {
656
651
  const done = (err) => {
657
652
  if (settled) return;
658
653
  settled = true;
654
+ disposeAbort();
659
655
  clearTimeout(timer);
660
656
  let reason = "aborted";
661
657
  if (err instanceof Error) reason = err.message;
@@ -664,6 +660,9 @@ async function fetchFilters(subscribe, filters, opts) {
664
660
  if (err === void 0) resolve();
665
661
  else reject(err);
666
662
  };
663
+ const disposeAbort = onAbort(opts.signal, () => {
664
+ if (opts.signal !== void 0) done(abortReason(opts.signal));
665
+ });
667
666
  const timer = setTimeout(() => {
668
667
  end = { type: "timeout" };
669
668
  done();
@@ -694,7 +693,6 @@ async function fetchFilters(subscribe, filters, opts) {
694
693
  done();
695
694
  }
696
695
  });
697
- opts.signal?.addEventListener("abort", () => done(opts.signal ? abortReason(opts.signal) : void 0), { once: true });
698
696
  });
699
697
  return {
700
698
  events,
@@ -722,7 +720,17 @@ function armEoseTimeout(sub, eoseTimeoutMs) {
722
720
  }
723
721
  //#endregion
724
722
  //#region src/relay/relay.ts
725
- /** Relay lifecycle states. */
723
+ /**
724
+ * Relay lifecycle states:
725
+ *
726
+ * - `initialized` — constructed; `connect()` not yet attempted.
727
+ * - `connecting` — a `connect()` attempt is in flight (socket opening / resubscribing).
728
+ * - `connected` — socket open; REQ/EVENT/COUNT flow.
729
+ * - `disconnected` — socket down: a reconnect is scheduled when `enableReconnect` has live
730
+ * subscriptions to restore, or `disconnect()` severed manually (REQ state kept).
731
+ * - `closed` — terminal until the next `connect()`: `close()` or a socket death without a scheduled
732
+ * reconnect.
733
+ */
726
734
  const RelayStatus = {
727
735
  Initialized: "initialized",
728
736
  Connecting: "connecting",
@@ -730,6 +738,7 @@ const RelayStatus = {
730
738
  Disconnected: "disconnected",
731
739
  Closed: "closed"
732
740
  };
741
+ const DEFAULT_REQUEST_TIMEOUT_MS = 4400;
733
742
  const DEFAULT_BACKOFF = [
734
743
  1e3,
735
744
  2e3,
@@ -747,7 +756,6 @@ const noop = () => {};
747
756
  var Relay = class Relay {
748
757
  url;
749
758
  #ws;
750
- #connected = false;
751
759
  #gen = 0;
752
760
  #status = RelayStatus.Initialized;
753
761
  #connecting;
@@ -777,25 +785,19 @@ var Relay = class Relay {
777
785
  #answeredResult;
778
786
  #authPromise;
779
787
  #authSigner;
780
- #intentionalClose = false;
788
+ /** `close()`/`disconnect()` was called; cleared by the next `connect()`. */
789
+ #manualStop = false;
781
790
  #reconnectAttempts = 0;
782
791
  #reconnectTimer;
783
- #skipReconnect = false;
784
- /** Collapses error+close pairs into a single terminal/reconnect action. */
785
- #deathHandled = false;
786
792
  #enablePing;
787
793
  #ping;
788
- onnotice;
789
- onclose;
790
- /** Fired when the relay sends a NIP-42 AUTH challenge. */
791
- onauth;
792
- /** Fired after a successful reconnect (not the initial connect). */
793
- onreconnect;
794
- /**
795
- * Fired when a delivered EVENT fails id/signature verification, just before the event is dropped.
796
- * A poisoned verifier dropping events does not fire it.
797
- */
798
- oninvalidevent;
794
+ #listeners = {
795
+ notice: /* @__PURE__ */ new Set(),
796
+ close: /* @__PURE__ */ new Set(),
797
+ auth: /* @__PURE__ */ new Set(),
798
+ reconnect: /* @__PURE__ */ new Set(),
799
+ invalidevent: /* @__PURE__ */ new Set()
800
+ };
799
801
  constructor(url, opts = {}) {
800
802
  this.url = normalizeURL(url);
801
803
  this.#WS = opts.websocketImplementation ?? getWebSocketImplementation();
@@ -823,7 +825,7 @@ var Relay = class Relay {
823
825
  liveByFp: this.#liveByFp,
824
826
  liveBySubId: this.#liveBySubId,
825
827
  subs: this.#subs,
826
- connected: () => this.#connected,
828
+ connected: () => this.#isOpen(),
827
829
  enableReconnect: () => this.#enableReconnect,
828
830
  send: (message) => this.#send(message),
829
831
  scheduleReconnect: () => this.#scheduleReconnect(),
@@ -836,11 +838,17 @@ var Relay = class Relay {
836
838
  return this.#challenge;
837
839
  }
838
840
  get connected() {
839
- return this.#connected;
841
+ return this.#isOpen();
840
842
  }
841
843
  get status() {
842
844
  return this.#status;
843
845
  }
846
+ #isOpen() {
847
+ return this.#status === RelayStatus.Connected;
848
+ }
849
+ #setStatus(next) {
850
+ this.#status = next;
851
+ }
844
852
  get generation() {
845
853
  return this.#gen;
846
854
  }
@@ -855,26 +863,38 @@ var Relay = class Relay {
855
863
  get inFlightCount() {
856
864
  return this.#publishes.size + this.#counts.size + this.#neg.size;
857
865
  }
866
+ /**
867
+ * Listen for a relay event. Multiple listeners per type are allowed and fire in registration
868
+ * order; a throwing listener is reported and does not break the dispatch. Returns an unsubscribe
869
+ * function.
870
+ */
871
+ on(type, listener) {
872
+ const set = this.#listeners[type];
873
+ set.add(listener);
874
+ return () => {
875
+ set.delete(listener);
876
+ };
877
+ }
878
+ #emit(type, payload) {
879
+ for (const listener of [...this.#listeners[type]]) invokeSafely(() => listener(payload));
880
+ }
858
881
  static async connect(url, opts) {
859
882
  const relay = new Relay(url, opts);
860
883
  await relay.connect({ signal: opts?.signal });
861
884
  return relay;
862
885
  }
863
886
  async connect(opts) {
864
- if (this.#connected) return;
887
+ if (this.#isOpen()) return;
865
888
  if (this.#connecting) {
866
889
  await raceSignal(this.#connecting, opts?.signal);
867
890
  return;
868
891
  }
869
892
  throwIfAborted(opts?.signal);
870
893
  const gen = ++this.#gen;
871
- this.#intentionalClose = false;
872
- this.#skipReconnect = false;
873
- this.#deathHandled = false;
894
+ this.#manualStop = false;
874
895
  this.#clearReconnectTimer();
875
- this.#status = RelayStatus.Connecting;
896
+ this.#setStatus(RelayStatus.Connecting);
876
897
  const timeoutMs = opts?.timeoutMs ?? this.#connectTimeoutMs;
877
- const isReconnect = this.#reconnectAttempts > 0;
878
898
  let resolveConnect = noop;
879
899
  let rejectConnect = noop;
880
900
  let settled = false;
@@ -885,12 +905,7 @@ var Relay = class Relay {
885
905
  });
886
906
  const release = () => {
887
907
  if (ws === void 0) return;
888
- try {
889
- ws.removeEventListener("open", handlers.onOpen);
890
- ws.removeEventListener("error", handlers.onError);
891
- ws.removeEventListener("close", handlers.onClose);
892
- ws.removeEventListener("message", handlers.onMessage);
893
- } catch {}
908
+ this.#removeSocketListeners(handlers);
894
909
  if (this.#socketHandlers === handlers) this.#socketHandlers = void 0;
895
910
  try {
896
911
  if (ws.readyState !== this.#WS.CLOSED && ws.readyState !== this.#WS.CLOSING) ws.close();
@@ -914,7 +929,6 @@ var Relay = class Relay {
914
929
  release();
915
930
  return;
916
931
  }
917
- if (!isReconnect && !this.#enableReconnect) this.#skipReconnect = true;
918
932
  release();
919
933
  finish(new RelayTimeoutError("connection timed out", this.url));
920
934
  if (gen === this.#gen) this.#handleSocketDeath("connection timed out", {
@@ -926,7 +940,6 @@ var Relay = class Relay {
926
940
  try {
927
941
  ws = new this.#WS(this.url);
928
942
  } catch (error) {
929
- if (!isReconnect && !this.#enableReconnect) this.#skipReconnect = true;
930
943
  finish(error);
931
944
  if (gen === this.#gen) this.#handleSocketDeath("connection failed", {
932
945
  fromConnectAttempt: true,
@@ -941,9 +954,7 @@ var Relay = class Relay {
941
954
  release();
942
955
  return;
943
956
  }
944
- this.#connected = true;
945
- this.#status = RelayStatus.Connected;
946
- this.#deathHandled = false;
957
+ this.#setStatus(RelayStatus.Connected);
947
958
  const wasReconnect = this.#reconnectAttempts > 0;
948
959
  this.#challenge = void 0;
949
960
  this.#authPromise = void 0;
@@ -951,18 +962,16 @@ var Relay = class Relay {
951
962
  this.#answeredChallenge = void 0;
952
963
  this.#answeredResult = void 0;
953
964
  if (!resubscribeAll(this.#live)) {
954
- this.#connected = false;
955
- this.#status = RelayStatus.Disconnected;
956
- if (!isReconnect && !this.#enableReconnect) this.#skipReconnect = true;
965
+ this.#setStatus(RelayStatus.Disconnected);
957
966
  release();
958
967
  finish(new RelayConnectionError("connection failed", this.url));
959
- if (this.#enableReconnect && !this.#intentionalClose && !this.#skipReconnect && this.#subs.size > 0) this.#scheduleReconnect();
968
+ if (this.#enableReconnect && !this.#manualStop && this.#subs.size > 0) this.#scheduleReconnect();
960
969
  return;
961
970
  }
962
971
  this.#reconnectAttempts = 0;
963
972
  if (this.#enablePing) this.#ping.start();
964
973
  else this.#ping.stop();
965
- if (wasReconnect) invokeSafely(() => this.onreconnect?.());
974
+ if (wasReconnect) this.#emit("reconnect", void 0);
966
975
  finish();
967
976
  };
968
977
  const onError = () => {
@@ -970,9 +979,7 @@ var Relay = class Relay {
970
979
  release();
971
980
  return;
972
981
  }
973
- this.#connected = false;
974
982
  const fromConnectAttempt = !settled;
975
- if (!settled && !isReconnect && !this.#enableReconnect) this.#skipReconnect = true;
976
983
  release();
977
984
  finish(new RelayConnectionError("connection failed", this.url));
978
985
  if (gen === this.#gen) this.#handleSocketDeath("connection failed", {
@@ -985,9 +992,7 @@ var Relay = class Relay {
985
992
  release();
986
993
  return;
987
994
  }
988
- this.#connected = false;
989
995
  const fromConnectAttempt = !settled;
990
- if (!settled && !isReconnect && !this.#enableReconnect) this.#skipReconnect = true;
991
996
  release();
992
997
  if (fromConnectAttempt) finish(new RelayConnectionError("websocket closed", this.url));
993
998
  if (gen === this.#gen) this.#handleSocketDeath("websocket closed", {
@@ -1014,23 +1019,25 @@ var Relay = class Relay {
1014
1019
  ws.addEventListener("message", onMessage);
1015
1020
  await raceSignal(connecting, opts?.signal);
1016
1021
  }
1017
- #detachSocketHandlers() {
1018
- const h = this.#socketHandlers;
1019
- if (!h) return;
1022
+ #removeSocketListeners(h) {
1020
1023
  try {
1021
1024
  h.ws.removeEventListener("open", h.onOpen);
1022
1025
  h.ws.removeEventListener("error", h.onError);
1023
1026
  h.ws.removeEventListener("close", h.onClose);
1024
1027
  h.ws.removeEventListener("message", h.onMessage);
1025
1028
  } catch {}
1029
+ }
1030
+ #detachSocketHandlers() {
1031
+ const h = this.#socketHandlers;
1032
+ if (!h) return;
1033
+ this.#removeSocketListeners(h);
1026
1034
  this.#socketHandlers = void 0;
1027
1035
  }
1028
1036
  /** Graceful shutdown: disables reconnect and closes all subscriptions. */
1029
1037
  close() {
1030
1038
  this.#gen += 1;
1031
- this.#status = RelayStatus.Closed;
1032
- this.#intentionalClose = true;
1033
- this.#skipReconnect = true;
1039
+ this.#setStatus(RelayStatus.Closed);
1040
+ this.#manualStop = true;
1034
1041
  this.#clearReconnectTimer();
1035
1042
  if (this.#connectTimer !== void 0) {
1036
1043
  clearTimeout(this.#connectTimer);
@@ -1041,13 +1048,10 @@ var Relay = class Relay {
1041
1048
  try {
1042
1049
  closeAllSubscriptions(this.#live, "relay closed");
1043
1050
  } finally {
1044
- this.#rejectPublishes(new RelayClosedError("relay closed", this.url));
1045
- this.#rejectCounts(new RelayClosedError("relay closed", this.url));
1046
- this.#rejectNeg(new RelayClosedError("relay closed", this.url));
1051
+ this.#failPending(new RelayClosedError("relay closed", this.url));
1047
1052
  this.#detachSocketHandlers();
1048
1053
  this.#teardownSocket();
1049
- this.#connected = false;
1050
- invokeSafely(() => this.onclose?.());
1054
+ this.#emit("close", void 0);
1051
1055
  }
1052
1056
  }
1053
1057
  /**
@@ -1057,9 +1061,8 @@ var Relay = class Relay {
1057
1061
  */
1058
1062
  disconnect() {
1059
1063
  this.#gen += 1;
1060
- this.#status = RelayStatus.Disconnected;
1061
- this.#intentionalClose = true;
1062
- this.#skipReconnect = true;
1064
+ this.#setStatus(RelayStatus.Disconnected);
1065
+ this.#manualStop = true;
1063
1066
  this.#clearReconnectTimer();
1064
1067
  if (this.#connectTimer !== void 0) {
1065
1068
  clearTimeout(this.#connectTimer);
@@ -1067,12 +1070,9 @@ var Relay = class Relay {
1067
1070
  }
1068
1071
  this.#ping.stop();
1069
1072
  this.#connectFinish?.(new RelayClosedError("relay disconnected", this.url));
1070
- this.#rejectPublishes(new RelayClosedError("relay disconnected", this.url));
1071
- this.#rejectCounts(new RelayClosedError("relay disconnected", this.url));
1072
- this.#rejectNeg(new RelayClosedError("relay disconnected", this.url));
1073
+ this.#failPending(new RelayClosedError("relay disconnected", this.url));
1073
1074
  this.#detachSocketHandlers();
1074
1075
  this.#teardownSocket();
1075
- this.#connected = false;
1076
1076
  }
1077
1077
  #teardownSocket() {
1078
1078
  try {
@@ -1104,49 +1104,50 @@ var Relay = class Relay {
1104
1104
  for (const session of this.#neg.values()) failNegSession(session, err);
1105
1105
  this.#neg.clear();
1106
1106
  }
1107
+ #failPending(err) {
1108
+ this.#rejectPublishes(err);
1109
+ this.#rejectCounts(err);
1110
+ this.#rejectNeg(err);
1111
+ }
1107
1112
  /** Unexpected socket death. Keep subscriptions if reconnecting. */
1108
1113
  #handleSocketDeath(reason, opts) {
1109
1114
  if (opts.gen !== this.#gen) return;
1110
- if (this.#deathHandled) return;
1111
- this.#deathHandled = true;
1115
+ this.#gen += 1;
1112
1116
  this.#ping.stop();
1113
1117
  this.#detachSocketHandlers();
1114
- this.#connected = false;
1115
1118
  this.#ws = void 0;
1116
- this.#rejectPublishes(new RelayClosedError(reason, this.url));
1117
- this.#rejectCounts(new RelayClosedError(reason, this.url));
1118
- this.#rejectNeg(new RelayClosedError(reason, this.url));
1119
- if (this.#enableReconnect && !this.#intentionalClose && !this.#skipReconnect && this.#subs.size > 0) {
1120
- this.#status = RelayStatus.Disconnected;
1119
+ this.#failPending(new RelayClosedError(reason, this.url));
1120
+ if (this.#enableReconnect && !this.#manualStop && this.#subs.size > 0) {
1121
+ this.#setStatus(RelayStatus.Disconnected);
1121
1122
  this.#scheduleReconnect();
1122
1123
  return;
1123
1124
  }
1124
- if (!this.#intentionalClose) this.#status = RelayStatus.Disconnected;
1125
+ this.#setStatus(RelayStatus.Disconnected);
1125
1126
  if (opts.fromConnectAttempt !== true || this.#subs.size > 0) {
1126
1127
  closeAllSubscriptions(this.#live, reason);
1127
- if (!this.#intentionalClose) invokeSafely(() => this.onclose?.());
1128
+ if (!this.#manualStop) this.#emit("close", void 0);
1128
1129
  }
1129
- this.#status = RelayStatus.Closed;
1130
+ this.#setStatus(RelayStatus.Closed);
1130
1131
  }
1131
1132
  #scheduleReconnect() {
1132
1133
  if (this.#reconnectTimer !== void 0) return;
1133
- if (this.#connected) return;
1134
+ if (this.#isOpen()) return;
1134
1135
  const delay = this.#backoff[Math.min(this.#reconnectAttempts, this.#backoff.length - 1)] ?? 6e4;
1135
1136
  this.#reconnectAttempts += 1;
1136
1137
  this.#reconnectTimer = setTimeout(() => {
1137
1138
  this.#reconnectTimer = void 0;
1138
- if (this.#intentionalClose || this.#connected) return;
1139
+ if (this.#manualStop || this.#isOpen()) return;
1139
1140
  (async () => {
1140
1141
  try {
1141
1142
  await this.connect();
1142
1143
  } catch {
1143
- if (this.#enableReconnect && !this.#intentionalClose && !this.#skipReconnect && this.#subs.size > 0 && !this.#connected) this.#scheduleReconnect();
1144
+ if (this.#enableReconnect && !this.#manualStop && this.#subs.size > 0 && !this.#isOpen()) this.#scheduleReconnect();
1144
1145
  }
1145
1146
  })();
1146
1147
  }, delay);
1147
1148
  }
1148
1149
  #send(message) {
1149
- if (!this.#ws || !this.#connected) throw new RelayClosedError("not connected", this.url);
1150
+ if (!this.#ws || !this.#isOpen()) throw new RelayClosedError("not connected", this.url);
1150
1151
  const raw = typeof message === "string" ? message : encodeClientMessage(message);
1151
1152
  this.#ws.send(raw);
1152
1153
  }
@@ -1233,7 +1234,7 @@ var Relay = class Relay {
1233
1234
  }
1234
1235
  case "NOTICE": {
1235
1236
  const [, notice] = msg;
1236
- invokeSafely(() => this.onnotice?.(notice));
1237
+ this.#emit("notice", notice);
1237
1238
  break;
1238
1239
  }
1239
1240
  case "AUTH": {
@@ -1245,7 +1246,7 @@ var Relay = class Relay {
1245
1246
  this.#answeredResult = void 0;
1246
1247
  }
1247
1248
  this.#challenge = authChallenge;
1248
- invokeSafely(() => this.onauth?.(authChallenge));
1249
+ this.#emit("auth", authChallenge);
1249
1250
  break;
1250
1251
  }
1251
1252
  }
@@ -1253,7 +1254,7 @@ var Relay = class Relay {
1253
1254
  /** Low-level REQ with callbacks. Survives reconnect when enableReconnect is on. */
1254
1255
  subscribe(filters, opts = {}) {
1255
1256
  if (filters.length === 0) throw new MessageError("REQ requires at least one filter");
1256
- if (!this.#connected && !this.#enableReconnect) throw new RelayClosedError("not connected", this.url);
1257
+ if (!this.#isOpen() && !this.#enableReconnect) throw new RelayClosedError("not connected", this.url);
1257
1258
  const canonical = canonicalizeFilters(filters);
1258
1259
  if (opts.closeOnEose === true) return openExclusive(this.#live, canonical, opts);
1259
1260
  return subscribeLive(this.#live, canonical, opts);
@@ -1262,12 +1263,12 @@ var Relay = class Relay {
1262
1263
  if (this.#verifyDead) return false;
1263
1264
  try {
1264
1265
  const ok = this.#verify(event);
1265
- if (!ok) invokeSafely(() => this.oninvalidevent?.());
1266
+ if (!ok) this.#emit("invalidevent", void 0);
1266
1267
  return ok;
1267
1268
  } catch (error) {
1268
- if (error instanceof WasmVerifyPoisonedError || error instanceof WebAssembly.RuntimeError) {
1269
+ if (error instanceof WasmPoisonedError) {
1269
1270
  this.#verifyDead = true;
1270
- invokeSafely(() => this.onnotice?.("verify-poisoned: wasm instance aborted"));
1271
+ this.#emit("notice", "wasm-poisoned: instance aborted");
1271
1272
  return false;
1272
1273
  }
1273
1274
  throw error;
@@ -1285,53 +1286,61 @@ var Relay = class Relay {
1285
1286
  async fetch(filters, opts) {
1286
1287
  if (filters.length === 0) throw new MessageError("REQ requires at least one filter");
1287
1288
  const canonical = canonicalizeFilters(filters);
1288
- if (!this.#connected) await this.connect({ signal: opts?.signal });
1289
+ if (!this.#isOpen()) await this.connect({ signal: opts?.signal });
1289
1290
  return fetchFilters((f, o) => this.subscribe(f, o), canonical, {
1290
1291
  timeoutMs: opts?.timeoutMs ?? 4400,
1291
1292
  signal: opts?.signal,
1292
1293
  id: opts?.id
1293
1294
  });
1294
1295
  }
1295
- /** Publish an event and wait for OK. */
1296
+ /**
1297
+ * Publish an event and wait for OK. A concurrent publish of the same event id joins the in-flight
1298
+ * publish instead of racing it — the joiner's `timeoutMs` does not apply.
1299
+ */
1296
1300
  async publish(event, opts) {
1297
- if (!this.#connected) throw new RelayClosedError("not connected", this.url);
1301
+ if (!this.#isOpen()) throw new RelayClosedError("not connected", this.url);
1302
+ const existing = this.#publishes.get(event.id);
1303
+ if (existing?.event !== void 0) return existing.promise;
1298
1304
  const timeoutMs = opts?.timeoutMs ?? this.#publishTimeoutMs;
1299
- return await new Promise((resolve, reject) => {
1300
- const timer = setTimeout(() => {
1301
- this.#publishes.delete(event.id);
1302
- reject(new RelayTimeoutError("publish timed out", this.url));
1303
- }, timeoutMs);
1304
- this.#publishes.set(event.id, {
1305
+ const promise = new Promise((resolve, reject) => {
1306
+ const waiter = {
1305
1307
  resolve,
1306
1308
  reject,
1307
- timer,
1309
+ timer: setTimeout(() => {
1310
+ if (this.#publishes.get(event.id) === waiter) {
1311
+ this.#publishes.delete(event.id);
1312
+ reject(new RelayTimeoutError("publish timed out", this.url));
1313
+ }
1314
+ }, timeoutMs),
1308
1315
  event,
1309
- timeoutMs
1310
- });
1316
+ timeoutMs,
1317
+ get promise() {
1318
+ return promise;
1319
+ }
1320
+ };
1321
+ this.#publishes.set(event.id, waiter);
1311
1322
  try {
1312
1323
  this.#send(["EVENT", event]);
1313
1324
  } catch (error) {
1314
- clearTimeout(timer);
1315
- this.#publishes.delete(event.id);
1325
+ clearTimeout(waiter.timer);
1326
+ if (this.#publishes.get(event.id) === waiter) this.#publishes.delete(event.id);
1316
1327
  reject(error instanceof Error ? error : new RelayPublishError("publish failed", this.url));
1317
1328
  }
1318
1329
  });
1330
+ return promise;
1319
1331
  }
1320
1332
  /**
1321
1333
  * NIP-45 COUNT: ask the relay how many events match `filters`. Resolves with the COUNT payload
1322
1334
  * (count / optional approximate / optional hll).
1323
1335
  */
1324
1336
  async count(filters, opts) {
1325
- if (!this.#connected) throw new RelayClosedError("not connected", this.url);
1337
+ if (!this.#isOpen()) throw new RelayClosedError("not connected", this.url);
1326
1338
  if (filters.length === 0) throw new RelayError("COUNT requires at least one filter", this.url);
1327
1339
  throwIfAborted(opts?.signal);
1328
1340
  const canonical = canonicalizeFilters(filters);
1329
1341
  const id = opts?.id === void 0 ? this.nextSubId("count") : createSubscriptionId(opts.id);
1330
1342
  const timeoutMs = opts?.timeoutMs ?? this.#publishTimeoutMs;
1331
1343
  return new Promise((resolve, reject) => {
1332
- const cleanup = () => {
1333
- opts?.signal?.removeEventListener("abort", onAbort);
1334
- };
1335
1344
  const waiter = {
1336
1345
  resolve: (result) => {
1337
1346
  if (waiter.timer !== void 0) clearTimeout(waiter.timer);
@@ -1352,14 +1361,13 @@ var Relay = class Relay {
1352
1361
  authRetried: false,
1353
1362
  timeoutMs
1354
1363
  };
1355
- const onAbort = () => {
1364
+ const cleanup = onAbort(opts?.signal, () => {
1356
1365
  if (opts?.signal) waiter.reject(abortReason(opts.signal));
1357
- };
1366
+ });
1358
1367
  waiter.timer = setTimeout(() => {
1359
1368
  waiter.reject(new RelayTimeoutError("count timed out", this.url));
1360
1369
  }, timeoutMs);
1361
1370
  this.#counts.set(id, waiter);
1362
- opts?.signal?.addEventListener("abort", onAbort, { once: true });
1363
1371
  try {
1364
1372
  this.#send([
1365
1373
  "COUNT",
@@ -1389,14 +1397,10 @@ var Relay = class Relay {
1389
1397
  };
1390
1398
  throw error;
1391
1399
  }
1392
- if (!this.#connected) throw new RelayClosedError("not connected", this.url);
1400
+ if (!this.#isOpen()) throw new RelayClosedError("not connected", this.url);
1393
1401
  const timeoutMs = opts?.timeoutMs ?? this.#publishTimeoutMs;
1394
- return new Promise((resolve, reject) => {
1395
- const timer = setTimeout(() => {
1396
- this.#publishes.delete(event.id);
1397
- reject(new RelayTimeoutError("auth timed out", this.url));
1398
- }, timeoutMs);
1399
- this.#publishes.set(event.id, {
1402
+ const authPromise = new Promise((resolve, reject) => {
1403
+ const waiter = {
1400
1404
  resolve: (result) => {
1401
1405
  if (this.#challenge === challenge) {
1402
1406
  this.#answeredChallenge = challenge;
@@ -1405,17 +1409,27 @@ var Relay = class Relay {
1405
1409
  resolve(result);
1406
1410
  },
1407
1411
  reject,
1408
- timer,
1409
- timeoutMs
1410
- });
1412
+ timer: setTimeout(() => {
1413
+ if (this.#publishes.get(event.id) === waiter) {
1414
+ this.#publishes.delete(event.id);
1415
+ reject(new RelayTimeoutError("auth timed out", this.url));
1416
+ }
1417
+ }, timeoutMs),
1418
+ timeoutMs,
1419
+ get promise() {
1420
+ return authPromise;
1421
+ }
1422
+ };
1423
+ this.#publishes.set(event.id, waiter);
1411
1424
  try {
1412
1425
  this.#send(["AUTH", event]);
1413
1426
  } catch (error) {
1414
- clearTimeout(timer);
1415
- this.#publishes.delete(event.id);
1427
+ clearTimeout(waiter.timer);
1428
+ if (this.#publishes.get(event.id) === waiter) this.#publishes.delete(event.id);
1416
1429
  reject(error instanceof Error ? error : new RelayPublishError("auth send failed", this.url));
1417
1430
  }
1418
1431
  });
1432
+ return authPromise;
1419
1433
  })();
1420
1434
  this.#authPromise = pending;
1421
1435
  try {
@@ -1428,8 +1442,8 @@ var Relay = class Relay {
1428
1442
  }
1429
1443
  /**
1430
1444
  * Clear a cached AUTH rejection for the current challenge so a later `auth()` signs again. A
1431
- * successful auth and in-flight answers are kept. When a challenge is pending and unanswered,
1432
- * `onauth` is re-fired so the pool's automatic auth can run once more.
1445
+ * successful auth and in-flight answers are kept. When a challenge is pending and unanswered, an
1446
+ * `auth` event is re-fired so the pool's automatic auth can run once more.
1433
1447
  */
1434
1448
  resetAuth() {
1435
1449
  if (this.#answeredResult !== void 0 && !this.#answeredResult.ok) {
@@ -1438,7 +1452,7 @@ var Relay = class Relay {
1438
1452
  }
1439
1453
  if (this.#challenge !== void 0 && this.#challenge !== this.#authedChallenge) {
1440
1454
  const challenge = this.#challenge;
1441
- invokeSafely(() => this.onauth?.(challenge));
1455
+ this.#emit("auth", challenge);
1442
1456
  }
1443
1457
  }
1444
1458
  async #ensureAuthed() {
@@ -1463,7 +1477,7 @@ var Relay = class Relay {
1463
1477
  dropSubscription(this.#live, sub, reason);
1464
1478
  return;
1465
1479
  }
1466
- if (sub.closed || !this.#connected) {
1480
+ if (sub.closed || !this.#isOpen()) {
1467
1481
  dropSubscription(this.#live, sub, reason);
1468
1482
  return;
1469
1483
  }
@@ -1494,7 +1508,7 @@ var Relay = class Relay {
1494
1508
  fail();
1495
1509
  return;
1496
1510
  }
1497
- if (!this.#connected) {
1511
+ if (!this.#isOpen()) {
1498
1512
  fail();
1499
1513
  return;
1500
1514
  }
@@ -1518,7 +1532,7 @@ var Relay = class Relay {
1518
1532
  }
1519
1533
  const finish = (result) => {
1520
1534
  if (waiter.timer !== void 0) clearTimeout(waiter.timer);
1521
- this.#publishes.delete(eventId);
1535
+ if (this.#publishes.get(eventId) === waiter) this.#publishes.delete(eventId);
1522
1536
  waiter.resolve(result);
1523
1537
  };
1524
1538
  try {
@@ -1538,7 +1552,7 @@ var Relay = class Relay {
1538
1552
  });
1539
1553
  return;
1540
1554
  }
1541
- if (!this.#connected) {
1555
+ if (!this.#isOpen()) {
1542
1556
  finish({
1543
1557
  ok: false,
1544
1558
  message
@@ -1546,8 +1560,10 @@ var Relay = class Relay {
1546
1560
  return;
1547
1561
  }
1548
1562
  waiter.timer = setTimeout(() => {
1549
- this.#publishes.delete(eventId);
1550
- waiter.reject(new RelayTimeoutError("publish timed out", this.url));
1563
+ if (this.#publishes.get(eventId) === waiter) {
1564
+ this.#publishes.delete(eventId);
1565
+ waiter.reject(new RelayTimeoutError("publish timed out", this.url));
1566
+ }
1551
1567
  }, waiter.timeoutMs);
1552
1568
  this.#send(["EVENT", waiter.event]);
1553
1569
  } catch {
@@ -1566,7 +1582,7 @@ var Relay = class Relay {
1566
1582
  * `NEG-MSG`), not a per-message budget. Default: {@link RelayOptions.publishTimeoutMs}.
1567
1583
  */
1568
1584
  async negReconcile(filter, storage, opts) {
1569
- if (!this.#connected) throw new RelayClosedError("not connected", this.url);
1585
+ if (!this.#isOpen()) throw new RelayClosedError("not connected", this.url);
1570
1586
  throwIfAborted(opts?.signal);
1571
1587
  const canonical = canonicalizeFilter(filter);
1572
1588
  const id = opts?.id === void 0 ? this.nextSubId("neg") : assertSubscriptionId(opts.id);
@@ -1602,565 +1618,6 @@ var Relay = class Relay {
1602
1618
  }
1603
1619
  };
1604
1620
  //#endregion
1605
- //#region src/relay/fan-in.ts
1606
- /**
1607
- * Aggregate EOSE/close across routed jobs. One `seen` set. `pending` counts URL list entries
1608
- * (duplicates included). `pendingEose` counts unique `jobIndex:url` keys.
1609
- */
1610
- function fanIn(pool, jobs, opts = {}) {
1611
- const seen = /* @__PURE__ */ new Set();
1612
- const closers = [];
1613
- let closed = false;
1614
- let eoseFired = false;
1615
- let sawEose = false;
1616
- let eoseTimer;
1617
- const eoseDone = /* @__PURE__ */ new Set();
1618
- const eoseAttempted = /* @__PURE__ */ new Set();
1619
- let pendingEose = 0;
1620
- let pending = 0;
1621
- const fireEose = () => {
1622
- if (closed || eoseFired) return;
1623
- eoseFired = true;
1624
- if (eoseTimer !== void 0) {
1625
- clearTimeout(eoseTimer);
1626
- eoseTimer = void 0;
1627
- }
1628
- invokeSafely(() => opts.oneose?.());
1629
- };
1630
- /**
1631
- * Remove one relay from the pending-EOSE set. `eosed` credits a real EOSE; a CLOSED or a connect
1632
- * failure only drops the wait — the aggregate `oneose` fires once every remaining relay ended AND
1633
- * at least one actually EOSE'd.
1634
- */
1635
- const settleEose = (jobIndex, url, eosed) => {
1636
- const key = `${jobIndex}:${url}`;
1637
- if (eoseDone.has(key)) return;
1638
- eoseDone.add(key);
1639
- if (eosed) sawEose = true;
1640
- pendingEose -= 1;
1641
- if (pendingEose === 0 && sawEose) fireEose();
1642
- };
1643
- const settleClose = () => {
1644
- closed = true;
1645
- if (eoseTimer !== void 0) {
1646
- clearTimeout(eoseTimer);
1647
- eoseTimer = void 0;
1648
- }
1649
- };
1650
- const closeAll = (reason) => {
1651
- if (closed) return;
1652
- settleClose();
1653
- for (const c of closers) c.close(reason);
1654
- invokeSafely(() => opts.onclose?.(reason ?? "closed by client"));
1655
- };
1656
- if (opts.signal?.aborted === true) {
1657
- closed = true;
1658
- invokeSafely(() => opts.onclose?.("aborted"));
1659
- return { close: closeAll };
1660
- }
1661
- opts.signal?.addEventListener("abort", () => closeAll("aborted"), { once: true });
1662
- const failUrl = (jobIndex, key) => {
1663
- settleEose(jobIndex, key, false);
1664
- pending -= 1;
1665
- if (pending <= 0 && closers.length === 0 && !closed) {
1666
- settleClose();
1667
- invokeSafely(() => opts.onclose?.("all relays failed"));
1668
- }
1669
- };
1670
- const attach = (relay, job, jobIndex) => {
1671
- if (closed) return;
1672
- const received = opts.receivedEvent;
1673
- const sub = relay.subscribe([...job.filters], {
1674
- id: jobs.length === 1 ? job.id : void 0,
1675
- closeOnEose: opts.closeOnEose,
1676
- alreadyHaveEvent: (id) => {
1677
- let have = seen.has(id);
1678
- invokeSafely(() => {
1679
- have = have || Boolean(opts.alreadyHaveEvent?.(id));
1680
- });
1681
- return have;
1682
- },
1683
- receivedEvent: received === void 0 ? void 0 : (id) => invokeSafely(() => received(id, relay.url)),
1684
- onevent: (event) => {
1685
- seen.add(event.id);
1686
- invokeSafely(() => opts.onevent?.(event, relay.url));
1687
- },
1688
- oneose: () => settleEose(jobIndex, relay.url, true),
1689
- onclose: (reason) => {
1690
- settleEose(jobIndex, relay.url, false);
1691
- pending -= 1;
1692
- if (pending <= 0 && !closed) {
1693
- settleClose();
1694
- invokeSafely(() => opts.onclose?.(reason));
1695
- }
1696
- }
1697
- });
1698
- closers.push(sub);
1699
- };
1700
- for (const [jobIndex, job] of jobs.entries()) {
1701
- const jobUrls = /* @__PURE__ */ new Set();
1702
- for (const url of job.urls) {
1703
- let key;
1704
- try {
1705
- key = normalizeURL(url);
1706
- } catch {
1707
- key = url;
1708
- }
1709
- if (jobUrls.has(key)) continue;
1710
- jobUrls.add(key);
1711
- pending += 1;
1712
- const eoseKey = `${jobIndex}:${key}`;
1713
- if (!eoseAttempted.has(eoseKey)) {
1714
- eoseAttempted.add(eoseKey);
1715
- pendingEose += 1;
1716
- }
1717
- const tryAttach = (relay) => {
1718
- try {
1719
- attach(relay, job, jobIndex);
1720
- } catch (error) {
1721
- if (error instanceof RelayClosedError) {
1722
- failUrl(jobIndex, key);
1723
- return;
1724
- }
1725
- throw error;
1726
- }
1727
- };
1728
- (async () => {
1729
- let relay;
1730
- try {
1731
- relay = await pool.ensureRelay(key, {
1732
- signal: opts.signal,
1733
- timeoutMs: opts.connectTimeoutMs
1734
- });
1735
- } catch {
1736
- const existing = pool.getRelay(key);
1737
- if (existing !== void 0) {
1738
- tryAttach(existing);
1739
- return;
1740
- }
1741
- failUrl(jobIndex, key);
1742
- return;
1743
- }
1744
- tryAttach(relay);
1745
- })();
1746
- }
1747
- }
1748
- if (opts.eoseTimeoutMs !== void 0 && pending > 0) eoseTimer = setTimeout(() => {
1749
- eoseTimer = void 0;
1750
- fireEose();
1751
- }, opts.eoseTimeoutMs);
1752
- if (pending === 0) queueMicrotask(() => {
1753
- if (closed) return;
1754
- settleClose();
1755
- invokeSafely(() => opts.onclose?.("no relays"));
1756
- });
1757
- return { close: closeAll };
1758
- }
1759
- async function fetchRouted(pool, jobs, opts = {}) {
1760
- throwIfAborted(opts.signal);
1761
- const byId = /* @__PURE__ */ new Map();
1762
- await Promise.all(jobs.flatMap((job) => {
1763
- const urls = [];
1764
- for (const raw of job.urls) {
1765
- let key;
1766
- try {
1767
- key = normalizeURL(raw);
1768
- } catch {
1769
- key = raw;
1770
- }
1771
- if (!urls.includes(key)) urls.push(key);
1772
- }
1773
- return urls.map(async (url) => {
1774
- let batch;
1775
- let relayUrl;
1776
- try {
1777
- const relay = await pool.ensureRelay(url, {
1778
- signal: opts.signal,
1779
- timeoutMs: opts.connectTimeoutMs
1780
- });
1781
- relayUrl = relay.url;
1782
- batch = (await relay.fetch([...job.filters], {
1783
- timeoutMs: opts.timeoutMs,
1784
- signal: opts.signal
1785
- })).events;
1786
- } catch {
1787
- if (opts.signal?.aborted === true) throw abortReason(opts.signal);
1788
- return;
1789
- }
1790
- for (const event of batch) if (!byId.has(event.id)) byId.set(event.id, event);
1791
- for (const event of batch) invokeSafely(() => opts.onevent?.(event, relayUrl));
1792
- });
1793
- }));
1794
- return [...byId.values()];
1795
- }
1796
- //#endregion
1797
- //#region src/relay/pool.ts
1798
- function isIdle(relay) {
1799
- return relay.status !== RelayStatus.Connecting && relay.subscriptionCount === 0 && relay.inFlightCount === 0;
1800
- }
1801
- /** Multi-relay coordinator: connection reuse, cross-relay event dedup, fan-out publish. */
1802
- var Pool = class {
1803
- #relays = /* @__PURE__ */ new Map();
1804
- #opts;
1805
- #lastActivity = /* @__PURE__ */ new Map();
1806
- #idleTimeoutMs;
1807
- #idleTimer;
1808
- #allowInsecure;
1809
- #trustedInsecure;
1810
- #pinned;
1811
- /** Per-relay timestamps of verification failures inside the sliding window. */
1812
- #invalidEvents = /* @__PURE__ */ new Map();
1813
- /** Normalized URL → epoch ms when its suspension lifts. */
1814
- #suspendedUntil = /* @__PURE__ */ new Map();
1815
- #resumeTimers = /* @__PURE__ */ new Map();
1816
- constructor(opts = {}) {
1817
- this.#opts = opts;
1818
- this.#allowInsecure = opts.allowInsecure ?? false;
1819
- this.#trustedInsecure = new Set((opts.trustedInsecureUrls ?? []).map(normalizeURL));
1820
- this.#pinned = new Set((opts.pinnedUrls ?? []).map(normalizeURL));
1821
- this.#idleTimeoutMs = opts.idleTimeoutMs ?? 0;
1822
- if (this.#idleTimeoutMs > 0) this.#idleTimer = setInterval(() => this.cleanIdleRelays(), opts.idleCleanupIntervalMs ?? 3e4);
1823
- }
1824
- setAllowInsecure(allow) {
1825
- this.#allowInsecure = allow;
1826
- }
1827
- setTrustedInsecureUrls(urls) {
1828
- this.#trustedInsecure = new Set(urls.map(normalizeURL));
1829
- }
1830
- setPinnedUrls(urls) {
1831
- this.#pinned = new Set(urls.map(normalizeURL));
1832
- }
1833
- /**
1834
- * Drop cached AUTH rejections and re-fire pending challenges on every pooled relay, so a later
1835
- * `setSigner` takes effect without reconnect.
1836
- */
1837
- resetAuth() {
1838
- for (const relay of this.#relays.values()) relay.resetAuth();
1839
- }
1840
- cleanIdleRelays() {
1841
- if (this.#idleTimeoutMs <= 0) return;
1842
- const now = Date.now();
1843
- const idle = [];
1844
- for (const [url, relay] of this.#relays) {
1845
- if (this.#pinned.has(url) || !isIdle(relay)) continue;
1846
- const last = this.#lastActivity.get(url) ?? 0;
1847
- if (!relay.connected || now - last >= this.#idleTimeoutMs) idle.push(url);
1848
- }
1849
- for (const url of this.#lastActivity.keys()) if (!this.#relays.has(url)) this.#lastActivity.delete(url);
1850
- if (idle.length === 0) return;
1851
- this.close(idle);
1852
- invokeSafely(() => this.#opts.onIdleRelaysClosed?.(idle));
1853
- }
1854
- #touch(url) {
1855
- this.#lastActivity.set(url, Date.now());
1856
- }
1857
- /**
1858
- * Count one verification failure for `norm`; on the `limit + 1`-th inside `windowMs` suspend the
1859
- * relay: drop its connection (subscriptions kept), record `until`, and fire `onRelaySuspended`.
1860
- */
1861
- #noteInvalidEvent(norm) {
1862
- const policy = this.#opts.invalidEventPolicy;
1863
- if (policy === void 0) return;
1864
- const now = Date.now();
1865
- const window = (this.#invalidEvents.get(norm) ?? []).filter((at) => now - at < policy.windowMs);
1866
- window.push(now);
1867
- if (window.length <= policy.limit) {
1868
- this.#invalidEvents.set(norm, window);
1869
- return;
1870
- }
1871
- this.#invalidEvents.delete(norm);
1872
- if (this.#suspendedUntil.has(norm)) return;
1873
- const until = now + policy.cooldownMs;
1874
- this.#suspendedUntil.set(norm, until);
1875
- this.#relays.get(norm)?.disconnect();
1876
- const resume = setTimeout(() => {
1877
- this.#resumeTimers.delete(norm);
1878
- this.#suspendedUntil.delete(norm);
1879
- const relay = this.#relays.get(norm);
1880
- if (relay === void 0 || relay.connected || relay.subscriptionCount === 0) return;
1881
- (async () => {
1882
- try {
1883
- await relay.connect();
1884
- } catch {}
1885
- })();
1886
- }, policy.cooldownMs);
1887
- this.#resumeTimers.set(norm, resume);
1888
- invokeSafely(() => this.#opts.onRelaySuspended?.(norm, until));
1889
- }
1890
- #rejectInsecure(url, norm) {
1891
- if (this.#allowInsecure) return;
1892
- if (!isInsecureRelayUrl(url)) return;
1893
- if (this.#trustedInsecure.has(norm)) return;
1894
- throw new RelayConnectionError("insecure relay connection blocked", norm);
1895
- }
1896
- #stopIdleCleanup() {
1897
- if (this.#idleTimer === void 0) return;
1898
- clearInterval(this.#idleTimer);
1899
- this.#idleTimer = void 0;
1900
- }
1901
- /**
1902
- * Soft cap: closing the least-recently-used idle non-pinned relay when a new non-pinned relay
1903
- * would push the count past `maxRelays`. Busy relays are never evicted; with none idle the cap is
1904
- * exceeded rather than failing.
1905
- */
1906
- #enforceMaxRelays(incoming) {
1907
- const cap = this.#opts.maxRelays;
1908
- if (cap === void 0 || this.#pinned.has(incoming)) return;
1909
- let count = 0;
1910
- let oldestUrl;
1911
- let oldestAt = Number.POSITIVE_INFINITY;
1912
- for (const [url, relay] of this.#relays) {
1913
- if (this.#pinned.has(url)) continue;
1914
- count += 1;
1915
- if (!isIdle(relay)) continue;
1916
- const at = this.#lastActivity.get(url) ?? 0;
1917
- if (at < oldestAt) {
1918
- oldestAt = at;
1919
- oldestUrl = url;
1920
- }
1921
- }
1922
- if (count >= cap && oldestUrl !== void 0) this.close([oldestUrl]);
1923
- }
1924
- async ensureRelay(url, opts) {
1925
- const norm = normalizeURL(url);
1926
- this.#rejectInsecure(url, norm);
1927
- const suspendedUntil = this.#suspendedUntil.get(norm);
1928
- if (suspendedUntil !== void 0) {
1929
- if (Date.now() < suspendedUntil) throw new RelaySuspendedError(norm, suspendedUntil);
1930
- this.#suspendedUntil.delete(norm);
1931
- }
1932
- let relay = this.#relays.get(norm);
1933
- if (!relay) {
1934
- this.#enforceMaxRelays(norm);
1935
- const signFn = this.#opts.automaticallyAuth?.(norm) ?? void 0;
1936
- const created = new Relay(norm, {
1937
- websocketImplementation: this.#opts.websocketImplementation,
1938
- verifyEvent: this.#opts.verifyEvent,
1939
- publishTimeoutMs: this.#opts.publishTimeoutMs,
1940
- connectTimeoutMs: this.#opts.connectTimeoutMs,
1941
- enableReconnect: this.#opts.enableReconnect,
1942
- reconnectBackoffMs: this.#opts.reconnectBackoffMs,
1943
- enablePing: this.#opts.enablePing,
1944
- pingIntervalMs: this.#opts.pingIntervalMs,
1945
- pingTimeoutMs: this.#opts.pingTimeoutMs,
1946
- authSigner: signFn
1947
- });
1948
- created.onclose = () => {
1949
- this.#relays.delete(norm);
1950
- this.#lastActivity.delete(norm);
1951
- };
1952
- created.oninvalidevent = () => {
1953
- this.#noteInvalidEvent(norm);
1954
- };
1955
- if (signFn) created.onauth = () => {
1956
- (async () => {
1957
- try {
1958
- await created.auth(signFn);
1959
- } catch {}
1960
- })();
1961
- };
1962
- this.#relays.set(norm, created);
1963
- relay = created;
1964
- }
1965
- this.#touch(norm);
1966
- if (!relay.connected) try {
1967
- await relay.connect({
1968
- signal: opts?.signal,
1969
- timeoutMs: opts?.timeoutMs ?? this.#opts.connectTimeoutMs
1970
- });
1971
- } catch (error) {
1972
- if (this.#opts.enableReconnect !== true) {
1973
- this.#relays.delete(norm);
1974
- this.#lastActivity.delete(norm);
1975
- }
1976
- throw error;
1977
- }
1978
- return relay;
1979
- }
1980
- close(urls) {
1981
- if (!urls) {
1982
- this.#stopIdleCleanup();
1983
- for (const timer of this.#resumeTimers.values()) clearTimeout(timer);
1984
- this.#resumeTimers.clear();
1985
- this.#suspendedUntil.clear();
1986
- this.#invalidEvents.clear();
1987
- for (const relay of this.#relays.values()) relay.close();
1988
- this.#relays.clear();
1989
- this.#lastActivity.clear();
1990
- return;
1991
- }
1992
- for (const url of urls) {
1993
- const norm = normalizeURL(url);
1994
- const timer = this.#resumeTimers.get(norm);
1995
- if (timer !== void 0) {
1996
- clearTimeout(timer);
1997
- this.#resumeTimers.delete(norm);
1998
- }
1999
- this.#relays.get(norm)?.close();
2000
- this.#relays.delete(norm);
2001
- this.#lastActivity.delete(norm);
2002
- }
2003
- }
2004
- /**
2005
- * Subscribe across relays. Deduplicates by event id. Returns a closer; callbacks receive every
2006
- * new event once.
2007
- */
2008
- subscribe(relays, filters, opts = {}) {
2009
- if (filters.length === 0) throw new MessageError("REQ requires at least one filter");
2010
- if (opts.id !== void 0) assertSubscriptionId(opts.id);
2011
- const canonical = canonicalizeFilters(filters);
2012
- return fanIn(this, [{
2013
- urls: relays,
2014
- filters: canonical,
2015
- id: opts.id
2016
- }], {
2017
- onevent: opts.onevent,
2018
- oneose: opts.oneose,
2019
- onclose: opts.onclose,
2020
- signal: opts.signal,
2021
- eoseTimeoutMs: opts.eoseTimeoutMs,
2022
- alreadyHaveEvent: opts.alreadyHaveEvent,
2023
- receivedEvent: opts.receivedEvent,
2024
- closeOnEose: opts.closeOnEose,
2025
- connectTimeoutMs: this.#opts.connectTimeoutMs
2026
- });
2027
- }
2028
- /**
2029
- * One-shot fetch per relay: each entry reports its own events and end reason (`eose`, `closed`,
2030
- * `timeout`), or `failed` when the connect or REQ itself errored. An abort rejects the whole
2031
- * call. Events are not deduped across relays — use {@link fetch} for that.
2032
- */
2033
- async fetchEach(relays, filters, opts) {
2034
- if (filters.length === 0) throw new MessageError("REQ requires at least one filter");
2035
- throwIfAborted(opts?.signal);
2036
- const canonical = canonicalizeFilters(filters);
2037
- return Promise.all(relays.map(async (url) => {
2038
- try {
2039
- const relay = await this.ensureRelay(url, {
2040
- signal: opts?.signal,
2041
- timeoutMs: this.#opts.connectTimeoutMs
2042
- });
2043
- this.#touch(relay.url);
2044
- const result = await relay.fetch([...canonical], {
2045
- timeoutMs: opts?.timeoutMs,
2046
- signal: opts?.signal
2047
- });
2048
- return {
2049
- url: relay.url,
2050
- events: result.events,
2051
- end: result.end
2052
- };
2053
- } catch (error) {
2054
- if (opts?.signal?.aborted === true) throw abortReason(opts.signal);
2055
- return {
2056
- url,
2057
- events: [],
2058
- end: {
2059
- type: "failed",
2060
- reason: error instanceof Error ? error.message : String(error)
2061
- }
2062
- };
2063
- }
2064
- }));
2065
- }
2066
- /** Fetch events until each connected relay EOSE or timeout; dedupe by id. */
2067
- async fetch(relays, filters, opts) {
2068
- if (filters.length === 0) throw new MessageError("REQ requires at least one filter");
2069
- return fetchRouted(this, [{
2070
- urls: relays,
2071
- filters: canonicalizeFilters(filters)
2072
- }], {
2073
- timeoutMs: opts?.timeoutMs,
2074
- signal: opts?.signal,
2075
- connectTimeoutMs: this.#opts.connectTimeoutMs,
2076
- onevent: opts?.onevent
2077
- });
2078
- }
2079
- /** Publish to all listed relays; returns per-relay outcomes. */
2080
- async publish(relays, event, opts) {
2081
- return await Promise.all(relays.map(async (url) => {
2082
- try {
2083
- const relay = await this.ensureRelay(url, { timeoutMs: this.#opts.connectTimeoutMs });
2084
- this.#touch(relay.url);
2085
- const result = await relay.publish(event, { timeoutMs: opts?.timeoutMs });
2086
- return {
2087
- url: relay.url,
2088
- result
2089
- };
2090
- } catch (error) {
2091
- return {
2092
- url,
2093
- error: error instanceof Error ? error.message : String(error)
2094
- };
2095
- }
2096
- }));
2097
- }
2098
- /** First successful publish (Promise.any semantics). */
2099
- async publishAny(relays, event, opts) {
2100
- return Promise.any(relays.map(async (url) => {
2101
- const relay = await this.ensureRelay(url, { timeoutMs: this.#opts.connectTimeoutMs });
2102
- this.#touch(relay.url);
2103
- const result = await relay.publish(event, { timeoutMs: opts?.timeoutMs });
2104
- if (!result.ok) throw new RelayPublishError(result.message || "rejected", relay.url);
2105
- return {
2106
- url: relay.url,
2107
- result
2108
- };
2109
- }));
2110
- }
2111
- /**
2112
- * NIP-45 COUNT across relays. Per-relay outcomes; failures do not throw. Counts are not summed —
2113
- * each relay reports independently (may overlap).
2114
- */
2115
- async count(relays, filters, opts) {
2116
- throwIfAborted(opts?.signal);
2117
- const canonical = canonicalizeFilters(filters);
2118
- return await Promise.all(relays.map(async (url) => {
2119
- try {
2120
- const relay = await this.ensureRelay(url, {
2121
- signal: opts?.signal,
2122
- timeoutMs: this.#opts.connectTimeoutMs
2123
- });
2124
- this.#touch(relay.url);
2125
- const payload = await relay.count(canonical, {
2126
- timeoutMs: opts?.timeoutMs,
2127
- signal: opts?.signal
2128
- });
2129
- const out = {
2130
- url: relay.url,
2131
- count: payload.count
2132
- };
2133
- if (payload.approximate !== void 0) out.approximate = payload.approximate;
2134
- if (payload.hll !== void 0) out.hll = payload.hll;
2135
- return out;
2136
- } catch (error) {
2137
- if (opts?.signal?.aborted === true) throw abortReason(opts.signal);
2138
- return {
2139
- url,
2140
- error: error instanceof Error ? error.message : String(error)
2141
- };
2142
- }
2143
- }));
2144
- }
2145
- listRelays() {
2146
- return [...this.#relays.keys()];
2147
- }
2148
- /** Lookup a pooled relay, including disconnected reconnecting entries. */
2149
- getRelay(url) {
2150
- try {
2151
- return this.#relays.get(normalizeURL(url));
2152
- } catch {
2153
- return this.#relays.get(url);
2154
- }
2155
- }
2156
- /** Currently connected URLs. Unlike listRelays(), excludes reconnecting/disconnected entries. */
2157
- connectedUrls() {
2158
- const urls = [];
2159
- for (const [url, relay] of this.#relays) if (relay.connected) urls.push(url);
2160
- return urls;
2161
- }
2162
- };
2163
- //#endregion
2164
- 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 };
1621
+ export { getWebSocketImplementation as a, RelayConnectionError as c, RelaySuspendedError as d, RelayTimeoutError as f, subscriptionToAsyncIterable as i, RelayError as l, Relay as n, useWebSocketImplementation as o, RelayStatus as r, RelayClosedError as s, DEFAULT_REQUEST_TIMEOUT_MS as t, RelayPublishError as u };
2165
1622
 
2166
- //# sourceMappingURL=pool-As20OGxB.mjs.map
1623
+ //# sourceMappingURL=relay-CvxPiQ_m.mjs.map