@qntx/nostr 0.7.0 → 0.9.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 (171) hide show
  1. package/README.md +1 -1
  2. package/dist/{builder-CdLE4J6O.mjs → builder-CVRmUeNm.mjs} +5 -5
  3. package/dist/builder-CVRmUeNm.mjs.map +1 -0
  4. package/dist/{builder-DgkqDylK.d.mts → builder-GBddxj8i.d.mts} +12 -12
  5. package/dist/{client-CILhdfsF.mjs → client-CeyDLoyO.mjs} +110 -90
  6. package/dist/client-CeyDLoyO.mjs.map +1 -0
  7. package/dist/client.d.mts +1 -1
  8. package/dist/client.mjs +1 -1
  9. package/dist/core.d.mts +8 -8
  10. package/dist/core.mjs +9 -9
  11. package/dist/emitter-DkCY6NkU.mjs +32 -0
  12. package/dist/emitter-DkCY6NkU.mjs.map +1 -0
  13. package/dist/{error-L_QdmSnV.mjs → error-BILMe3Gr.mjs} +2 -2
  14. package/dist/{error-L_QdmSnV.mjs.map → error-BILMe3Gr.mjs.map} +1 -1
  15. package/dist/{error-gtJvV6YS.mjs → error-DlhGW4mJ.mjs} +2 -2
  16. package/dist/{error-gtJvV6YS.mjs.map → error-DlhGW4mJ.mjs.map} +1 -1
  17. package/dist/{event-DinQMmJ1.mjs → event-C2eLiPpV.mjs} +16 -6
  18. package/dist/event-C2eLiPpV.mjs.map +1 -0
  19. package/dist/{event-DJeb_DIL.d.mts → event-j3BwI5dF.d.mts} +2 -4
  20. package/dist/{filter-jI_9htF7.d.mts → filter-CfEgQBXt.d.mts} +3 -5
  21. package/dist/{filter-BQl3GlHt.mjs → filter-TrF2LEoy.mjs} +3 -36
  22. package/dist/filter-TrF2LEoy.mjs.map +1 -0
  23. package/dist/{gossip-CcTp9VpU.mjs → gossip-5ezg0Quo.mjs} +4 -4
  24. package/dist/gossip-5ezg0Quo.mjs.map +1 -0
  25. package/dist/{gossip-C7HLW5kI.d.mts → gossip-BWb9HmfK.d.mts} +10 -7
  26. package/dist/gossip.d.mts +1 -1
  27. package/dist/gossip.mjs +1 -1
  28. package/dist/http-CuazNRIP.mjs.map +1 -1
  29. package/dist/{http-C4HwIu4B.d.mts → http-kwWG-V1X.d.mts} +9 -3
  30. package/dist/{index-Tx15HGAD.d.mts → index-C0mFpelb.d.mts} +9 -9
  31. package/dist/{index-DocgD9K0.d.mts → index-C1m5cHpd.d.mts} +42 -28
  32. package/dist/{index-InQZpHyQ.d.mts → index-CoX7SnO7.d.mts} +7 -4
  33. package/dist/{index-iLT8UzY3.d.mts → index-D0M3kjY2.d.mts} +23 -23
  34. package/dist/{index-DU9tRFTZ.d.mts → index-Hp26bgYG.d.mts} +11 -4
  35. package/dist/{index-DiXwNn2M.d.mts → index-JD1O7m2v.d.mts} +4 -4
  36. package/dist/index.d.mts +20 -20
  37. package/dist/index.mjs +20 -20
  38. package/dist/{instance-BcKzxHIP.mjs → instance-CI_IaZpf.mjs} +3 -3
  39. package/dist/{instance-BcKzxHIP.mjs.map → instance-CI_IaZpf.mjs.map} +1 -1
  40. package/dist/{instance-DjsL8h84.d.mts → instance-X_WkCnpR.d.mts} +3 -3
  41. package/dist/{key-D_5OgGT7.d.mts → key-CyKN1RTi.d.mts} +2 -2
  42. package/dist/{key-bshqfmZL.mjs → key-DY_HEIpY.mjs} +5 -4
  43. package/dist/key-DY_HEIpY.mjs.map +1 -0
  44. package/dist/{kind-Cxwhhkx8.mjs → kind-D8fENiiL.mjs} +11 -5
  45. package/dist/kind-D8fENiiL.mjs.map +1 -0
  46. package/dist/{loaders-DD0XAGzw.mjs → loaders-DifimjJ7.mjs} +11 -10
  47. package/dist/loaders-DifimjJ7.mjs.map +1 -0
  48. package/dist/loaders.d.mts +2 -2
  49. package/dist/loaders.mjs +1 -1
  50. package/dist/{memory-D_-5-T31.mjs → memory-DENGQ_f6.mjs} +2 -2
  51. package/dist/memory-DENGQ_f6.mjs.map +1 -0
  52. package/dist/{memory-index-C6rujtoc.mjs → memory-index-D_lvlYAy.mjs} +4 -4
  53. package/dist/{memory-index-C6rujtoc.mjs.map → memory-index-D_lvlYAy.mjs.map} +1 -1
  54. package/dist/{message-CbBZOhq3.d.mts → message-CcUz22qh.d.mts} +6 -6
  55. package/dist/{message-CWLcFTQ1.mjs → message-Rd5Z_17h.mjs} +6 -5
  56. package/dist/message-Rd5Z_17h.mjs.map +1 -0
  57. package/dist/{nip17-cTDFfFVt.d.mts → nip17-B7MI82Iq.d.mts} +5 -5
  58. package/dist/{nip19-D1OnJ192.d.mts → nip19-DpPN17bY.d.mts} +23 -23
  59. package/dist/{nip46-DrsPI8EX.d.mts → nip46-BHaxC9TZ.d.mts} +17 -17
  60. package/dist/{nip51-DoPUvQ--.d.mts → nip51-O_vhOiK2.d.mts} +11 -11
  61. package/dist/{nip59-BFIihLYY.d.mts → nip59-CdDoJptA.d.mts} +10 -3
  62. package/dist/{nip65-52HiwiyD.d.mts → nip65-BL79HI8N.d.mts} +3 -3
  63. package/dist/{nip77-C9QcZn7b.d.mts → nip77-wHA_nEV5.d.mts} +7 -7
  64. package/dist/nips/blossom.d.mts +8 -8
  65. package/dist/nips/blossom.mjs +3 -3
  66. package/dist/nips/blossom.mjs.map +1 -1
  67. package/dist/nips/nip04.mjs +1 -1
  68. package/dist/nips/nip05.d.mts +9 -9
  69. package/dist/nips/nip05.mjs +1 -1
  70. package/dist/nips/nip05.mjs.map +1 -1
  71. package/dist/nips/nip10.d.mts +8 -8
  72. package/dist/nips/nip10.mjs +35 -33
  73. package/dist/nips/nip10.mjs.map +1 -1
  74. package/dist/nips/nip11.d.mts +28 -28
  75. package/dist/nips/nip11.mjs +1 -1
  76. package/dist/nips/nip11.mjs.map +1 -1
  77. package/dist/nips/nip13.d.mts +1 -1
  78. package/dist/nips/nip13.mjs +1 -1
  79. package/dist/nips/nip17.d.mts +1 -1
  80. package/dist/nips/nip17.mjs +7 -5
  81. package/dist/nips/nip17.mjs.map +1 -1
  82. package/dist/nips/nip19.d.mts +1 -1
  83. package/dist/nips/nip19.mjs +1 -1
  84. package/dist/nips/nip19.mjs.map +1 -1
  85. package/dist/nips/nip21.d.mts +4 -4
  86. package/dist/nips/nip21.mjs +1 -1
  87. package/dist/nips/nip21.mjs.map +1 -1
  88. package/dist/nips/nip27.d.mts +20 -20
  89. package/dist/nips/nip27.mjs +2 -2
  90. package/dist/nips/nip27.mjs.map +1 -1
  91. package/dist/nips/nip42.d.mts +1 -1
  92. package/dist/nips/nip42.mjs +2 -2
  93. package/dist/nips/nip44.mjs +2 -2
  94. package/dist/nips/nip44.mjs.map +1 -1
  95. package/dist/nips/nip46.d.mts +1 -1
  96. package/dist/nips/nip46.mjs +1 -1
  97. package/dist/nips/nip46.mjs.map +1 -1
  98. package/dist/nips/nip49.d.mts +4 -4
  99. package/dist/nips/nip49.mjs +1 -1
  100. package/dist/nips/nip49.mjs.map +1 -1
  101. package/dist/nips/nip51.d.mts +1 -1
  102. package/dist/nips/nip51.mjs +4 -4
  103. package/dist/nips/nip51.mjs.map +1 -1
  104. package/dist/nips/nip57.d.mts +19 -19
  105. package/dist/nips/nip57.mjs +4 -4
  106. package/dist/nips/nip57.mjs.map +1 -1
  107. package/dist/nips/nip59.d.mts +1 -1
  108. package/dist/nips/nip59.mjs +16 -8
  109. package/dist/nips/nip59.mjs.map +1 -1
  110. package/dist/nips/nip65.d.mts +1 -1
  111. package/dist/nips/nip65.mjs +3 -3
  112. package/dist/nips/nip77.d.mts +1 -1
  113. package/dist/nips/nip77.mjs +9 -6
  114. package/dist/nips/nip77.mjs.map +1 -1
  115. package/dist/nips/nip98.d.mts +1 -1
  116. package/dist/nips/nip98.mjs +4 -4
  117. package/dist/{pool-SzJKPIa3.d.mts → pool-BDI_u03Q.d.mts} +48 -28
  118. package/dist/{pool-YHRZgcQ3.mjs → pool-jok2-iBJ.mjs} +64 -47
  119. package/dist/pool-jok2-iBJ.mjs.map +1 -0
  120. package/dist/{put-DS0yzVJx.mjs → put-JsiCKMyZ.mjs} +5 -5
  121. package/dist/put-JsiCKMyZ.mjs.map +1 -0
  122. package/dist/{reactive-BJRoVHxU.mjs → reactive-6Wn9qf_d.mjs} +51 -29
  123. package/dist/reactive-6Wn9qf_d.mjs.map +1 -0
  124. package/dist/{reactive-BJ1ggn4d.d.mts → reactive-X4p6nABy.d.mts} +18 -9
  125. package/dist/{relay-CvxPiQ_m.mjs → relay-__QtO33h.mjs} +1059 -863
  126. package/dist/relay-__QtO33h.mjs.map +1 -0
  127. package/dist/relay.d.mts +4 -4
  128. package/dist/relay.mjs +2 -2
  129. package/dist/{signer-yW3slDC2.mjs → signer-D7PtVvQn.mjs} +16 -9
  130. package/dist/signer-D7PtVvQn.mjs.map +1 -0
  131. package/dist/signer.d.mts +2 -2
  132. package/dist/signer.mjs +2 -2
  133. package/dist/{storage-CZ52ifcQ.mjs → storage-oihjE0_x.mjs} +23 -21
  134. package/dist/storage-oihjE0_x.mjs.map +1 -0
  135. package/dist/storage.d.mts +2 -2
  136. package/dist/storage.mjs +4 -4
  137. package/dist/store.d.mts +2 -2
  138. package/dist/store.mjs +1 -1
  139. package/dist/testing.d.mts +4 -4
  140. package/dist/testing.mjs +8 -8
  141. package/dist/testing.mjs.map +1 -1
  142. package/dist/{types-DNmiscfd.d.mts → types-BJ4wZ7JJ.d.mts} +2 -2
  143. package/dist/{types-Cm-XMgtH.d.mts → types-Ccox7-5v.d.mts} +7 -7
  144. package/dist/{util-yFYzlbrm.mjs → util-DMF9AIO0.mjs} +6 -2
  145. package/dist/util-DMF9AIO0.mjs.map +1 -0
  146. package/dist/{verifier-CgTFpgsC.d.mts → verifier-BjL2M0ka.d.mts} +2 -2
  147. package/dist/{verifier-BTLOpo1n.mjs → verifier-uC_jd-Nw.mjs} +4 -4
  148. package/dist/verifier-uC_jd-Nw.mjs.map +1 -0
  149. package/dist/wasm.browser.d.mts +1 -1
  150. package/dist/wasm.browser.mjs +2 -2
  151. package/dist/wasm.d.mts +1 -1
  152. package/dist/wasm.mjs +2 -2
  153. package/package.json +5 -5
  154. package/dist/builder-CdLE4J6O.mjs.map +0 -1
  155. package/dist/client-CILhdfsF.mjs.map +0 -1
  156. package/dist/event-DinQMmJ1.mjs.map +0 -1
  157. package/dist/filter-BQl3GlHt.mjs.map +0 -1
  158. package/dist/gossip-CcTp9VpU.mjs.map +0 -1
  159. package/dist/key-bshqfmZL.mjs.map +0 -1
  160. package/dist/kind-Cxwhhkx8.mjs.map +0 -1
  161. package/dist/loaders-DD0XAGzw.mjs.map +0 -1
  162. package/dist/memory-D_-5-T31.mjs.map +0 -1
  163. package/dist/message-CWLcFTQ1.mjs.map +0 -1
  164. package/dist/pool-YHRZgcQ3.mjs.map +0 -1
  165. package/dist/put-DS0yzVJx.mjs.map +0 -1
  166. package/dist/reactive-BJRoVHxU.mjs.map +0 -1
  167. package/dist/relay-CvxPiQ_m.mjs.map +0 -1
  168. package/dist/signer-yW3slDC2.mjs.map +0 -1
  169. package/dist/storage-CZ52ifcQ.mjs.map +0 -1
  170. package/dist/util-yFYzlbrm.mjs.map +0 -1
  171. package/dist/verifier-BTLOpo1n.mjs.map +0 -1
@@ -1,11 +1,12 @@
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
- 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-CWLcFTQ1.mjs";
5
- import { t as NoSignerError } from "./error-gtJvV6YS.mjs";
1
+ import { _ as MessageError, b as WasmPoisonedError, d as nowSeconds, u as normalizeURL, v as NostrError } from "./util-DMF9AIO0.mjs";
2
+ import { n as verifyEvent } from "./verifier-uC_jd-Nw.mjs";
3
+ import { n as canonicalizeFilters, o as matchFilters, r as filterFingerprint, t as canonicalizeFilter } from "./filter-TrF2LEoy.mjs";
4
+ import { n as createSubscriptionId, r as encodeClientMessage, s as parseRelayMessage, t as assertSubscriptionId } from "./message-Rd5Z_17h.mjs";
5
+ import { t as NoSignerError } from "./error-DlhGW4mJ.mjs";
6
+ import { i as throwIfAborted, n as onAbort, r as raceSignal, t as abortReason } from "./abort-DBq2STPj.mjs";
6
7
  import { t as invokeSafely } from "./report-CHld4uWj.mjs";
7
8
  import { isAuthRequired, makeAuthEvent } from "./nips/nip42.mjs";
8
- import { i as throwIfAborted, n as onAbort, r as raceSignal, t as abortReason } from "./abort-DBq2STPj.mjs";
9
+ import { t as Emitter } from "./emitter-DkCY6NkU.mjs";
9
10
  import { Nip77Error, runNegSession } from "./nips/nip77.mjs";
10
11
  //#region src/relay/error.ts
11
12
  /** Base class for relay errors; carries the relay URL when known. */
@@ -62,6 +63,519 @@ function getWebSocketImplementation() {
62
63
  return impl;
63
64
  }
64
65
  //#endregion
66
+ //#region src/relay/auth.ts
67
+ var RelayAuth = class {
68
+ #challenge;
69
+ #authedChallenge;
70
+ /** Challenge value already answered on this connection; duplicates are not re-signed. */
71
+ #answeredChallenge;
72
+ /** Settled OK verdict for `#answeredChallenge`; set only once the relay replies. */
73
+ #answeredResult;
74
+ #authPromise;
75
+ #url;
76
+ #isOpen;
77
+ #sendAuth;
78
+ #defaultTimeoutMs;
79
+ constructor(deps) {
80
+ this.#url = deps.url;
81
+ this.#isOpen = deps.isOpen;
82
+ this.#sendAuth = deps.sendAuth;
83
+ this.#defaultTimeoutMs = deps.defaultTimeoutMs;
84
+ }
85
+ /** Latest NIP-42 challenge, if any. */
86
+ get challenge() {
87
+ return this.#challenge;
88
+ }
89
+ /**
90
+ * A re-sent identical challenge keeps the in-flight/settled dedupe; only a new challenge value
91
+ * resets the answer state.
92
+ */
93
+ handleChallenge(challenge) {
94
+ if (challenge !== this.#challenge) {
95
+ this.#authPromise = void 0;
96
+ this.#authedChallenge = void 0;
97
+ this.#answeredChallenge = void 0;
98
+ this.#answeredResult = void 0;
99
+ }
100
+ this.#challenge = challenge;
101
+ }
102
+ /** New connection: challenge and answer state belong to the old socket. */
103
+ resetConnection() {
104
+ this.#challenge = void 0;
105
+ this.#authPromise = void 0;
106
+ this.#authedChallenge = void 0;
107
+ this.#answeredChallenge = void 0;
108
+ this.#answeredResult = void 0;
109
+ }
110
+ /** Sign the current challenge and wait for OK. */
111
+ async auth(sign, opts) {
112
+ const challenge = this.#challenge;
113
+ if (challenge === void 0 || challenge === "") throw new RelayError("no AUTH challenge received from relay", this.#url);
114
+ if (this.#authPromise) return this.#authPromise;
115
+ if (this.#answeredChallenge === challenge && this.#answeredResult !== void 0) return this.#answeredResult;
116
+ const pending = (async () => {
117
+ const template = makeAuthEvent(this.#url, challenge);
118
+ let event;
119
+ try {
120
+ event = await sign(template);
121
+ } catch (error) {
122
+ if (error instanceof NoSignerError) return {
123
+ ok: false,
124
+ message: "auth: no signer"
125
+ };
126
+ throw error;
127
+ }
128
+ if (!this.#isOpen()) throw new RelayClosedError("not connected", this.#url);
129
+ const result = await this.#sendAuth(event, opts?.timeoutMs ?? this.#defaultTimeoutMs);
130
+ if (this.#challenge === challenge) {
131
+ this.#answeredChallenge = challenge;
132
+ this.#answeredResult = result;
133
+ }
134
+ return result;
135
+ })();
136
+ this.#authPromise = pending;
137
+ try {
138
+ const result = await pending;
139
+ if (result.ok && this.#challenge === challenge) this.#authedChallenge = challenge;
140
+ return result;
141
+ } finally {
142
+ if (this.#authPromise === pending) this.#authPromise = void 0;
143
+ }
144
+ }
145
+ async ensureAuthed(signer) {
146
+ if (!signer) return false;
147
+ for (let i = 0; i < 3; i++) {
148
+ if (this.#challenge === void 0 || this.#challenge === "") return false;
149
+ if (this.#authedChallenge === this.#challenge) return true;
150
+ const signed = this.#challenge;
151
+ const result = await this.auth(signer);
152
+ if (this.#authedChallenge === this.#challenge) return true;
153
+ if (!result.ok) {
154
+ if (this.#challenge === void 0 || this.#challenge === "" || this.#challenge === signed) return false;
155
+ continue;
156
+ }
157
+ }
158
+ return this.#authedChallenge === this.#challenge;
159
+ }
160
+ /**
161
+ * Clear a cached AUTH rejection for the current challenge (a successful auth and in-flight
162
+ * answers are kept) and return the unanswered challenge to re-emit, if any.
163
+ */
164
+ resetRejection() {
165
+ if (this.#answeredResult !== void 0 && !this.#answeredResult.ok) {
166
+ this.#answeredChallenge = void 0;
167
+ this.#answeredResult = void 0;
168
+ }
169
+ if (this.#challenge !== void 0 && this.#challenge !== this.#authedChallenge) return this.#challenge;
170
+ }
171
+ };
172
+ //#endregion
173
+ //#region src/relay/count-tracker.ts
174
+ /**
175
+ * COUNT request correlation: one waiter per request id with a timeout, abort cleanup and a re-send
176
+ * after AUTH.
177
+ */
178
+ var CountTracker = class {
179
+ #entries = /* @__PURE__ */ new Map();
180
+ get size() {
181
+ return this.#entries.size;
182
+ }
183
+ get(id) {
184
+ return this.#entries.get(id);
185
+ }
186
+ /**
187
+ * Register a waiter, wire the abort listener, arm its timeout and call `send`. A send throw
188
+ * rejects (`sendError` when the throw is not an Error). resolve/reject unwind the timer, abort
189
+ * listener and map entry.
190
+ */
191
+ async track(id, opts) {
192
+ return new Promise((resolve, reject) => {
193
+ const waiter = {
194
+ id,
195
+ resolve: (result) => {
196
+ this.#finish(waiter);
197
+ cleanup();
198
+ resolve(result);
199
+ },
200
+ reject: (err) => {
201
+ this.#finish(waiter);
202
+ cleanup();
203
+ reject(err);
204
+ },
205
+ timer: void 0,
206
+ filters: opts.filters,
207
+ authRetried: false,
208
+ timeoutMs: opts.timeoutMs,
209
+ timeoutError: opts.timeoutError
210
+ };
211
+ const cleanup = onAbort(opts.signal, () => {
212
+ if (opts.signal !== void 0) waiter.reject(abortReason(opts.signal));
213
+ });
214
+ waiter.timer = setTimeout(() => {
215
+ waiter.reject(opts.timeoutError());
216
+ }, opts.timeoutMs);
217
+ this.#entries.set(id, waiter);
218
+ try {
219
+ opts.send();
220
+ } catch (error) {
221
+ waiter.reject(error instanceof Error ? error : opts.sendError());
222
+ }
223
+ });
224
+ }
225
+ /** Stop a waiter's timeout without settling (AUTH retry window). */
226
+ clearTimer(waiter) {
227
+ if (waiter.timer !== void 0) {
228
+ clearTimeout(waiter.timer);
229
+ waiter.timer = void 0;
230
+ }
231
+ }
232
+ /** Re-arm the timeout for a re-sent COUNT (after an AUTH retry). */
233
+ restartTimer(id) {
234
+ const waiter = this.#entries.get(id);
235
+ if (waiter === void 0) return;
236
+ waiter.timer = setTimeout(() => {
237
+ waiter.reject(waiter.timeoutError());
238
+ }, waiter.timeoutMs);
239
+ }
240
+ /**
241
+ * Re-send a COUNT after the relay demanded AUTH: pause the timeout through the AUTH window,
242
+ * re-arm it and re-send on success, reject on failure.
243
+ */
244
+ async authRetry(waiter, deps) {
245
+ this.clearTimer(waiter);
246
+ try {
247
+ const ok = await deps.ensureAuthed();
248
+ if (this.#entries.get(waiter.id) !== waiter) return;
249
+ if (!ok || !deps.isOpen()) {
250
+ waiter.reject(deps.failError());
251
+ return;
252
+ }
253
+ this.restartTimer(waiter.id);
254
+ deps.send();
255
+ } catch (error) {
256
+ if (this.#entries.get(waiter.id) === void 0) return;
257
+ waiter.reject(error instanceof Error ? error : deps.failError());
258
+ }
259
+ }
260
+ rejectAll(err) {
261
+ for (const waiter of this.#entries.values()) {
262
+ this.clearTimer(waiter);
263
+ waiter.reject(err);
264
+ }
265
+ this.#entries.clear();
266
+ }
267
+ #finish(waiter) {
268
+ this.clearTimer(waiter);
269
+ this.#entries.delete(waiter.id);
270
+ }
271
+ };
272
+ //#endregion
273
+ //#region src/relay/subscription.ts
274
+ /**
275
+ * Internal REQ runtime: owns its lifecycle flags, watermark bookkeeping and handler dispatch.
276
+ * `onLocalClose` is supplied by the registry — it sends CLOSE / detaches from a live group — and is
277
+ * invoked by {@link close} only, never by {@link end}.
278
+ */
279
+ var Subscription = class {
280
+ id;
281
+ filters;
282
+ closeOnEose;
283
+ #handlers;
284
+ #onLocalClose;
285
+ #abortDispose;
286
+ #eoseTimer;
287
+ #closed = false;
288
+ #eosed = false;
289
+ /** True after one CLOSED `auth-required:` retry. */
290
+ #authRetried = false;
291
+ /** Inclusive NIP-01 `since` watermark from verified EVENTs. */
292
+ #lastCreatedAt;
293
+ /** Event ids at `lastCreatedAt` (same-second reconnect dedup). Not all seen ids. */
294
+ #idsAtWatermark = /* @__PURE__ */ new Set();
295
+ constructor(filters, opts, onLocalClose) {
296
+ this.#onLocalClose = onLocalClose;
297
+ this.id = createSubscriptionId(opts.id);
298
+ this.filters = filters;
299
+ this.closeOnEose = opts.closeOnEose === true;
300
+ this.#handlers = {
301
+ onevent: opts.onevent,
302
+ oneose: opts.oneose,
303
+ onclose: opts.onclose,
304
+ alreadyHaveEvent: opts.alreadyHaveEvent,
305
+ receivedEvent: opts.receivedEvent
306
+ };
307
+ if (opts.eoseTimeoutMs !== void 0) this.#eoseTimer = setTimeout(() => {
308
+ this.#eoseTimer = void 0;
309
+ this.markEose();
310
+ }, opts.eoseTimeoutMs);
311
+ if (opts.signal) {
312
+ if (opts.signal.aborted) this.close("aborted");
313
+ else this.#abortDispose = onAbort(opts.signal, () => this.close("aborted"));
314
+ }
315
+ }
316
+ get closed() {
317
+ return this.#closed;
318
+ }
319
+ get eosed() {
320
+ return this.#eosed;
321
+ }
322
+ get lastCreatedAt() {
323
+ return this.#lastCreatedAt;
324
+ }
325
+ get idsAtWatermark() {
326
+ return this.#idsAtWatermark;
327
+ }
328
+ /**
329
+ * Local close: idempotent; clears the EOSE timer and abort listener, detaches via `onLocalClose`
330
+ * (sends CLOSE / leaves the live group), then fires `onclose`.
331
+ */
332
+ close(reason = "closed by client") {
333
+ if (this.#closed) return;
334
+ this.#closed = true;
335
+ this.#teardown();
336
+ this.#onLocalClose(this);
337
+ invokeSafely(() => this.#handlers.onclose?.(reason));
338
+ }
339
+ /** Remote/transport termination: like {@link close} but `onLocalClose` is not called. */
340
+ end(reason) {
341
+ if (this.#closed) return;
342
+ this.#closed = true;
343
+ this.#teardown();
344
+ invokeSafely(() => this.#handlers.onclose?.(reason));
345
+ }
346
+ #teardown() {
347
+ if (this.#eoseTimer !== void 0) {
348
+ clearTimeout(this.#eoseTimer);
349
+ this.#eoseTimer = void 0;
350
+ }
351
+ this.#abortDispose?.();
352
+ this.#abortDispose = void 0;
353
+ }
354
+ /** Mark EOSE: fires `oneose`, then closes when `closeOnEose`. No-op when closed or EOSE'd. */
355
+ markEose() {
356
+ if (this.#closed || this.#eosed) return;
357
+ this.#eosed = true;
358
+ if (this.#eoseTimer !== void 0) {
359
+ clearTimeout(this.#eoseTimer);
360
+ this.#eoseTimer = void 0;
361
+ }
362
+ invokeSafely(() => this.#handlers.oneose?.());
363
+ if (this.closeOnEose) this.close("eose");
364
+ }
365
+ /** Clear `eosed` for a replayed REQ (AUTH retry / reconnect). */
366
+ rearm() {
367
+ this.#eosed = false;
368
+ }
369
+ /** First `auth-required:` retry wins; later calls report false without side effects. */
370
+ beginAuthRetry() {
371
+ if (this.#authRetried) return false;
372
+ this.#authRetried = true;
373
+ return true;
374
+ }
375
+ resetAuthRetry() {
376
+ this.#authRetried = false;
377
+ }
378
+ /** `receivedEvent` for every EVENT id, before watermark/alreadyHaveEvent/verify. */
379
+ notifyReceived(id) {
380
+ invokeSafely(() => this.#handlers.receivedEvent?.(id));
381
+ }
382
+ alreadyHas(id) {
383
+ let have = false;
384
+ invokeSafely(() => {
385
+ have = Boolean(this.#handlers.alreadyHaveEvent?.(id));
386
+ });
387
+ return have;
388
+ }
389
+ isAtWatermark(id) {
390
+ return this.#idsAtWatermark.has(id);
391
+ }
392
+ /** Verified event: advance the watermark, then fire `onevent`. */
393
+ deliver(event) {
394
+ this.noteVerified(event);
395
+ invokeSafely(() => this.#handlers.onevent?.(event));
396
+ }
397
+ /**
398
+ * Advance the reconnect watermark after a verified EVENT. A far-future `created_at` (buggy or
399
+ * hostile publishers do exist on the public network) must not push the replayed `since` past wall
400
+ * clock — clamp it to the current wall clock. The worst case is re-delivery of events already
401
+ * seen, never event loss.
402
+ */
403
+ noteVerified(event) {
404
+ const ts = Math.min(event.created_at, nowSeconds());
405
+ if (this.#lastCreatedAt === void 0 || ts > this.#lastCreatedAt) {
406
+ this.#lastCreatedAt = ts;
407
+ this.#idsAtWatermark.clear();
408
+ this.#idsAtWatermark.add(event.id);
409
+ return;
410
+ }
411
+ if (ts === this.#lastCreatedAt) this.#idsAtWatermark.add(event.id);
412
+ }
413
+ /**
414
+ * Filters for re-REQ. Original `filters` stay unchanged. NIP-01 `since` is inclusive — never
415
+ * `lastCreatedAt + 1`.
416
+ */
417
+ replayFilters() {
418
+ const since = this.#lastCreatedAt;
419
+ if (since === void 0) return [...this.filters];
420
+ return this.filters.map((f) => ({
421
+ ...f,
422
+ since: f.since === void 0 ? since : Math.max(f.since, since)
423
+ }));
424
+ }
425
+ };
426
+ /** AsyncIterable wrapper over a subscription's events until EOSE or close. */
427
+ function subscriptionToAsyncIterable(start, opts) {
428
+ const queue = [];
429
+ let head = 0;
430
+ let done = false;
431
+ let error;
432
+ let wake;
433
+ let closer;
434
+ let localClose = false;
435
+ let disposeAbort;
436
+ const notify = () => {
437
+ wake?.();
438
+ wake = void 0;
439
+ };
440
+ const waitForWake = async () => new Promise((resolve) => {
441
+ wake = resolve;
442
+ });
443
+ const closeLocal = (reason) => {
444
+ localClose = true;
445
+ disposeAbort?.();
446
+ closer?.close(reason);
447
+ done = true;
448
+ notify();
449
+ };
450
+ closer = start({
451
+ onevent(event) {
452
+ queue.push(event);
453
+ notify();
454
+ },
455
+ oneose() {
456
+ if (opts?.includeEose === false) closeLocal("eose");
457
+ },
458
+ onclose(reason) {
459
+ if (opts?.signal?.aborted === true) localClose = true;
460
+ if (!localClose && reason) error = new RelayClosedError(reason);
461
+ done = true;
462
+ notify();
463
+ }
464
+ });
465
+ if (opts?.signal) {
466
+ if (opts.signal.aborted) closeLocal("aborted");
467
+ else {
468
+ disposeAbort = onAbort(opts.signal, () => closeLocal("aborted"));
469
+ if (done) disposeAbort();
470
+ }
471
+ }
472
+ return {
473
+ close(reason) {
474
+ closeLocal(reason ?? "closed by client");
475
+ },
476
+ [Symbol.asyncIterator]() {
477
+ return {
478
+ async next() {
479
+ while (true) {
480
+ const value = head < queue.length ? queue[head] : void 0;
481
+ if (value !== void 0) {
482
+ head += 1;
483
+ if (head === queue.length) {
484
+ queue.length = 0;
485
+ head = 0;
486
+ }
487
+ return {
488
+ value,
489
+ done: false
490
+ };
491
+ }
492
+ if (error !== void 0) throw error;
493
+ if (done) return {
494
+ value: void 0,
495
+ done: true
496
+ };
497
+ await waitForWake();
498
+ }
499
+ },
500
+ async return() {
501
+ closeLocal("iterator returned");
502
+ return {
503
+ value: void 0,
504
+ done: true
505
+ };
506
+ }
507
+ };
508
+ }
509
+ };
510
+ }
511
+ //#endregion
512
+ //#region src/relay/fetch.ts
513
+ function streamFilters(subscribe, filters, opts) {
514
+ return subscriptionToAsyncIterable((handlers) => subscribe(filters, {
515
+ ...handlers,
516
+ id: opts?.id,
517
+ signal: opts?.signal
518
+ }), { signal: opts?.signal });
519
+ }
520
+ async function fetchFilters(subscribe, filters, opts) {
521
+ throwIfAborted(opts.signal);
522
+ const events = [];
523
+ const seen = /* @__PURE__ */ new Set();
524
+ let end = { type: "timeout" };
525
+ await new Promise((resolve, reject) => {
526
+ let settled = false;
527
+ const done = (err) => {
528
+ if (settled) return;
529
+ settled = true;
530
+ disposeAbort();
531
+ clearTimeout(timer);
532
+ let reason = "aborted";
533
+ if (err instanceof Error) reason = err.message;
534
+ else if (err === void 0) reason = "fetch complete";
535
+ sub.close(reason);
536
+ if (err === void 0) resolve();
537
+ else reject(err);
538
+ };
539
+ const disposeAbort = onAbort(opts.signal, () => {
540
+ if (opts.signal !== void 0) done(abortReason(opts.signal));
541
+ });
542
+ const timer = setTimeout(() => {
543
+ end = { type: "timeout" };
544
+ done();
545
+ }, opts.timeoutMs);
546
+ const sub = subscribe(filters, {
547
+ id: opts.id,
548
+ signal: opts.signal,
549
+ closeOnEose: true,
550
+ onevent(event) {
551
+ if (seen.has(event.id)) return;
552
+ seen.add(event.id);
553
+ events.push(event);
554
+ },
555
+ oneose() {
556
+ end = { type: "eose" };
557
+ done();
558
+ },
559
+ onclose(reason) {
560
+ if (settled) return;
561
+ if (opts.signal?.aborted === true) {
562
+ done(abortReason(opts.signal));
563
+ return;
564
+ }
565
+ end = {
566
+ type: "closed",
567
+ reason
568
+ };
569
+ done();
570
+ }
571
+ });
572
+ });
573
+ return {
574
+ events,
575
+ end
576
+ };
577
+ }
578
+ //#endregion
65
579
  //#region src/relay/neg-session.ts
66
580
  function createNegSession() {
67
581
  return {
@@ -88,6 +602,21 @@ function failNegSession(session, err) {
88
602
  function failNegErr(session, reason) {
89
603
  failNegSession(session, new Nip77Error(reason));
90
604
  }
605
+ /** Route a NEG-MSG/NEG-ERR frame to its session; unknown ids are ignored. */
606
+ function dispatchNegMessage(sessions, msg) {
607
+ const session = sessions.get(msg[1]);
608
+ if (session === void 0) return;
609
+ if (msg[0] === "NEG-MSG") {
610
+ pushNegMsg(session, msg[2]);
611
+ return;
612
+ }
613
+ failNegErr(session, msg[2]);
614
+ }
615
+ /** Fail every tracked session (socket teardown). */
616
+ function failAllNegSessions(sessions, err) {
617
+ for (const session of sessions.values()) failNegSession(session, err);
618
+ sessions.clear();
619
+ }
91
620
  /** Wire queue/timeout/abort around `runNegSession`. Not a second session class. */
92
621
  async function runWiredNegSession(opts) {
93
622
  const { session, storage, filter, id, timeoutMs, signal, send, url } = opts;
@@ -144,6 +673,175 @@ async function runWiredNegSession(opts) {
144
673
  next
145
674
  });
146
675
  }
676
+ /**
677
+ * Drive a NIP-77 reconcile session against the session map: fail a same-id predecessor, register,
678
+ * wire queue/timeout/abort, and send NEG-CLOSE on exit.
679
+ */
680
+ async function runTrackedNegSession(opts) {
681
+ const { sessions, id, send } = opts;
682
+ const prev = sessions.get(id);
683
+ if (prev) failNegSession(prev, new Nip77Error("closed: replaced by new NEG-OPEN"));
684
+ const session = createNegSession();
685
+ sessions.set(id, session);
686
+ try {
687
+ return await runWiredNegSession({
688
+ session,
689
+ storage: opts.storage,
690
+ filter: opts.filter,
691
+ id,
692
+ timeoutMs: opts.timeoutMs,
693
+ signal: opts.signal,
694
+ send,
695
+ url: opts.url
696
+ });
697
+ } finally {
698
+ if (sessions.get(id) === session) {
699
+ sessions.delete(id);
700
+ try {
701
+ send(["NEG-CLOSE", id]);
702
+ } catch {}
703
+ }
704
+ }
705
+ }
706
+ //#endregion
707
+ //#region src/relay/ok-tracker.ts
708
+ var OkTracker = class {
709
+ #entries = /* @__PURE__ */ new Map();
710
+ get size() {
711
+ return this.#entries.size;
712
+ }
713
+ get(eventId) {
714
+ return this.#entries.get(eventId);
715
+ }
716
+ /** Joiner's view: only publish waiters (carrying an `event`) are joinable. */
717
+ inFlight(eventId) {
718
+ const waiter = this.#entries.get(eventId);
719
+ return waiter?.event === void 0 ? void 0 : waiter.promise;
720
+ }
721
+ /**
722
+ * Register a waiter, arm its timeout and call `send`. A send throw removes the entry and rejects
723
+ * (`sendError` when the throw is not an Error). The timer only rejects while this waiter still
724
+ * owns the id.
725
+ */
726
+ async track(eventId, opts) {
727
+ const promise = new Promise((resolve, reject) => {
728
+ const waiter = {
729
+ id: eventId,
730
+ resolve,
731
+ reject,
732
+ timer: setTimeout(() => {
733
+ if (this.#entries.get(eventId) === waiter) {
734
+ this.#entries.delete(eventId);
735
+ reject(opts.timeoutError());
736
+ }
737
+ }, opts.timeoutMs),
738
+ event: opts.event,
739
+ authRetried: false,
740
+ timeoutMs: opts.timeoutMs,
741
+ timeoutError: opts.timeoutError,
742
+ get promise() {
743
+ return promise;
744
+ }
745
+ };
746
+ this.#entries.set(eventId, waiter);
747
+ try {
748
+ opts.send();
749
+ } catch (error) {
750
+ this.#finish(waiter);
751
+ reject(error instanceof Error ? error : opts.sendError());
752
+ }
753
+ });
754
+ return promise;
755
+ }
756
+ /**
757
+ * An OK reply arrived: `retry` is the caller's auth-required policy — a kept waiter is returned
758
+ * unsettled for the retry path; otherwise the entry is settled and undefined is returned.
759
+ */
760
+ handleOk(eventId, ok, message, retry) {
761
+ const waiter = this.#entries.get(eventId);
762
+ if (waiter === void 0) return;
763
+ if (!retry(waiter)) {
764
+ this.settle(waiter, {
765
+ ok,
766
+ message
767
+ });
768
+ return;
769
+ }
770
+ return waiter;
771
+ }
772
+ /** Settle a waiter: stop its timer, drop the entry it still owns, then resolve. */
773
+ settle(waiter, result) {
774
+ this.#finish(waiter);
775
+ waiter.resolve(result);
776
+ }
777
+ /** Stop a waiter's timeout without settling (AUTH retry window). */
778
+ clearTimer(waiter) {
779
+ if (waiter.timer !== void 0) {
780
+ clearTimeout(waiter.timer);
781
+ waiter.timer = void 0;
782
+ }
783
+ }
784
+ /** Re-arm the timeout for a re-sent frame (after an AUTH retry). */
785
+ restartTimer(eventId) {
786
+ const waiter = this.#entries.get(eventId);
787
+ if (waiter === void 0) return;
788
+ waiter.timer = setTimeout(() => {
789
+ if (this.#entries.get(eventId) === waiter) {
790
+ this.#entries.delete(eventId);
791
+ waiter.reject(waiter.timeoutError());
792
+ }
793
+ }, waiter.timeoutMs);
794
+ }
795
+ /**
796
+ * Re-send a tracked EVENT after the relay demanded AUTH: pause the timeout through the AUTH
797
+ * window, re-arm it and re-send on success, settle `{ok:false,message}` on failure. AUTH waiters
798
+ * (no `event`) settle immediately.
799
+ */
800
+ async authRetry(waiter, message, deps) {
801
+ this.clearTimer(waiter);
802
+ const finish = (result) => {
803
+ this.settle(waiter, result);
804
+ };
805
+ try {
806
+ const { event } = waiter;
807
+ if (event === void 0) {
808
+ finish({
809
+ ok: false,
810
+ message
811
+ });
812
+ return;
813
+ }
814
+ const ok = await deps.ensureAuthed();
815
+ if (this.#entries.get(waiter.id) !== waiter) return;
816
+ if (!ok || !deps.isOpen()) {
817
+ finish({
818
+ ok: false,
819
+ message
820
+ });
821
+ return;
822
+ }
823
+ this.restartTimer(waiter.id);
824
+ deps.send(event);
825
+ } catch {
826
+ if (this.#entries.get(waiter.id) === void 0) return;
827
+ finish({
828
+ ok: false,
829
+ message
830
+ });
831
+ }
832
+ }
833
+ rejectAll(err) {
834
+ for (const waiter of this.#entries.values()) {
835
+ this.clearTimer(waiter);
836
+ waiter.reject(err);
837
+ }
838
+ this.#entries.clear();
839
+ }
840
+ #finish(waiter) {
841
+ this.clearTimer(waiter);
842
+ if (this.#entries.get(waiter.id) === waiter) this.#entries.delete(waiter.id);
843
+ }
844
+ };
147
845
  const PING_DUMMY_FILTER = {
148
846
  ids: ["a".repeat(64)],
149
847
  limit: 0
@@ -266,458 +964,254 @@ var PingLoop = class {
266
964
  }
267
965
  };
268
966
  //#endregion
269
- //#region src/relay/subscription.ts
270
- var Subscription = class {
271
- id;
272
- filters;
273
- handlers;
274
- closeOnEose;
275
- eosed = false;
276
- closed = false;
277
- /** True after one CLOSED `auth-required:` retry. */
278
- authRetried = false;
279
- /** Inclusive NIP-01 `since` watermark from verified EVENTs. */
280
- lastCreatedAt;
281
- /** Event ids at `lastCreatedAt` (same-second reconnect dedup). Not all seen ids. */
282
- idsAtWatermark = /* @__PURE__ */ new Set();
283
- #sendClose;
284
- #abort;
285
- constructor(filters, opts, sendClose) {
286
- this.#sendClose = sendClose;
287
- this.id = createSubscriptionId(opts.id);
288
- this.filters = filters;
289
- this.closeOnEose = opts.closeOnEose === true;
290
- this.handlers = {
291
- onevent: opts.onevent,
292
- oneose: opts.oneose,
293
- onclose: opts.onclose,
294
- alreadyHaveEvent: opts.alreadyHaveEvent,
295
- receivedEvent: opts.receivedEvent
296
- };
297
- if (opts.signal) {
298
- if (opts.signal.aborted) this.close("aborted");
299
- else this.#abort = onAbort(opts.signal, () => this.close("aborted"));
300
- }
967
+ //#region src/relay/subscription-registry.ts
968
+ /**
969
+ * Owns every open REQ of a relay: exclusive subscriptions keyed by id, and live coalescing groups
970
+ * (one wire REQ plus per-subscriber handles) keyed by fingerprint and wire id.
971
+ */
972
+ var SubscriptionRegistry = class {
973
+ #subs = /* @__PURE__ */ new Map();
974
+ #liveByFp = /* @__PURE__ */ new Map();
975
+ #liveBySubId = /* @__PURE__ */ new Map();
976
+ #isOpen;
977
+ #enableReconnect;
978
+ #send;
979
+ #scheduleReconnect;
980
+ #acceptEvent;
981
+ constructor(deps) {
982
+ this.#isOpen = deps.isOpen;
983
+ this.#enableReconnect = deps.enableReconnect;
984
+ this.#send = deps.send;
985
+ this.#scheduleReconnect = deps.scheduleReconnect;
986
+ this.#acceptEvent = deps.acceptEvent;
301
987
  }
302
- /** Drop the signal listener. Terminal paths that mark `closed` without `close()` run this too. */
303
- dispose() {
304
- this.#abort?.();
988
+ /** User subscriptions only; dummy ping REQs are not counted. */
989
+ get size() {
990
+ return this.#subs.size;
305
991
  }
306
- close(reason = "closed by client") {
307
- if (this.closed) return;
308
- this.closed = true;
309
- this.dispose();
310
- this.#sendClose(this.id);
311
- invokeSafely(() => this.handlers.onclose?.(reason));
992
+ get(subId) {
993
+ return this.#subs.get(subId);
312
994
  }
313
- /** Advance the reconnect watermark after a verified EVENT. */
314
- noteVerified(event) {
315
- if (this.lastCreatedAt === void 0 || event.created_at > this.lastCreatedAt) {
316
- this.lastCreatedAt = event.created_at;
317
- this.idsAtWatermark.clear();
318
- this.idsAtWatermark.add(event.id);
995
+ /** Exclusive one-shot REQ when `closeOnEose`, live coalescing otherwise. */
996
+ open(filters, opts) {
997
+ if (opts.closeOnEose === true) return this.#openExclusive(filters, opts);
998
+ return this.#openLive(filters, opts);
999
+ }
1000
+ handleEvent(subId, event) {
1001
+ const sub = this.#subs.get(subId);
1002
+ if (!sub || sub.closed) return;
1003
+ const group = this.#liveBySubId.get(subId);
1004
+ if (group) {
1005
+ this.#deliverLiveEvent(group, event);
319
1006
  return;
320
1007
  }
321
- if (event.created_at === this.lastCreatedAt) this.idsAtWatermark.add(event.id);
322
- }
323
- /**
324
- * Filters for re-REQ. Original `filters` stay unchanged. NIP-01 `since` is inclusive — never
325
- * `lastCreatedAt + 1`.
326
- */
327
- replayFilters() {
328
- const since = this.lastCreatedAt;
329
- if (since === void 0) return this.filters;
330
- return this.filters.map((f) => ({
331
- ...f,
332
- since: f.since === void 0 ? since : Math.max(f.since, since)
333
- }));
334
- }
335
- };
336
- /** AsyncIterable wrapper over a subscription's events until EOSE or close. */
337
- function subscriptionToAsyncIterable(start, opts) {
338
- const queue = [];
339
- let done = false;
340
- let error;
341
- let wake;
342
- let closer;
343
- let localClose = false;
344
- let disposeAbort;
345
- const notify = () => {
346
- wake?.();
347
- wake = void 0;
348
- };
349
- const waitForWake = async () => new Promise((resolve) => {
350
- wake = resolve;
351
- });
352
- const closeLocal = (reason) => {
353
- localClose = true;
354
- disposeAbort?.();
355
- closer?.close(reason);
356
- done = true;
357
- notify();
358
- };
359
- closer = start({
360
- onevent(event) {
361
- queue.push(event);
362
- notify();
363
- },
364
- oneose() {
365
- if (opts?.includeEose === false) closeLocal("eose");
366
- },
367
- onclose(reason) {
368
- if (opts?.signal?.aborted === true) localClose = true;
369
- if (!localClose && reason) error = new RelayClosedError(reason);
370
- done = true;
371
- notify();
1008
+ sub.notifyReceived(event.id);
1009
+ if (sub.isAtWatermark(event.id)) return;
1010
+ if (sub.alreadyHas(event.id)) return;
1011
+ if (!matchFilters(sub.filters, event)) return;
1012
+ if (!this.#acceptEvent(event)) return;
1013
+ sub.deliver(event);
1014
+ }
1015
+ handleEose(subId) {
1016
+ const sub = this.#subs.get(subId);
1017
+ if (!sub || sub.closed || sub.eosed) return;
1018
+ const group = this.#liveBySubId.get(subId);
1019
+ if (group) {
1020
+ sub.markEose();
1021
+ this.#deliverLiveEose(group);
1022
+ return;
372
1023
  }
373
- });
374
- if (opts?.signal) {
375
- if (opts.signal.aborted) closeLocal("aborted");
376
- else {
377
- disposeAbort = onAbort(opts.signal, () => closeLocal("aborted"));
378
- if (done) disposeAbort();
1024
+ sub.markEose();
1025
+ }
1026
+ /** Remote/transport termination of a REQ (CLOSED, auth-retry failure). */
1027
+ drop(sub, reason) {
1028
+ const group = this.#liveBySubId.get(sub.id);
1029
+ if (group) {
1030
+ this.#endLiveGroup(group, {
1031
+ sendClose: false,
1032
+ reason
1033
+ });
1034
+ return;
379
1035
  }
1036
+ this.#subs.delete(sub.id);
1037
+ sub.end(reason);
380
1038
  }
381
- return {
382
- close(reason) {
383
- closeLocal(reason ?? "closed by client");
384
- },
385
- [Symbol.asyncIterator]() {
386
- return {
387
- async next() {
388
- while (true) {
389
- const value = queue.shift();
390
- if (value !== void 0) return {
391
- value,
392
- done: false
393
- };
394
- if (error !== void 0) throw error;
395
- if (done) return {
396
- value: void 0,
397
- done: true
398
- };
399
- await waitForWake();
400
- }
401
- },
402
- async return() {
403
- closeLocal("iterator returned");
404
- return {
405
- value: void 0,
406
- done: true
407
- };
408
- }
409
- };
410
- }
411
- };
412
- }
413
- //#endregion
414
- //#region src/relay/subscribe.ts
415
- /** REQ subscription runtime: exclusive one-shot, live coalescing, dispatch, reconnect replay. */
416
- function openExclusive(ctx, filters, opts) {
417
- const sub = new Subscription(filters, opts, (id) => {
418
- ctx.subs.delete(id);
1039
+ /**
1040
+ * Re-REQ after the relay demanded AUTH: drop the subscription when auth fails or the
1041
+ * socket/subscription died during the AUTH window, replay the REQ on success.
1042
+ */
1043
+ async authRetry(sub, reason, ensureAuthed) {
419
1044
  try {
420
- if (ctx.connected()) ctx.send(["CLOSE", id]);
421
- } catch {}
422
- });
423
- ctx.subs.set(sub.id, sub);
424
- if (ctx.connected()) ctx.send([
425
- "REQ",
426
- sub.id,
427
- ...filters
428
- ]);
429
- else if (ctx.enableReconnect()) ctx.scheduleReconnect();
430
- if (opts.eoseTimeoutMs !== void 0) ctx.armEoseTimeout(sub, opts.eoseTimeoutMs);
431
- return sub;
432
- }
433
- function subscribeLive(ctx, filters, opts) {
434
- const fp = filterFingerprint(filters);
435
- let group = ctx.liveByFp.get(fp);
436
- if (group?.sub.eosed === true) group = void 0;
437
- let created = false;
438
- if (!group) {
439
- created = true;
440
- const slot = { group: void 0 };
441
- const wire = new Subscription(filters, { id: opts.id }, () => {
442
- const g = slot.group;
443
- if (g) endLiveGroup(ctx, g, {
444
- sendClose: true,
445
- reason: "closed by client"
446
- });
447
- });
448
- group = {
449
- fp,
450
- sub: wire,
451
- attachments: /* @__PURE__ */ new Set()
452
- };
453
- slot.group = group;
454
- ctx.liveByFp.set(fp, group);
455
- ctx.liveBySubId.set(wire.id, group);
456
- ctx.subs.set(wire.id, wire);
457
- }
458
- const slot = { handle: void 0 };
459
- const attached = group;
460
- const handle = new Subscription(filters, {
461
- ...opts,
462
- id: group.sub.id
463
- }, () => {
464
- const current = slot.handle;
465
- if (!current) return;
466
- detachLive(ctx, attached, current);
467
- });
468
- slot.handle = handle;
469
- if (handle.closed) {
470
- if (created && group.attachments.size === 0) forgetLiveGroup(ctx, group);
471
- return handle;
1045
+ if (!await ensureAuthed() || sub.closed || !this.#isOpen()) {
1046
+ this.drop(sub, reason);
1047
+ return;
1048
+ }
1049
+ this.replay(sub);
1050
+ } catch {
1051
+ this.drop(sub, reason);
1052
+ }
472
1053
  }
473
- group.attachments.add(handle);
474
- if (created) {
475
- if (ctx.connected()) ctx.send([
1054
+ /** Re-REQ after AUTH: rearm the wire and its handles, then send the replayed filters. */
1055
+ replay(sub) {
1056
+ sub.rearm();
1057
+ const group = this.#liveBySubId.get(sub.id);
1058
+ if (group) for (const att of group.attachments) att.rearm();
1059
+ this.#send([
476
1060
  "REQ",
477
- group.sub.id,
478
- ...filters
1061
+ sub.id,
1062
+ ...sub.replayFilters()
479
1063
  ]);
480
- else if (ctx.enableReconnect()) ctx.scheduleReconnect();
481
- }
482
- if (opts.eoseTimeoutMs !== void 0) ctx.armEoseTimeout(handle, opts.eoseTimeoutMs);
483
- return handle;
484
- }
485
- function forgetLiveGroup(ctx, group) {
486
- if (ctx.liveByFp.get(group.fp) === group) ctx.liveByFp.delete(group.fp);
487
- ctx.liveBySubId.delete(group.sub.id);
488
- ctx.subs.delete(group.sub.id);
489
- group.sub.closed = true;
490
- group.sub.dispose();
491
- }
492
- function detachLive(ctx, group, handle) {
493
- if (!group.attachments.delete(handle)) return;
494
- if (group.attachments.size > 0) return;
495
- endLiveGroup(ctx, group, {
496
- sendClose: true,
497
- reason: "closed by client"
498
- });
499
- }
500
- function endLiveGroup(ctx, group, opts) {
501
- const { id } = group.sub;
502
- const remaining = [...group.attachments];
503
- group.attachments.clear();
504
- forgetLiveGroup(ctx, group);
505
- if (opts.sendClose) try {
506
- if (ctx.connected()) ctx.send(["CLOSE", id]);
507
- } catch {}
508
- for (const att of remaining) invokeSafely(() => {
509
- att.close(opts.reason);
510
- });
511
- }
512
- function deliverLiveEvent(ctx, group, event) {
513
- const { sub } = group;
514
- const attachments = [...group.attachments];
515
- for (const att of attachments) invokeSafely(() => {
516
- att.handlers.receivedEvent?.(event.id);
517
- });
518
- if (sub.idsAtWatermark.has(event.id)) return;
519
- const recipients = [];
520
- for (const att of attachments) {
521
- if (att.closed) continue;
522
- let skip = false;
523
- invokeSafely(() => {
524
- skip = Boolean(att.handlers.alreadyHaveEvent?.(event.id));
525
- });
526
- if (!skip) recipients.push(att);
527
- }
528
- if (recipients.length === 0) return;
529
- if (!ctx.acceptEvent(event)) return;
530
- sub.noteVerified(event);
531
- for (const att of recipients) att.noteVerified(event);
532
- for (const att of recipients) invokeSafely(() => {
533
- att.handlers.onevent?.(event);
534
- });
535
- }
536
- function deliverLiveEose(group) {
537
- const attachments = [...group.attachments];
538
- for (const att of attachments) {
539
- if (att.closed || att.eosed) continue;
540
- att.eosed = true;
541
- invokeSafely(() => {
542
- att.handlers.oneose?.();
543
- });
544
- }
545
- }
546
- function closeAllSubscriptions(ctx, reason) {
547
- const groups = [...ctx.liveBySubId.values()];
548
- for (const group of groups) invokeSafely(() => {
549
- endLiveGroup(ctx, group, {
550
- sendClose: false,
551
- reason
552
- });
553
- });
554
- for (const sub of ctx.subs.values()) if (!sub.closed) {
555
- sub.closed = true;
556
- sub.dispose();
557
- invokeSafely(() => {
558
- sub.handlers.onclose?.(reason);
559
- });
560
- }
561
- ctx.subs.clear();
562
- ctx.liveByFp.clear();
563
- ctx.liveBySubId.clear();
564
- }
565
- function dropSubscription(ctx, sub, reason) {
566
- const group = ctx.liveBySubId.get(sub.id);
567
- if (group) {
568
- endLiveGroup(ctx, group, {
569
- sendClose: false,
570
- reason
571
- });
572
- return;
573
- }
574
- ctx.subs.delete(sub.id);
575
- sub.closed = true;
576
- sub.dispose();
577
- invokeSafely(() => {
578
- sub.handlers.onclose?.(reason);
579
- });
580
- }
581
- function onSubEvent(ctx, subId, event) {
582
- const sub = ctx.subs.get(subId);
583
- if (!sub || sub.closed) return;
584
- const group = ctx.liveBySubId.get(subId);
585
- if (group) {
586
- deliverLiveEvent(ctx, group, event);
587
- return;
588
- }
589
- invokeSafely(() => {
590
- sub.handlers.receivedEvent?.(event.id);
591
- });
592
- if (sub.idsAtWatermark.has(event.id)) return;
593
- let have = false;
594
- invokeSafely(() => {
595
- have = Boolean(sub.handlers.alreadyHaveEvent?.(event.id));
596
- });
597
- if (have) return;
598
- if (!ctx.acceptEvent(event)) return;
599
- sub.noteVerified(event);
600
- invokeSafely(() => {
601
- sub.handlers.onevent?.(event);
602
- });
603
- }
604
- function onSubEose(ctx, subId) {
605
- const sub = ctx.subs.get(subId);
606
- if (!sub || sub.closed) return;
607
- if (sub.eosed) return;
608
- sub.eosed = true;
609
- const group = ctx.liveBySubId.get(subId);
610
- if (group) deliverLiveEose(group);
611
- else {
612
- invokeSafely(() => {
613
- sub.handlers.oneose?.();
614
- });
615
- if (sub.closeOnEose) sub.close("eose");
616
1064
  }
617
- }
618
- function resubscribeAll(ctx) {
619
- for (const sub of ctx.subs.values()) {
620
- if (sub.closed) continue;
621
- sub.eosed = false;
622
- sub.authRetried = false;
623
- const group = ctx.liveBySubId.get(sub.id);
624
- if (group) for (const att of group.attachments) att.eosed = false;
625
- try {
626
- ctx.send([
627
- "REQ",
628
- sub.id,
629
- ...sub.replayFilters()
630
- ]);
631
- } catch {
632
- return false;
1065
+ /** Reconnect path: rearm every open REQ and re-send; false on send failure. */
1066
+ replayAll() {
1067
+ for (const sub of this.#subs.values()) {
1068
+ if (sub.closed) continue;
1069
+ sub.rearm();
1070
+ sub.resetAuthRetry();
1071
+ const group = this.#liveBySubId.get(sub.id);
1072
+ if (group) for (const att of group.attachments) {
1073
+ att.rearm();
1074
+ att.resetAuthRetry();
1075
+ }
1076
+ try {
1077
+ this.#send([
1078
+ "REQ",
1079
+ sub.id,
1080
+ ...sub.replayFilters()
1081
+ ]);
1082
+ } catch {
1083
+ return false;
1084
+ }
633
1085
  }
1086
+ return true;
634
1087
  }
635
- return true;
636
- }
637
- function streamFilters(subscribe, filters, opts) {
638
- return subscriptionToAsyncIterable((handlers) => subscribe(filters, {
639
- ...handlers,
640
- id: opts?.id,
641
- signal: opts?.signal
642
- }), { signal: opts?.signal });
643
- }
644
- async function fetchFilters(subscribe, filters, opts) {
645
- throwIfAborted(opts.signal);
646
- const events = [];
647
- const seen = /* @__PURE__ */ new Set();
648
- let end = { type: "timeout" };
649
- await new Promise((resolve, reject) => {
650
- let settled = false;
651
- const done = (err) => {
652
- if (settled) return;
653
- settled = true;
654
- disposeAbort();
655
- clearTimeout(timer);
656
- let reason = "aborted";
657
- if (err instanceof Error) reason = err.message;
658
- else if (err === void 0) reason = "fetch complete";
659
- sub.close(reason);
660
- if (err === void 0) resolve();
661
- else reject(err);
662
- };
663
- const disposeAbort = onAbort(opts.signal, () => {
664
- if (opts.signal !== void 0) done(abortReason(opts.signal));
1088
+ closeAll(reason) {
1089
+ const groups = [...this.#liveBySubId.values()];
1090
+ for (const group of groups) invokeSafely(() => {
1091
+ this.#endLiveGroup(group, {
1092
+ sendClose: false,
1093
+ reason
1094
+ });
665
1095
  });
666
- const timer = setTimeout(() => {
667
- end = { type: "timeout" };
668
- done();
669
- }, opts.timeoutMs);
670
- const sub = subscribe(filters, {
671
- id: opts.id,
672
- signal: opts.signal,
673
- closeOnEose: true,
674
- onevent(event) {
675
- if (seen.has(event.id)) return;
676
- seen.add(event.id);
677
- events.push(event);
678
- },
679
- oneose() {
680
- end = { type: "eose" };
681
- done();
682
- },
683
- onclose(reason) {
684
- if (settled) return;
685
- if (opts.signal?.aborted === true) {
686
- done(abortReason(opts.signal));
687
- return;
688
- }
689
- end = {
690
- type: "closed",
691
- reason
692
- };
693
- done();
694
- }
1096
+ for (const sub of this.#subs.values()) sub.end(reason);
1097
+ this.#subs.clear();
1098
+ this.#liveByFp.clear();
1099
+ this.#liveBySubId.clear();
1100
+ }
1101
+ #openExclusive(filters, opts) {
1102
+ const sub = new Subscription(filters, opts, (closed) => {
1103
+ this.#subs.delete(closed.id);
1104
+ try {
1105
+ if (this.#isOpen()) this.#send(["CLOSE", closed.id]);
1106
+ } catch {}
1107
+ });
1108
+ this.#subs.set(sub.id, sub);
1109
+ if (this.#isOpen()) this.#send([
1110
+ "REQ",
1111
+ sub.id,
1112
+ ...filters
1113
+ ]);
1114
+ else if (this.#enableReconnect) this.#scheduleReconnect();
1115
+ return sub;
1116
+ }
1117
+ #openLive(filters, opts) {
1118
+ const fp = filterFingerprint(filters);
1119
+ let group = this.#liveByFp.get(fp);
1120
+ if (group?.sub.eosed === true) group = void 0;
1121
+ let created = false;
1122
+ if (!group) {
1123
+ created = true;
1124
+ const slot = { group: void 0 };
1125
+ const wire = new Subscription(filters, { id: opts.id }, () => {
1126
+ const g = slot.group;
1127
+ if (g) this.#endLiveGroup(g, {
1128
+ sendClose: true,
1129
+ reason: "closed by client"
1130
+ });
1131
+ });
1132
+ group = {
1133
+ fp,
1134
+ sub: wire,
1135
+ attachments: /* @__PURE__ */ new Set()
1136
+ };
1137
+ slot.group = group;
1138
+ this.#liveByFp.set(fp, group);
1139
+ this.#liveBySubId.set(wire.id, group);
1140
+ this.#subs.set(wire.id, wire);
1141
+ }
1142
+ const slot = { handle: void 0 };
1143
+ const attached = group;
1144
+ const handle = new Subscription(filters, {
1145
+ ...opts,
1146
+ id: group.sub.id
1147
+ }, () => {
1148
+ const current = slot.handle;
1149
+ if (!current) return;
1150
+ this.#detachLive(attached, current);
695
1151
  });
696
- });
697
- return {
698
- events,
699
- end
700
- };
701
- }
702
- function armEoseTimeout(sub, eoseTimeoutMs) {
703
- const timer = setTimeout(() => {
704
- if (sub.eosed || sub.closed) return;
705
- sub.eosed = true;
706
- invokeSafely(() => {
707
- sub.handlers.oneose?.();
1152
+ slot.handle = handle;
1153
+ if (handle.closed) {
1154
+ if (created && group.attachments.size === 0) this.#forgetLiveGroup(group, "closed by client");
1155
+ return handle;
1156
+ }
1157
+ group.attachments.add(handle);
1158
+ if (created) {
1159
+ if (this.#isOpen()) this.#send([
1160
+ "REQ",
1161
+ group.sub.id,
1162
+ ...filters
1163
+ ]);
1164
+ else if (this.#enableReconnect) this.#scheduleReconnect();
1165
+ }
1166
+ return handle;
1167
+ }
1168
+ #forgetLiveGroup(group, reason) {
1169
+ if (this.#liveByFp.get(group.fp) === group) this.#liveByFp.delete(group.fp);
1170
+ this.#liveBySubId.delete(group.sub.id);
1171
+ this.#subs.delete(group.sub.id);
1172
+ group.sub.end(reason);
1173
+ }
1174
+ #detachLive(group, handle) {
1175
+ if (!group.attachments.delete(handle)) return;
1176
+ if (group.attachments.size > 0) return;
1177
+ this.#endLiveGroup(group, {
1178
+ sendClose: true,
1179
+ reason: "closed by client"
708
1180
  });
709
- }, eoseTimeoutMs);
710
- const prevClose = sub.handlers.onclose;
711
- sub.handlers.onclose = (reason) => {
712
- clearTimeout(timer);
713
- prevClose?.(reason);
714
- };
715
- const prevEose = sub.handlers.oneose;
716
- sub.handlers.oneose = () => {
717
- clearTimeout(timer);
718
- prevEose?.();
719
- };
720
- }
1181
+ }
1182
+ #endLiveGroup(group, opts) {
1183
+ const { id } = group.sub;
1184
+ const remaining = [...group.attachments];
1185
+ group.attachments.clear();
1186
+ this.#forgetLiveGroup(group, opts.reason);
1187
+ if (opts.sendClose) try {
1188
+ if (this.#isOpen()) this.#send(["CLOSE", id]);
1189
+ } catch {}
1190
+ for (const att of remaining) invokeSafely(() => {
1191
+ att.close(opts.reason);
1192
+ });
1193
+ }
1194
+ #deliverLiveEvent(group, event) {
1195
+ const { sub } = group;
1196
+ const attachments = [...group.attachments];
1197
+ for (const att of attachments) att.notifyReceived(event.id);
1198
+ if (sub.isAtWatermark(event.id)) return;
1199
+ const recipients = [];
1200
+ for (const att of attachments) {
1201
+ if (att.closed) continue;
1202
+ if (!att.alreadyHas(event.id)) recipients.push(att);
1203
+ }
1204
+ if (recipients.length === 0) return;
1205
+ if (!matchFilters(sub.filters, event)) return;
1206
+ if (!this.#acceptEvent(event)) return;
1207
+ sub.noteVerified(event);
1208
+ for (const att of recipients) att.deliver(event);
1209
+ }
1210
+ #deliverLiveEose(group) {
1211
+ const attachments = [...group.attachments];
1212
+ for (const att of attachments) att.markEose();
1213
+ }
1214
+ };
721
1215
  //#endregion
722
1216
  //#region src/relay/relay.ts
723
1217
  /**
@@ -762,12 +1256,11 @@ var Relay = class Relay {
762
1256
  #connectFinish;
763
1257
  #connectTimer;
764
1258
  #socketHandlers;
765
- #subs = /* @__PURE__ */ new Map();
766
- #liveByFp = /* @__PURE__ */ new Map();
767
- #liveBySubId = /* @__PURE__ */ new Map();
768
- #live;
769
- #publishes = /* @__PURE__ */ new Map();
770
- #counts = /* @__PURE__ */ new Map();
1259
+ #subscriptions;
1260
+ #ok = new OkTracker();
1261
+ #countRequests = new CountTracker();
1262
+ #authState;
1263
+ #ensureAuthed;
771
1264
  #neg = /* @__PURE__ */ new Map();
772
1265
  #WS;
773
1266
  #verify;
@@ -777,13 +1270,6 @@ var Relay = class Relay {
777
1270
  #enableReconnect;
778
1271
  #backoff;
779
1272
  #serial = 0;
780
- #challenge;
781
- #authedChallenge;
782
- /** Challenge value already answered on this connection; duplicates are not re-signed. */
783
- #answeredChallenge;
784
- /** Settled OK verdict for `#answeredChallenge`; set only once the relay replies. */
785
- #answeredResult;
786
- #authPromise;
787
1273
  #authSigner;
788
1274
  /** `close()`/`disconnect()` was called; cleared by the next `connect()`. */
789
1275
  #manualStop = false;
@@ -791,13 +1277,7 @@ var Relay = class Relay {
791
1277
  #reconnectTimer;
792
1278
  #enablePing;
793
1279
  #ping;
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
- };
1280
+ #events = new Emitter();
801
1281
  constructor(url, opts = {}) {
802
1282
  this.url = normalizeURL(url);
803
1283
  this.#WS = opts.websocketImplementation ?? getWebSocketImplementation();
@@ -821,21 +1301,29 @@ var Relay = class Relay {
821
1301
  pingIntervalMs: opts.pingIntervalMs ?? 29e3,
822
1302
  pingTimeoutMs: opts.pingTimeoutMs ?? 2e4
823
1303
  });
824
- this.#live = {
825
- liveByFp: this.#liveByFp,
826
- liveBySubId: this.#liveBySubId,
827
- subs: this.#subs,
828
- connected: () => this.#isOpen(),
829
- enableReconnect: () => this.#enableReconnect,
1304
+ this.#subscriptions = new SubscriptionRegistry({
1305
+ isOpen: () => this.#isOpen(),
1306
+ enableReconnect: this.#enableReconnect,
830
1307
  send: (message) => this.#send(message),
831
1308
  scheduleReconnect: () => this.#scheduleReconnect(),
832
- acceptEvent: (event) => this.#acceptEvent(event),
833
- armEoseTimeout
834
- };
1309
+ acceptEvent: (event) => this.#acceptEvent(event)
1310
+ });
1311
+ this.#authState = new RelayAuth({
1312
+ url: this.url,
1313
+ isOpen: () => this.#isOpen(),
1314
+ sendAuth: (event, timeoutMs) => this.#ok.track(event.id, {
1315
+ timeoutMs,
1316
+ timeoutError: () => new RelayTimeoutError("auth timed out", this.url),
1317
+ sendError: () => new RelayPublishError("auth send failed", this.url),
1318
+ send: () => this.#send(["AUTH", event])
1319
+ }),
1320
+ defaultTimeoutMs: this.#publishTimeoutMs
1321
+ });
1322
+ this.#ensureAuthed = this.#authState.ensureAuthed.bind(this.#authState, this.#authSigner);
835
1323
  }
836
1324
  /** Latest NIP-42 challenge, if any. */
837
1325
  get challenge() {
838
- return this.#challenge;
1326
+ return this.#authState.challenge;
839
1327
  }
840
1328
  get connected() {
841
1329
  return this.#isOpen();
@@ -857,11 +1345,11 @@ var Relay = class Relay {
857
1345
  }
858
1346
  /** User subscriptions only; dummy ping REQs are not counted. */
859
1347
  get subscriptionCount() {
860
- return this.#subs.size;
1348
+ return this.#subscriptions.size;
861
1349
  }
862
1350
  /** One-shot requests still awaiting a relay reply (EVENT ACK, COUNT, NEG). */
863
1351
  get inFlightCount() {
864
- return this.#publishes.size + this.#counts.size + this.#neg.size;
1352
+ return this.#ok.size + this.#countRequests.size + this.#neg.size;
865
1353
  }
866
1354
  /**
867
1355
  * Listen for a relay event. Multiple listeners per type are allowed and fire in registration
@@ -869,14 +1357,7 @@ var Relay = class Relay {
869
1357
  * function.
870
1358
  */
871
1359
  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));
1360
+ return this.#events.on(type, listener);
880
1361
  }
881
1362
  static async connect(url, opts) {
882
1363
  const relay = new Relay(url, opts);
@@ -912,6 +1393,15 @@ var Relay = class Relay {
912
1393
  } catch {}
913
1394
  if (this.#ws === ws) this.#ws = void 0;
914
1395
  };
1396
+ const guarded = (fn) => {
1397
+ return () => {
1398
+ if (gen !== this.#gen) {
1399
+ release();
1400
+ return;
1401
+ }
1402
+ fn();
1403
+ };
1404
+ };
915
1405
  const finish = (err) => {
916
1406
  if (settled) return;
917
1407
  settled = true;
@@ -924,18 +1414,14 @@ var Relay = class Relay {
924
1414
  };
925
1415
  this.#connecting = connecting;
926
1416
  this.#connectFinish = finish;
927
- const timer = setTimeout(() => {
928
- if (gen !== this.#gen) {
929
- release();
930
- return;
931
- }
1417
+ const timer = setTimeout(guarded(() => {
932
1418
  release();
933
1419
  finish(new RelayTimeoutError("connection timed out", this.url));
934
1420
  if (gen === this.#gen) this.#handleSocketDeath("connection timed out", {
935
1421
  fromConnectAttempt: true,
936
1422
  gen
937
1423
  });
938
- }, timeoutMs);
1424
+ }), timeoutMs);
939
1425
  this.#connectTimer = timer;
940
1426
  try {
941
1427
  ws = new this.#WS(this.url);
@@ -949,36 +1435,24 @@ var Relay = class Relay {
949
1435
  return;
950
1436
  }
951
1437
  this.#ws = ws;
952
- const onOpen = () => {
953
- if (gen !== this.#gen) {
954
- release();
955
- return;
956
- }
1438
+ const onOpen = guarded(() => {
957
1439
  this.#setStatus(RelayStatus.Connected);
958
1440
  const wasReconnect = this.#reconnectAttempts > 0;
959
- this.#challenge = void 0;
960
- this.#authPromise = void 0;
961
- this.#authedChallenge = void 0;
962
- this.#answeredChallenge = void 0;
963
- this.#answeredResult = void 0;
964
- if (!resubscribeAll(this.#live)) {
1441
+ this.#authState.resetConnection();
1442
+ if (!this.#subscriptions.replayAll()) {
965
1443
  this.#setStatus(RelayStatus.Disconnected);
966
1444
  release();
967
1445
  finish(new RelayConnectionError("connection failed", this.url));
968
- if (this.#enableReconnect && !this.#manualStop && this.#subs.size > 0) this.#scheduleReconnect();
1446
+ if (this.#enableReconnect && !this.#manualStop && this.#subscriptions.size > 0) this.#scheduleReconnect();
969
1447
  return;
970
1448
  }
971
1449
  this.#reconnectAttempts = 0;
972
1450
  if (this.#enablePing) this.#ping.start();
973
1451
  else this.#ping.stop();
974
- if (wasReconnect) this.#emit("reconnect", void 0);
1452
+ if (wasReconnect) this.#events.emit("reconnect", void 0);
975
1453
  finish();
976
- };
977
- const onError = () => {
978
- if (gen !== this.#gen) {
979
- release();
980
- return;
981
- }
1454
+ });
1455
+ const onError = guarded(() => {
982
1456
  const fromConnectAttempt = !settled;
983
1457
  release();
984
1458
  finish(new RelayConnectionError("connection failed", this.url));
@@ -986,12 +1460,8 @@ var Relay = class Relay {
986
1460
  fromConnectAttempt,
987
1461
  gen
988
1462
  });
989
- };
990
- const onClose = () => {
991
- if (gen !== this.#gen) {
992
- release();
993
- return;
994
- }
1463
+ });
1464
+ const onClose = guarded(() => {
995
1465
  const fromConnectAttempt = !settled;
996
1466
  release();
997
1467
  if (fromConnectAttempt) finish(new RelayConnectionError("websocket closed", this.url));
@@ -999,7 +1469,7 @@ var Relay = class Relay {
999
1469
  fromConnectAttempt,
1000
1470
  gen
1001
1471
  });
1002
- };
1472
+ });
1003
1473
  const onMessage = (ev) => {
1004
1474
  if (gen !== this.#gen) return;
1005
1475
  const data = typeof ev === "object" && ev !== null && "data" in ev ? ev.data : ev;
@@ -1046,12 +1516,12 @@ var Relay = class Relay {
1046
1516
  this.#ping.stop();
1047
1517
  this.#connectFinish?.(new RelayClosedError("relay closed", this.url));
1048
1518
  try {
1049
- closeAllSubscriptions(this.#live, "relay closed");
1519
+ this.#subscriptions.closeAll("relay closed");
1050
1520
  } finally {
1051
1521
  this.#failPending(new RelayClosedError("relay closed", this.url));
1052
1522
  this.#detachSocketHandlers();
1053
1523
  this.#teardownSocket();
1054
- this.#emit("close", void 0);
1524
+ this.#events.emit("close", void 0);
1055
1525
  }
1056
1526
  }
1057
1527
  /**
@@ -1086,28 +1556,10 @@ var Relay = class Relay {
1086
1556
  this.#reconnectTimer = void 0;
1087
1557
  }
1088
1558
  }
1089
- #rejectPublishes(err) {
1090
- for (const [, waiter] of this.#publishes) {
1091
- clearTimeout(waiter.timer);
1092
- waiter.reject(err);
1093
- }
1094
- this.#publishes.clear();
1095
- }
1096
- #rejectCounts(err) {
1097
- for (const [, waiter] of this.#counts) {
1098
- clearTimeout(waiter.timer);
1099
- waiter.reject(err);
1100
- }
1101
- this.#counts.clear();
1102
- }
1103
- #rejectNeg(err) {
1104
- for (const session of this.#neg.values()) failNegSession(session, err);
1105
- this.#neg.clear();
1106
- }
1107
1559
  #failPending(err) {
1108
- this.#rejectPublishes(err);
1109
- this.#rejectCounts(err);
1110
- this.#rejectNeg(err);
1560
+ this.#ok.rejectAll(err);
1561
+ this.#countRequests.rejectAll(err);
1562
+ failAllNegSessions(this.#neg, err);
1111
1563
  }
1112
1564
  /** Unexpected socket death. Keep subscriptions if reconnecting. */
1113
1565
  #handleSocketDeath(reason, opts) {
@@ -1117,15 +1569,15 @@ var Relay = class Relay {
1117
1569
  this.#detachSocketHandlers();
1118
1570
  this.#ws = void 0;
1119
1571
  this.#failPending(new RelayClosedError(reason, this.url));
1120
- if (this.#enableReconnect && !this.#manualStop && this.#subs.size > 0) {
1572
+ if (this.#enableReconnect && !this.#manualStop && this.#subscriptions.size > 0) {
1121
1573
  this.#setStatus(RelayStatus.Disconnected);
1122
1574
  this.#scheduleReconnect();
1123
1575
  return;
1124
1576
  }
1125
1577
  this.#setStatus(RelayStatus.Disconnected);
1126
- if (opts.fromConnectAttempt !== true || this.#subs.size > 0) {
1127
- closeAllSubscriptions(this.#live, reason);
1128
- if (!this.#manualStop) this.#emit("close", void 0);
1578
+ if (opts.fromConnectAttempt !== true || this.#subscriptions.size > 0) {
1579
+ this.#subscriptions.closeAll(reason);
1580
+ if (!this.#manualStop) this.#events.emit("close", void 0);
1129
1581
  }
1130
1582
  this.#setStatus(RelayStatus.Closed);
1131
1583
  }
@@ -1141,7 +1593,7 @@ var Relay = class Relay {
1141
1593
  try {
1142
1594
  await this.connect();
1143
1595
  } catch {
1144
- if (this.#enableReconnect && !this.#manualStop && this.#subs.size > 0 && !this.#isOpen()) this.#scheduleReconnect();
1596
+ if (this.#enableReconnect && !this.#manualStop && this.#subscriptions.size > 0 && !this.#isOpen()) this.#scheduleReconnect();
1145
1597
  }
1146
1598
  })();
1147
1599
  }, delay);
@@ -1162,91 +1614,78 @@ var Relay = class Relay {
1162
1614
  case "EVENT": {
1163
1615
  const [, subId, event] = msg;
1164
1616
  if (this.#ping.hasWaiter(subId)) return;
1165
- onSubEvent(this.#live, subId, event);
1617
+ this.#subscriptions.handleEvent(subId, event);
1166
1618
  break;
1167
1619
  }
1168
1620
  case "EOSE": {
1169
1621
  const [, subId] = msg;
1170
1622
  if (this.#ping.finishDummyPing(subId)) return;
1171
- onSubEose(this.#live, subId);
1623
+ this.#subscriptions.handleEose(subId);
1172
1624
  break;
1173
1625
  }
1174
1626
  case "CLOSED": {
1175
1627
  const [, subId, reason] = msg;
1176
1628
  if (this.#ping.finishDummyPing(subId)) return;
1177
- const countWaiter = this.#counts.get(subId);
1629
+ const countWaiter = this.#countRequests.get(subId);
1178
1630
  if (countWaiter) {
1179
1631
  if (isAuthRequired(reason) && this.#authSigner && !countWaiter.authRetried) {
1180
1632
  countWaiter.authRetried = true;
1181
- this.#authThenRecount(subId, countWaiter, reason);
1633
+ this.#countRequests.authRetry(countWaiter, {
1634
+ ensureAuthed: this.#ensureAuthed,
1635
+ isOpen: () => this.#isOpen(),
1636
+ failError: () => new RelayClosedError(reason || "COUNT closed", this.url),
1637
+ send: () => this.#send([
1638
+ "COUNT",
1639
+ subId,
1640
+ ...countWaiter.filters
1641
+ ])
1642
+ });
1182
1643
  return;
1183
1644
  }
1184
1645
  countWaiter.reject(new RelayClosedError(reason || "COUNT closed", this.url));
1185
1646
  return;
1186
1647
  }
1187
- const sub = this.#subs.get(subId);
1648
+ const sub = this.#subscriptions.get(subId);
1188
1649
  if (!sub) return;
1189
- if (isAuthRequired(reason) && this.#authSigner && !sub.authRetried) {
1190
- sub.authRetried = true;
1191
- this.#authThenResubscribe(sub, reason);
1650
+ if (isAuthRequired(reason) && this.#authSigner && sub.beginAuthRetry()) {
1651
+ this.#subscriptions.authRetry(sub, reason, this.#ensureAuthed);
1192
1652
  return;
1193
1653
  }
1194
- dropSubscription(this.#live, sub, reason);
1654
+ this.#subscriptions.drop(sub, reason);
1195
1655
  break;
1196
1656
  }
1197
1657
  case "OK": {
1198
1658
  const [, eventId, ok, message] = msg;
1199
- const waiter = this.#publishes.get(eventId);
1659
+ const waiter = this.#ok.handleOk(eventId, ok, message, (w) => !ok && isAuthRequired(message) && w.event !== void 0 && !w.authRetried && this.#authSigner !== void 0);
1200
1660
  if (!waiter) return;
1201
- if (!ok && isAuthRequired(message) && waiter.event !== void 0 && waiter.authRetried !== true && this.#authSigner) {
1202
- waiter.authRetried = true;
1203
- this.#authThenRepublish(waiter, eventId, message);
1204
- return;
1205
- }
1206
- clearTimeout(waiter.timer);
1207
- this.#publishes.delete(eventId);
1208
- waiter.resolve({
1209
- ok,
1210
- message
1661
+ waiter.authRetried = true;
1662
+ this.#ok.authRetry(waiter, message, {
1663
+ ensureAuthed: this.#ensureAuthed,
1664
+ isOpen: () => this.#isOpen(),
1665
+ send: (event) => this.#send(["EVENT", event])
1211
1666
  });
1212
- break;
1667
+ return;
1213
1668
  }
1214
1669
  case "COUNT": {
1215
1670
  const [, countId, payload] = msg;
1216
- const waiter = this.#counts.get(countId);
1671
+ const waiter = this.#countRequests.get(countId);
1217
1672
  if (!waiter) return;
1218
1673
  waiter.resolve(payload);
1219
1674
  break;
1220
1675
  }
1221
- case "NEG-MSG": {
1222
- const [, negId, hex] = msg;
1223
- const session = this.#neg.get(negId);
1224
- if (!session) return;
1225
- pushNegMsg(session, hex);
1226
- break;
1227
- }
1228
- case "NEG-ERR": {
1229
- const [, negId, reason] = msg;
1230
- const session = this.#neg.get(negId);
1231
- if (!session) return;
1232
- failNegErr(session, reason);
1676
+ case "NEG-MSG":
1677
+ case "NEG-ERR":
1678
+ dispatchNegMessage(this.#neg, msg);
1233
1679
  break;
1234
- }
1235
1680
  case "NOTICE": {
1236
1681
  const [, notice] = msg;
1237
- this.#emit("notice", notice);
1682
+ this.#events.emit("notice", notice);
1238
1683
  break;
1239
1684
  }
1240
1685
  case "AUTH": {
1241
1686
  const [, authChallenge] = msg;
1242
- if (authChallenge !== this.#challenge) {
1243
- this.#authPromise = void 0;
1244
- this.#authedChallenge = void 0;
1245
- this.#answeredChallenge = void 0;
1246
- this.#answeredResult = void 0;
1247
- }
1248
- this.#challenge = authChallenge;
1249
- this.#emit("auth", authChallenge);
1687
+ this.#authState.handleChallenge(authChallenge);
1688
+ this.#events.emit("auth", authChallenge);
1250
1689
  break;
1251
1690
  }
1252
1691
  }
@@ -1256,19 +1695,18 @@ var Relay = class Relay {
1256
1695
  if (filters.length === 0) throw new MessageError("REQ requires at least one filter");
1257
1696
  if (!this.#isOpen() && !this.#enableReconnect) throw new RelayClosedError("not connected", this.url);
1258
1697
  const canonical = canonicalizeFilters(filters);
1259
- if (opts.closeOnEose === true) return openExclusive(this.#live, canonical, opts);
1260
- return subscribeLive(this.#live, canonical, opts);
1698
+ return this.#subscriptions.open(canonical, opts);
1261
1699
  }
1262
1700
  #acceptEvent(event) {
1263
1701
  if (this.#verifyDead) return false;
1264
1702
  try {
1265
1703
  const ok = this.#verify(event);
1266
- if (!ok) this.#emit("invalidevent", void 0);
1704
+ if (!ok) this.#events.emit("invalidevent", void 0);
1267
1705
  return ok;
1268
1706
  } catch (error) {
1269
1707
  if (error instanceof WasmPoisonedError) {
1270
1708
  this.#verifyDead = true;
1271
- this.#emit("notice", "wasm-poisoned: instance aborted");
1709
+ this.#events.emit("notice", "wasm-poisoned: instance aborted");
1272
1710
  return false;
1273
1711
  }
1274
1712
  throw error;
@@ -1299,35 +1737,16 @@ var Relay = class Relay {
1299
1737
  */
1300
1738
  async publish(event, opts) {
1301
1739
  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;
1740
+ const existing = this.#ok.inFlight(event.id);
1741
+ if (existing !== void 0) return existing;
1304
1742
  const timeoutMs = opts?.timeoutMs ?? this.#publishTimeoutMs;
1305
- const promise = new Promise((resolve, reject) => {
1306
- const waiter = {
1307
- resolve,
1308
- reject,
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),
1315
- event,
1316
- timeoutMs,
1317
- get promise() {
1318
- return promise;
1319
- }
1320
- };
1321
- this.#publishes.set(event.id, waiter);
1322
- try {
1323
- this.#send(["EVENT", event]);
1324
- } catch (error) {
1325
- clearTimeout(waiter.timer);
1326
- if (this.#publishes.get(event.id) === waiter) this.#publishes.delete(event.id);
1327
- reject(error instanceof Error ? error : new RelayPublishError("publish failed", this.url));
1328
- }
1743
+ return this.#ok.track(event.id, {
1744
+ timeoutMs,
1745
+ timeoutError: () => new RelayTimeoutError("publish timed out", this.url),
1746
+ sendError: () => new RelayPublishError("publish failed", this.url),
1747
+ event,
1748
+ send: () => this.#send(["EVENT", event])
1329
1749
  });
1330
- return promise;
1331
1750
  }
1332
1751
  /**
1333
1752
  * NIP-45 COUNT: ask the relay how many events match `filters`. Resolves with the COUNT payload
@@ -1340,105 +1759,22 @@ var Relay = class Relay {
1340
1759
  const canonical = canonicalizeFilters(filters);
1341
1760
  const id = opts?.id === void 0 ? this.nextSubId("count") : createSubscriptionId(opts.id);
1342
1761
  const timeoutMs = opts?.timeoutMs ?? this.#publishTimeoutMs;
1343
- return new Promise((resolve, reject) => {
1344
- const waiter = {
1345
- resolve: (result) => {
1346
- if (waiter.timer !== void 0) clearTimeout(waiter.timer);
1347
- waiter.timer = void 0;
1348
- this.#counts.delete(id);
1349
- cleanup();
1350
- resolve(result);
1351
- },
1352
- reject: (err) => {
1353
- if (waiter.timer !== void 0) clearTimeout(waiter.timer);
1354
- waiter.timer = void 0;
1355
- this.#counts.delete(id);
1356
- cleanup();
1357
- reject(err);
1358
- },
1359
- timer: void 0,
1360
- filters: [...canonical],
1361
- authRetried: false,
1362
- timeoutMs
1363
- };
1364
- const cleanup = onAbort(opts?.signal, () => {
1365
- if (opts?.signal) waiter.reject(abortReason(opts.signal));
1366
- });
1367
- waiter.timer = setTimeout(() => {
1368
- waiter.reject(new RelayTimeoutError("count timed out", this.url));
1369
- }, timeoutMs);
1370
- this.#counts.set(id, waiter);
1371
- try {
1372
- this.#send([
1373
- "COUNT",
1374
- id,
1375
- ...canonical
1376
- ]);
1377
- } catch (error) {
1378
- waiter.reject(error instanceof Error ? error : new RelayPublishError("count failed", this.url));
1379
- }
1762
+ return this.#countRequests.track(id, {
1763
+ timeoutMs,
1764
+ timeoutError: () => new RelayTimeoutError("count timed out", this.url),
1765
+ sendError: () => new RelayPublishError("count failed", this.url),
1766
+ filters: [...canonical],
1767
+ signal: opts?.signal,
1768
+ send: () => this.#send([
1769
+ "COUNT",
1770
+ id,
1771
+ ...canonical
1772
+ ])
1380
1773
  });
1381
1774
  }
1382
1775
  /** NIP-42 AUTH: sign the current challenge and wait for OK. */
1383
1776
  async auth(sign, opts) {
1384
- const challenge = this.#challenge;
1385
- if (challenge === void 0 || challenge === "") throw new RelayError("no AUTH challenge received from relay", this.url);
1386
- if (this.#authPromise) return this.#authPromise;
1387
- if (this.#answeredChallenge === challenge && this.#answeredResult !== void 0) return this.#answeredResult;
1388
- const pending = (async () => {
1389
- const template = makeAuthEvent(this.url, challenge);
1390
- let event;
1391
- try {
1392
- event = await sign(template);
1393
- } catch (error) {
1394
- if (error instanceof NoSignerError) return {
1395
- ok: false,
1396
- message: "auth: no signer"
1397
- };
1398
- throw error;
1399
- }
1400
- if (!this.#isOpen()) throw new RelayClosedError("not connected", this.url);
1401
- const timeoutMs = opts?.timeoutMs ?? this.#publishTimeoutMs;
1402
- const authPromise = new Promise((resolve, reject) => {
1403
- const waiter = {
1404
- resolve: (result) => {
1405
- if (this.#challenge === challenge) {
1406
- this.#answeredChallenge = challenge;
1407
- this.#answeredResult = result;
1408
- }
1409
- resolve(result);
1410
- },
1411
- reject,
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);
1424
- try {
1425
- this.#send(["AUTH", event]);
1426
- } catch (error) {
1427
- clearTimeout(waiter.timer);
1428
- if (this.#publishes.get(event.id) === waiter) this.#publishes.delete(event.id);
1429
- reject(error instanceof Error ? error : new RelayPublishError("auth send failed", this.url));
1430
- }
1431
- });
1432
- return authPromise;
1433
- })();
1434
- this.#authPromise = pending;
1435
- try {
1436
- const result = await pending;
1437
- if (result.ok && this.#challenge === challenge) this.#authedChallenge = challenge;
1438
- return result;
1439
- } finally {
1440
- if (this.#authPromise === pending) this.#authPromise = void 0;
1441
- }
1777
+ return this.#authState.auth(sign, opts);
1442
1778
  }
1443
1779
  /**
1444
1780
  * Clear a cached AUTH rejection for the current challenge so a later `auth()` signs again. A
@@ -1446,133 +1782,8 @@ var Relay = class Relay {
1446
1782
  * `auth` event is re-fired so the pool's automatic auth can run once more.
1447
1783
  */
1448
1784
  resetAuth() {
1449
- if (this.#answeredResult !== void 0 && !this.#answeredResult.ok) {
1450
- this.#answeredChallenge = void 0;
1451
- this.#answeredResult = void 0;
1452
- }
1453
- if (this.#challenge !== void 0 && this.#challenge !== this.#authedChallenge) {
1454
- const challenge = this.#challenge;
1455
- this.#emit("auth", challenge);
1456
- }
1457
- }
1458
- async #ensureAuthed() {
1459
- const signer = this.#authSigner;
1460
- if (!signer) return false;
1461
- for (let i = 0; i < 3; i++) {
1462
- if (this.#challenge === void 0 || this.#challenge === "") return false;
1463
- if (this.#authedChallenge === this.#challenge) return true;
1464
- const signed = this.#challenge;
1465
- const result = await this.auth(signer);
1466
- if (this.#authedChallenge === this.#challenge) return true;
1467
- if (!result.ok) {
1468
- if (this.#challenge === void 0 || this.#challenge === "" || this.#challenge === signed) return false;
1469
- continue;
1470
- }
1471
- }
1472
- return this.#authedChallenge === this.#challenge;
1473
- }
1474
- async #authThenResubscribe(sub, reason) {
1475
- try {
1476
- if (!await this.#ensureAuthed()) {
1477
- dropSubscription(this.#live, sub, reason);
1478
- return;
1479
- }
1480
- if (sub.closed || !this.#isOpen()) {
1481
- dropSubscription(this.#live, sub, reason);
1482
- return;
1483
- }
1484
- sub.eosed = false;
1485
- const group = this.#liveBySubId.get(sub.id);
1486
- if (group) for (const att of group.attachments) att.eosed = false;
1487
- this.#send([
1488
- "REQ",
1489
- sub.id,
1490
- ...sub.replayFilters()
1491
- ]);
1492
- } catch {
1493
- dropSubscription(this.#live, sub, reason);
1494
- }
1495
- }
1496
- async #authThenRecount(id, waiter, reason) {
1497
- if (waiter.timer !== void 0) {
1498
- clearTimeout(waiter.timer);
1499
- waiter.timer = void 0;
1500
- }
1501
- const fail = () => {
1502
- waiter.reject(new RelayClosedError(reason || "COUNT closed", this.url));
1503
- };
1504
- try {
1505
- const ok = await this.#ensureAuthed();
1506
- if (this.#counts.get(id) !== waiter) return;
1507
- if (!ok) {
1508
- fail();
1509
- return;
1510
- }
1511
- if (!this.#isOpen()) {
1512
- fail();
1513
- return;
1514
- }
1515
- waiter.timer = setTimeout(() => {
1516
- waiter.reject(new RelayTimeoutError("count timed out", this.url));
1517
- }, waiter.timeoutMs);
1518
- this.#send([
1519
- "COUNT",
1520
- id,
1521
- ...waiter.filters
1522
- ]);
1523
- } catch (error) {
1524
- if (!this.#counts.has(id)) return;
1525
- waiter.reject(error instanceof Error ? error : new RelayClosedError(reason || "COUNT closed", this.url));
1526
- }
1527
- }
1528
- async #authThenRepublish(waiter, eventId, message) {
1529
- if (waiter.timer !== void 0) {
1530
- clearTimeout(waiter.timer);
1531
- waiter.timer = void 0;
1532
- }
1533
- const finish = (result) => {
1534
- if (waiter.timer !== void 0) clearTimeout(waiter.timer);
1535
- if (this.#publishes.get(eventId) === waiter) this.#publishes.delete(eventId);
1536
- waiter.resolve(result);
1537
- };
1538
- try {
1539
- if (!waiter.event) {
1540
- finish({
1541
- ok: false,
1542
- message
1543
- });
1544
- return;
1545
- }
1546
- const ok = await this.#ensureAuthed();
1547
- if (this.#publishes.get(eventId) !== waiter) return;
1548
- if (!ok) {
1549
- finish({
1550
- ok: false,
1551
- message
1552
- });
1553
- return;
1554
- }
1555
- if (!this.#isOpen()) {
1556
- finish({
1557
- ok: false,
1558
- message
1559
- });
1560
- return;
1561
- }
1562
- waiter.timer = setTimeout(() => {
1563
- if (this.#publishes.get(eventId) === waiter) {
1564
- this.#publishes.delete(eventId);
1565
- waiter.reject(new RelayTimeoutError("publish timed out", this.url));
1566
- }
1567
- }, waiter.timeoutMs);
1568
- this.#send(["EVENT", waiter.event]);
1569
- } catch {
1570
- if (!this.#publishes.has(eventId)) return;
1571
- finish({
1572
- ok: false,
1573
- message
1574
- });
1575
- }
1785
+ const challenge = this.#authState.resetRejection();
1786
+ if (challenge !== void 0) this.#events.emit("auth", challenge);
1576
1787
  }
1577
1788
  /**
1578
1789
  * NIP-77: run Negentropy reconciliation against this relay. Returns the local-only (`have`) and
@@ -1584,32 +1795,17 @@ var Relay = class Relay {
1584
1795
  async negReconcile(filter, storage, opts) {
1585
1796
  if (!this.#isOpen()) throw new RelayClosedError("not connected", this.url);
1586
1797
  throwIfAborted(opts?.signal);
1587
- const canonical = canonicalizeFilter(filter);
1588
1798
  const id = opts?.id === void 0 ? this.nextSubId("neg") : assertSubscriptionId(opts.id);
1589
- const timeoutMs = opts?.timeoutMs ?? this.#publishTimeoutMs;
1590
- const prev = this.#neg.get(id);
1591
- if (prev) failNegSession(prev, new Nip77Error("closed: replaced by new NEG-OPEN"));
1592
- const session = createNegSession();
1593
- this.#neg.set(id, session);
1594
- try {
1595
- return await runWiredNegSession({
1596
- session,
1597
- storage,
1598
- filter: canonical,
1599
- id,
1600
- timeoutMs,
1601
- signal: opts?.signal,
1602
- send: (message) => this.#send(message),
1603
- url: this.url
1604
- });
1605
- } finally {
1606
- if (this.#neg.get(id) === session) {
1607
- this.#neg.delete(id);
1608
- try {
1609
- this.#send(["NEG-CLOSE", id]);
1610
- } catch {}
1611
- }
1612
- }
1799
+ return runTrackedNegSession({
1800
+ sessions: this.#neg,
1801
+ id,
1802
+ storage,
1803
+ filter: canonicalizeFilter(filter),
1804
+ timeoutMs: opts?.timeoutMs ?? this.#publishTimeoutMs,
1805
+ signal: opts?.signal,
1806
+ send: (message) => this.#send(message),
1807
+ url: this.url
1808
+ });
1613
1809
  }
1614
1810
  /** Generate a unique subscription id for this relay instance. */
1615
1811
  nextSubId(prefix = "sub") {
@@ -1620,4 +1816,4 @@ var Relay = class Relay {
1620
1816
  //#endregion
1621
1817
  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 };
1622
1818
 
1623
- //# sourceMappingURL=relay-CvxPiQ_m.mjs.map
1819
+ //# sourceMappingURL=relay-__QtO33h.mjs.map