@qntx/nostr 0.7.0 → 0.8.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 (147) hide show
  1. package/dist/{builder-DgkqDylK.d.mts → builder-CTwkB2zM.d.mts} +10 -10
  2. package/dist/{builder-CdLE4J6O.mjs → builder-aNU2gGFP.mjs} +4 -4
  3. package/dist/builder-aNU2gGFP.mjs.map +1 -0
  4. package/dist/{client-CILhdfsF.mjs → client-Cj2lUaVh.mjs} +70 -72
  5. package/dist/client-Cj2lUaVh.mjs.map +1 -0
  6. package/dist/client.d.mts +1 -1
  7. package/dist/client.mjs +1 -1
  8. package/dist/core.d.mts +2 -2
  9. package/dist/core.mjs +6 -6
  10. package/dist/emitter-DkCY6NkU.mjs +32 -0
  11. package/dist/emitter-DkCY6NkU.mjs.map +1 -0
  12. package/dist/{error-L_QdmSnV.mjs → error-BILMe3Gr.mjs} +2 -2
  13. package/dist/{error-L_QdmSnV.mjs.map → error-BILMe3Gr.mjs.map} +1 -1
  14. package/dist/{error-gtJvV6YS.mjs → error-DlhGW4mJ.mjs} +2 -2
  15. package/dist/{error-gtJvV6YS.mjs.map → error-DlhGW4mJ.mjs.map} +1 -1
  16. package/dist/{event-DinQMmJ1.mjs → event-CHf47dPM.mjs} +2 -2
  17. package/dist/{event-DinQMmJ1.mjs.map → event-CHf47dPM.mjs.map} +1 -1
  18. package/dist/{gossip-CcTp9VpU.mjs → gossip-DFxIBlsr.mjs} +3 -3
  19. package/dist/gossip-DFxIBlsr.mjs.map +1 -0
  20. package/dist/{gossip-C7HLW5kI.d.mts → gossip-OOeeFzG5.d.mts} +4 -4
  21. package/dist/gossip.d.mts +1 -1
  22. package/dist/gossip.mjs +1 -1
  23. package/dist/{index-InQZpHyQ.d.mts → index-BxCBX-8R.d.mts} +2 -2
  24. package/dist/{index-iLT8UzY3.d.mts → index-CPVEyzNX.d.mts} +22 -22
  25. package/dist/{index-Tx15HGAD.d.mts → index-C_w2WfVz.d.mts} +4 -4
  26. package/dist/{index-DocgD9K0.d.mts → index-Cl_1XdYP.d.mts} +29 -23
  27. package/dist/{index-DiXwNn2M.d.mts → index-DyKwQnbt.d.mts} +2 -2
  28. package/dist/index.d.mts +14 -14
  29. package/dist/index.mjs +17 -17
  30. package/dist/{instance-BcKzxHIP.mjs → instance-C62w3cEt.mjs} +3 -3
  31. package/dist/{instance-BcKzxHIP.mjs.map → instance-C62w3cEt.mjs.map} +1 -1
  32. package/dist/{key-bshqfmZL.mjs → key-DnVpchl6.mjs} +3 -3
  33. package/dist/{key-bshqfmZL.mjs.map → key-DnVpchl6.mjs.map} +1 -1
  34. package/dist/{loaders-DD0XAGzw.mjs → loaders-BmPkQfzI.mjs} +6 -6
  35. package/dist/loaders-BmPkQfzI.mjs.map +1 -0
  36. package/dist/loaders.d.mts +2 -2
  37. package/dist/loaders.mjs +1 -1
  38. package/dist/{memory-D_-5-T31.mjs → memory-CdJ07kub.mjs} +2 -2
  39. package/dist/memory-CdJ07kub.mjs.map +1 -0
  40. package/dist/{memory-index-C6rujtoc.mjs → memory-index-Cbc83vOZ.mjs} +3 -3
  41. package/dist/{memory-index-C6rujtoc.mjs.map → memory-index-Cbc83vOZ.mjs.map} +1 -1
  42. package/dist/{message-CWLcFTQ1.mjs → message-oNr7t0SJ.mjs} +3 -3
  43. package/dist/message-oNr7t0SJ.mjs.map +1 -0
  44. package/dist/{message-CbBZOhq3.d.mts → message-tclYCVNc.d.mts} +4 -4
  45. package/dist/{nip17-cTDFfFVt.d.mts → nip17-B5WgW4C4.d.mts} +4 -4
  46. package/dist/{nip19-D1OnJ192.d.mts → nip19-DpPN17bY.d.mts} +23 -23
  47. package/dist/{nip46-DrsPI8EX.d.mts → nip46-BHaxC9TZ.d.mts} +17 -17
  48. package/dist/{nip51-DoPUvQ--.d.mts → nip51-CR0Q8eym.d.mts} +10 -10
  49. package/dist/{nip59-BFIihLYY.d.mts → nip59-C-RWFQIv.d.mts} +9 -2
  50. package/dist/{nip65-52HiwiyD.d.mts → nip65-D1fbmhOg.d.mts} +2 -2
  51. package/dist/{nip77-C9QcZn7b.d.mts → nip77-DzUwUxHq.d.mts} +6 -6
  52. package/dist/nips/blossom.d.mts +6 -6
  53. package/dist/nips/blossom.mjs +2 -2
  54. package/dist/nips/blossom.mjs.map +1 -1
  55. package/dist/nips/nip04.mjs +1 -1
  56. package/dist/nips/nip05.d.mts +8 -8
  57. package/dist/nips/nip05.mjs +1 -1
  58. package/dist/nips/nip05.mjs.map +1 -1
  59. package/dist/nips/nip10.d.mts +7 -7
  60. package/dist/nips/nip10.mjs +34 -32
  61. package/dist/nips/nip10.mjs.map +1 -1
  62. package/dist/nips/nip11.d.mts +27 -27
  63. package/dist/nips/nip11.mjs +1 -1
  64. package/dist/nips/nip11.mjs.map +1 -1
  65. package/dist/nips/nip13.mjs +1 -1
  66. package/dist/nips/nip17.d.mts +1 -1
  67. package/dist/nips/nip17.mjs +6 -4
  68. package/dist/nips/nip17.mjs.map +1 -1
  69. package/dist/nips/nip19.d.mts +1 -1
  70. package/dist/nips/nip19.mjs +1 -1
  71. package/dist/nips/nip19.mjs.map +1 -1
  72. package/dist/nips/nip21.d.mts +4 -4
  73. package/dist/nips/nip21.mjs +1 -1
  74. package/dist/nips/nip21.mjs.map +1 -1
  75. package/dist/nips/nip27.d.mts +19 -19
  76. package/dist/nips/nip27.mjs +2 -2
  77. package/dist/nips/nip27.mjs.map +1 -1
  78. package/dist/nips/nip42.mjs +1 -1
  79. package/dist/nips/nip44.mjs +2 -2
  80. package/dist/nips/nip44.mjs.map +1 -1
  81. package/dist/nips/nip46.d.mts +1 -1
  82. package/dist/nips/nip46.mjs +1 -1
  83. package/dist/nips/nip46.mjs.map +1 -1
  84. package/dist/nips/nip49.d.mts +4 -4
  85. package/dist/nips/nip49.mjs +1 -1
  86. package/dist/nips/nip49.mjs.map +1 -1
  87. package/dist/nips/nip51.d.mts +1 -1
  88. package/dist/nips/nip51.mjs +3 -3
  89. package/dist/nips/nip51.mjs.map +1 -1
  90. package/dist/nips/nip57.d.mts +18 -18
  91. package/dist/nips/nip57.mjs +3 -3
  92. package/dist/nips/nip57.mjs.map +1 -1
  93. package/dist/nips/nip59.d.mts +1 -1
  94. package/dist/nips/nip59.mjs +15 -7
  95. package/dist/nips/nip59.mjs.map +1 -1
  96. package/dist/nips/nip65.d.mts +1 -1
  97. package/dist/nips/nip65.mjs +2 -2
  98. package/dist/nips/nip77.d.mts +1 -1
  99. package/dist/nips/nip77.mjs +1 -1
  100. package/dist/nips/nip77.mjs.map +1 -1
  101. package/dist/nips/nip98.mjs +3 -3
  102. package/dist/{pool-SzJKPIa3.d.mts → pool-CqQvUnsq.d.mts} +43 -24
  103. package/dist/{pool-YHRZgcQ3.mjs → pool-vlw74C4t.mjs} +39 -29
  104. package/dist/pool-vlw74C4t.mjs.map +1 -0
  105. package/dist/{put-DS0yzVJx.mjs → put-w13ci3sC.mjs} +3 -3
  106. package/dist/put-w13ci3sC.mjs.map +1 -0
  107. package/dist/{reactive-BJRoVHxU.mjs → reactive-Tz0otDIP.mjs} +49 -27
  108. package/dist/reactive-Tz0otDIP.mjs.map +1 -0
  109. package/dist/{reactive-BJ1ggn4d.d.mts → reactive-gGqUrzW8.d.mts} +16 -7
  110. package/dist/{relay-CvxPiQ_m.mjs → relay-quCs7MHD.mjs} +1049 -861
  111. package/dist/relay-quCs7MHD.mjs.map +1 -0
  112. package/dist/relay.d.mts +4 -4
  113. package/dist/relay.mjs +2 -2
  114. package/dist/{signer-yW3slDC2.mjs → signer-poTGe6Er.mjs} +6 -6
  115. package/dist/signer-poTGe6Er.mjs.map +1 -0
  116. package/dist/signer.d.mts +1 -1
  117. package/dist/signer.mjs +2 -2
  118. package/dist/{storage-CZ52ifcQ.mjs → storage-B5IXTVuS.mjs} +16 -14
  119. package/dist/storage-B5IXTVuS.mjs.map +1 -0
  120. package/dist/storage.d.mts +2 -2
  121. package/dist/storage.mjs +4 -4
  122. package/dist/store.d.mts +2 -2
  123. package/dist/store.mjs +1 -1
  124. package/dist/testing.d.mts +3 -3
  125. package/dist/testing.mjs +6 -6
  126. package/dist/testing.mjs.map +1 -1
  127. package/dist/{types-Cm-XMgtH.d.mts → types-BacwnDtU.d.mts} +5 -5
  128. package/dist/{util-yFYzlbrm.mjs → util-DMF9AIO0.mjs} +6 -2
  129. package/dist/util-DMF9AIO0.mjs.map +1 -0
  130. package/dist/{verifier-BTLOpo1n.mjs → verifier-B51Wkt5H.mjs} +3 -3
  131. package/dist/{verifier-BTLOpo1n.mjs.map → verifier-B51Wkt5H.mjs.map} +1 -1
  132. package/dist/wasm.browser.mjs +2 -2
  133. package/dist/wasm.mjs +2 -2
  134. package/package.json +1 -1
  135. package/dist/builder-CdLE4J6O.mjs.map +0 -1
  136. package/dist/client-CILhdfsF.mjs.map +0 -1
  137. package/dist/gossip-CcTp9VpU.mjs.map +0 -1
  138. package/dist/loaders-DD0XAGzw.mjs.map +0 -1
  139. package/dist/memory-D_-5-T31.mjs.map +0 -1
  140. package/dist/message-CWLcFTQ1.mjs.map +0 -1
  141. package/dist/pool-YHRZgcQ3.mjs.map +0 -1
  142. package/dist/put-DS0yzVJx.mjs.map +0 -1
  143. package/dist/reactive-BJRoVHxU.mjs.map +0 -1
  144. package/dist/relay-CvxPiQ_m.mjs.map +0 -1
  145. package/dist/signer-yW3slDC2.mjs.map +0 -1
  146. package/dist/storage-CZ52ifcQ.mjs.map +0 -1
  147. package/dist/util-yFYzlbrm.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";
1
+ import { _ as MessageError, b as WasmPoisonedError, u as normalizeURL, v as NostrError } from "./util-DMF9AIO0.mjs";
2
+ import { n as verifyEvent } from "./verifier-B51Wkt5H.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-CWLcFTQ1.mjs";
5
- import { t as NoSignerError } from "./error-gtJvV6YS.mjs";
4
+ import { n as createSubscriptionId, r as encodeClientMessage, s as parseRelayMessage, t as assertSubscriptionId } from "./message-oNr7t0SJ.mjs";
5
+ import { t as NoSignerError } from "./error-DlhGW4mJ.mjs";
6
6
  import { t as invokeSafely } from "./report-CHld4uWj.mjs";
7
7
  import { isAuthRequired, makeAuthEvent } from "./nips/nip42.mjs";
8
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,513 @@ 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
+ /** Advance the reconnect watermark after a verified EVENT. */
398
+ noteVerified(event) {
399
+ if (this.#lastCreatedAt === void 0 || event.created_at > this.#lastCreatedAt) {
400
+ this.#lastCreatedAt = event.created_at;
401
+ this.#idsAtWatermark.clear();
402
+ this.#idsAtWatermark.add(event.id);
403
+ return;
404
+ }
405
+ if (event.created_at === this.#lastCreatedAt) this.#idsAtWatermark.add(event.id);
406
+ }
407
+ /**
408
+ * Filters for re-REQ. Original `filters` stay unchanged. NIP-01 `since` is inclusive — never
409
+ * `lastCreatedAt + 1`.
410
+ */
411
+ replayFilters() {
412
+ const since = this.#lastCreatedAt;
413
+ if (since === void 0) return [...this.filters];
414
+ return this.filters.map((f) => ({
415
+ ...f,
416
+ since: f.since === void 0 ? since : Math.max(f.since, since)
417
+ }));
418
+ }
419
+ };
420
+ /** AsyncIterable wrapper over a subscription's events until EOSE or close. */
421
+ function subscriptionToAsyncIterable(start, opts) {
422
+ const queue = [];
423
+ let head = 0;
424
+ let done = false;
425
+ let error;
426
+ let wake;
427
+ let closer;
428
+ let localClose = false;
429
+ let disposeAbort;
430
+ const notify = () => {
431
+ wake?.();
432
+ wake = void 0;
433
+ };
434
+ const waitForWake = async () => new Promise((resolve) => {
435
+ wake = resolve;
436
+ });
437
+ const closeLocal = (reason) => {
438
+ localClose = true;
439
+ disposeAbort?.();
440
+ closer?.close(reason);
441
+ done = true;
442
+ notify();
443
+ };
444
+ closer = start({
445
+ onevent(event) {
446
+ queue.push(event);
447
+ notify();
448
+ },
449
+ oneose() {
450
+ if (opts?.includeEose === false) closeLocal("eose");
451
+ },
452
+ onclose(reason) {
453
+ if (opts?.signal?.aborted === true) localClose = true;
454
+ if (!localClose && reason) error = new RelayClosedError(reason);
455
+ done = true;
456
+ notify();
457
+ }
458
+ });
459
+ if (opts?.signal) {
460
+ if (opts.signal.aborted) closeLocal("aborted");
461
+ else {
462
+ disposeAbort = onAbort(opts.signal, () => closeLocal("aborted"));
463
+ if (done) disposeAbort();
464
+ }
465
+ }
466
+ return {
467
+ close(reason) {
468
+ closeLocal(reason ?? "closed by client");
469
+ },
470
+ [Symbol.asyncIterator]() {
471
+ return {
472
+ async next() {
473
+ while (true) {
474
+ const value = head < queue.length ? queue[head] : void 0;
475
+ if (value !== void 0) {
476
+ head += 1;
477
+ if (head === queue.length) {
478
+ queue.length = 0;
479
+ head = 0;
480
+ }
481
+ return {
482
+ value,
483
+ done: false
484
+ };
485
+ }
486
+ if (error !== void 0) throw error;
487
+ if (done) return {
488
+ value: void 0,
489
+ done: true
490
+ };
491
+ await waitForWake();
492
+ }
493
+ },
494
+ async return() {
495
+ closeLocal("iterator returned");
496
+ return {
497
+ value: void 0,
498
+ done: true
499
+ };
500
+ }
501
+ };
502
+ }
503
+ };
504
+ }
505
+ //#endregion
506
+ //#region src/relay/fetch.ts
507
+ function streamFilters(subscribe, filters, opts) {
508
+ return subscriptionToAsyncIterable((handlers) => subscribe(filters, {
509
+ ...handlers,
510
+ id: opts?.id,
511
+ signal: opts?.signal
512
+ }), { signal: opts?.signal });
513
+ }
514
+ async function fetchFilters(subscribe, filters, opts) {
515
+ throwIfAborted(opts.signal);
516
+ const events = [];
517
+ const seen = /* @__PURE__ */ new Set();
518
+ let end = { type: "timeout" };
519
+ await new Promise((resolve, reject) => {
520
+ let settled = false;
521
+ const done = (err) => {
522
+ if (settled) return;
523
+ settled = true;
524
+ disposeAbort();
525
+ clearTimeout(timer);
526
+ let reason = "aborted";
527
+ if (err instanceof Error) reason = err.message;
528
+ else if (err === void 0) reason = "fetch complete";
529
+ sub.close(reason);
530
+ if (err === void 0) resolve();
531
+ else reject(err);
532
+ };
533
+ const disposeAbort = onAbort(opts.signal, () => {
534
+ if (opts.signal !== void 0) done(abortReason(opts.signal));
535
+ });
536
+ const timer = setTimeout(() => {
537
+ end = { type: "timeout" };
538
+ done();
539
+ }, opts.timeoutMs);
540
+ const sub = subscribe(filters, {
541
+ id: opts.id,
542
+ signal: opts.signal,
543
+ closeOnEose: true,
544
+ onevent(event) {
545
+ if (seen.has(event.id)) return;
546
+ seen.add(event.id);
547
+ events.push(event);
548
+ },
549
+ oneose() {
550
+ end = { type: "eose" };
551
+ done();
552
+ },
553
+ onclose(reason) {
554
+ if (settled) return;
555
+ if (opts.signal?.aborted === true) {
556
+ done(abortReason(opts.signal));
557
+ return;
558
+ }
559
+ end = {
560
+ type: "closed",
561
+ reason
562
+ };
563
+ done();
564
+ }
565
+ });
566
+ });
567
+ return {
568
+ events,
569
+ end
570
+ };
571
+ }
572
+ //#endregion
65
573
  //#region src/relay/neg-session.ts
66
574
  function createNegSession() {
67
575
  return {
@@ -88,6 +596,21 @@ function failNegSession(session, err) {
88
596
  function failNegErr(session, reason) {
89
597
  failNegSession(session, new Nip77Error(reason));
90
598
  }
599
+ /** Route a NEG-MSG/NEG-ERR frame to its session; unknown ids are ignored. */
600
+ function dispatchNegMessage(sessions, msg) {
601
+ const session = sessions.get(msg[1]);
602
+ if (session === void 0) return;
603
+ if (msg[0] === "NEG-MSG") {
604
+ pushNegMsg(session, msg[2]);
605
+ return;
606
+ }
607
+ failNegErr(session, msg[2]);
608
+ }
609
+ /** Fail every tracked session (socket teardown). */
610
+ function failAllNegSessions(sessions, err) {
611
+ for (const session of sessions.values()) failNegSession(session, err);
612
+ sessions.clear();
613
+ }
91
614
  /** Wire queue/timeout/abort around `runNegSession`. Not a second session class. */
92
615
  async function runWiredNegSession(opts) {
93
616
  const { session, storage, filter, id, timeoutMs, signal, send, url } = opts;
@@ -144,6 +667,175 @@ async function runWiredNegSession(opts) {
144
667
  next
145
668
  });
146
669
  }
670
+ /**
671
+ * Drive a NIP-77 reconcile session against the session map: fail a same-id predecessor, register,
672
+ * wire queue/timeout/abort, and send NEG-CLOSE on exit.
673
+ */
674
+ async function runTrackedNegSession(opts) {
675
+ const { sessions, id, send } = opts;
676
+ const prev = sessions.get(id);
677
+ if (prev) failNegSession(prev, new Nip77Error("closed: replaced by new NEG-OPEN"));
678
+ const session = createNegSession();
679
+ sessions.set(id, session);
680
+ try {
681
+ return await runWiredNegSession({
682
+ session,
683
+ storage: opts.storage,
684
+ filter: opts.filter,
685
+ id,
686
+ timeoutMs: opts.timeoutMs,
687
+ signal: opts.signal,
688
+ send,
689
+ url: opts.url
690
+ });
691
+ } finally {
692
+ if (sessions.get(id) === session) {
693
+ sessions.delete(id);
694
+ try {
695
+ send(["NEG-CLOSE", id]);
696
+ } catch {}
697
+ }
698
+ }
699
+ }
700
+ //#endregion
701
+ //#region src/relay/ok-tracker.ts
702
+ var OkTracker = class {
703
+ #entries = /* @__PURE__ */ new Map();
704
+ get size() {
705
+ return this.#entries.size;
706
+ }
707
+ get(eventId) {
708
+ return this.#entries.get(eventId);
709
+ }
710
+ /** Joiner's view: only publish waiters (carrying an `event`) are joinable. */
711
+ inFlight(eventId) {
712
+ const waiter = this.#entries.get(eventId);
713
+ return waiter?.event === void 0 ? void 0 : waiter.promise;
714
+ }
715
+ /**
716
+ * Register a waiter, arm its timeout and call `send`. A send throw removes the entry and rejects
717
+ * (`sendError` when the throw is not an Error). The timer only rejects while this waiter still
718
+ * owns the id.
719
+ */
720
+ async track(eventId, opts) {
721
+ const promise = new Promise((resolve, reject) => {
722
+ const waiter = {
723
+ id: eventId,
724
+ resolve,
725
+ reject,
726
+ timer: setTimeout(() => {
727
+ if (this.#entries.get(eventId) === waiter) {
728
+ this.#entries.delete(eventId);
729
+ reject(opts.timeoutError());
730
+ }
731
+ }, opts.timeoutMs),
732
+ event: opts.event,
733
+ authRetried: false,
734
+ timeoutMs: opts.timeoutMs,
735
+ timeoutError: opts.timeoutError,
736
+ get promise() {
737
+ return promise;
738
+ }
739
+ };
740
+ this.#entries.set(eventId, waiter);
741
+ try {
742
+ opts.send();
743
+ } catch (error) {
744
+ this.#finish(waiter);
745
+ reject(error instanceof Error ? error : opts.sendError());
746
+ }
747
+ });
748
+ return promise;
749
+ }
750
+ /**
751
+ * An OK reply arrived: `retry` is the caller's auth-required policy — a kept waiter is returned
752
+ * unsettled for the retry path; otherwise the entry is settled and undefined is returned.
753
+ */
754
+ handleOk(eventId, ok, message, retry) {
755
+ const waiter = this.#entries.get(eventId);
756
+ if (waiter === void 0) return;
757
+ if (!retry(waiter)) {
758
+ this.settle(waiter, {
759
+ ok,
760
+ message
761
+ });
762
+ return;
763
+ }
764
+ return waiter;
765
+ }
766
+ /** Settle a waiter: stop its timer, drop the entry it still owns, then resolve. */
767
+ settle(waiter, result) {
768
+ this.#finish(waiter);
769
+ waiter.resolve(result);
770
+ }
771
+ /** Stop a waiter's timeout without settling (AUTH retry window). */
772
+ clearTimer(waiter) {
773
+ if (waiter.timer !== void 0) {
774
+ clearTimeout(waiter.timer);
775
+ waiter.timer = void 0;
776
+ }
777
+ }
778
+ /** Re-arm the timeout for a re-sent frame (after an AUTH retry). */
779
+ restartTimer(eventId) {
780
+ const waiter = this.#entries.get(eventId);
781
+ if (waiter === void 0) return;
782
+ waiter.timer = setTimeout(() => {
783
+ if (this.#entries.get(eventId) === waiter) {
784
+ this.#entries.delete(eventId);
785
+ waiter.reject(waiter.timeoutError());
786
+ }
787
+ }, waiter.timeoutMs);
788
+ }
789
+ /**
790
+ * Re-send a tracked EVENT after the relay demanded AUTH: pause the timeout through the AUTH
791
+ * window, re-arm it and re-send on success, settle `{ok:false,message}` on failure. AUTH waiters
792
+ * (no `event`) settle immediately.
793
+ */
794
+ async authRetry(waiter, message, deps) {
795
+ this.clearTimer(waiter);
796
+ const finish = (result) => {
797
+ this.settle(waiter, result);
798
+ };
799
+ try {
800
+ const { event } = waiter;
801
+ if (event === void 0) {
802
+ finish({
803
+ ok: false,
804
+ message
805
+ });
806
+ return;
807
+ }
808
+ const ok = await deps.ensureAuthed();
809
+ if (this.#entries.get(waiter.id) !== waiter) return;
810
+ if (!ok || !deps.isOpen()) {
811
+ finish({
812
+ ok: false,
813
+ message
814
+ });
815
+ return;
816
+ }
817
+ this.restartTimer(waiter.id);
818
+ deps.send(event);
819
+ } catch {
820
+ if (this.#entries.get(waiter.id) === void 0) return;
821
+ finish({
822
+ ok: false,
823
+ message
824
+ });
825
+ }
826
+ }
827
+ rejectAll(err) {
828
+ for (const waiter of this.#entries.values()) {
829
+ this.clearTimer(waiter);
830
+ waiter.reject(err);
831
+ }
832
+ this.#entries.clear();
833
+ }
834
+ #finish(waiter) {
835
+ this.clearTimer(waiter);
836
+ if (this.#entries.get(waiter.id) === waiter) this.#entries.delete(waiter.id);
837
+ }
838
+ };
147
839
  const PING_DUMMY_FILTER = {
148
840
  ids: ["a".repeat(64)],
149
841
  limit: 0
@@ -266,458 +958,252 @@ var PingLoop = class {
266
958
  }
267
959
  };
268
960
  //#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
- }
961
+ //#region src/relay/subscription-registry.ts
962
+ /**
963
+ * Owns every open REQ of a relay: exclusive subscriptions keyed by id, and live coalescing groups
964
+ * (one wire REQ plus per-subscriber handles) keyed by fingerprint and wire id.
965
+ */
966
+ var SubscriptionRegistry = class {
967
+ #subs = /* @__PURE__ */ new Map();
968
+ #liveByFp = /* @__PURE__ */ new Map();
969
+ #liveBySubId = /* @__PURE__ */ new Map();
970
+ #isOpen;
971
+ #enableReconnect;
972
+ #send;
973
+ #scheduleReconnect;
974
+ #acceptEvent;
975
+ constructor(deps) {
976
+ this.#isOpen = deps.isOpen;
977
+ this.#enableReconnect = deps.enableReconnect;
978
+ this.#send = deps.send;
979
+ this.#scheduleReconnect = deps.scheduleReconnect;
980
+ this.#acceptEvent = deps.acceptEvent;
301
981
  }
302
- /** Drop the signal listener. Terminal paths that mark `closed` without `close()` run this too. */
303
- dispose() {
304
- this.#abort?.();
982
+ /** User subscriptions only; dummy ping REQs are not counted. */
983
+ get size() {
984
+ return this.#subs.size;
305
985
  }
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));
986
+ get(subId) {
987
+ return this.#subs.get(subId);
312
988
  }
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);
989
+ /** Exclusive one-shot REQ when `closeOnEose`, live coalescing otherwise. */
990
+ open(filters, opts) {
991
+ if (opts.closeOnEose === true) return this.#openExclusive(filters, opts);
992
+ return this.#openLive(filters, opts);
993
+ }
994
+ handleEvent(subId, event) {
995
+ const sub = this.#subs.get(subId);
996
+ if (!sub || sub.closed) return;
997
+ const group = this.#liveBySubId.get(subId);
998
+ if (group) {
999
+ this.#deliverLiveEvent(group, event);
319
1000
  return;
320
1001
  }
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();
1002
+ sub.notifyReceived(event.id);
1003
+ if (sub.isAtWatermark(event.id)) return;
1004
+ if (sub.alreadyHas(event.id)) return;
1005
+ if (!this.#acceptEvent(event)) return;
1006
+ sub.deliver(event);
1007
+ }
1008
+ handleEose(subId) {
1009
+ const sub = this.#subs.get(subId);
1010
+ if (!sub || sub.closed || sub.eosed) return;
1011
+ const group = this.#liveBySubId.get(subId);
1012
+ if (group) {
1013
+ sub.markEose();
1014
+ this.#deliverLiveEose(group);
1015
+ return;
372
1016
  }
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();
1017
+ sub.markEose();
1018
+ }
1019
+ /** Remote/transport termination of a REQ (CLOSED, auth-retry failure). */
1020
+ drop(sub, reason) {
1021
+ const group = this.#liveBySubId.get(sub.id);
1022
+ if (group) {
1023
+ this.#endLiveGroup(group, {
1024
+ sendClose: false,
1025
+ reason
1026
+ });
1027
+ return;
379
1028
  }
1029
+ this.#subs.delete(sub.id);
1030
+ sub.end(reason);
380
1031
  }
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);
1032
+ /**
1033
+ * Re-REQ after the relay demanded AUTH: drop the subscription when auth fails or the
1034
+ * socket/subscription died during the AUTH window, replay the REQ on success.
1035
+ */
1036
+ async authRetry(sub, reason, ensureAuthed) {
419
1037
  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;
1038
+ if (!await ensureAuthed() || sub.closed || !this.#isOpen()) {
1039
+ this.drop(sub, reason);
1040
+ return;
1041
+ }
1042
+ this.replay(sub);
1043
+ } catch {
1044
+ this.drop(sub, reason);
1045
+ }
472
1046
  }
473
- group.attachments.add(handle);
474
- if (created) {
475
- if (ctx.connected()) ctx.send([
1047
+ /** Re-REQ after AUTH: rearm the wire and its handles, then send the replayed filters. */
1048
+ replay(sub) {
1049
+ sub.rearm();
1050
+ const group = this.#liveBySubId.get(sub.id);
1051
+ if (group) for (const att of group.attachments) att.rearm();
1052
+ this.#send([
476
1053
  "REQ",
477
- group.sub.id,
478
- ...filters
1054
+ sub.id,
1055
+ ...sub.replayFilters()
479
1056
  ]);
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
1057
  }
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;
1058
+ /** Reconnect path: rearm every open REQ and re-send; false on send failure. */
1059
+ replayAll() {
1060
+ for (const sub of this.#subs.values()) {
1061
+ if (sub.closed) continue;
1062
+ sub.rearm();
1063
+ sub.resetAuthRetry();
1064
+ const group = this.#liveBySubId.get(sub.id);
1065
+ if (group) for (const att of group.attachments) {
1066
+ att.rearm();
1067
+ att.resetAuthRetry();
1068
+ }
1069
+ try {
1070
+ this.#send([
1071
+ "REQ",
1072
+ sub.id,
1073
+ ...sub.replayFilters()
1074
+ ]);
1075
+ } catch {
1076
+ return false;
1077
+ }
633
1078
  }
1079
+ return true;
634
1080
  }
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));
1081
+ closeAll(reason) {
1082
+ const groups = [...this.#liveBySubId.values()];
1083
+ for (const group of groups) invokeSafely(() => {
1084
+ this.#endLiveGroup(group, {
1085
+ sendClose: false,
1086
+ reason
1087
+ });
665
1088
  });
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
- }
1089
+ for (const sub of this.#subs.values()) sub.end(reason);
1090
+ this.#subs.clear();
1091
+ this.#liveByFp.clear();
1092
+ this.#liveBySubId.clear();
1093
+ }
1094
+ #openExclusive(filters, opts) {
1095
+ const sub = new Subscription(filters, opts, (closed) => {
1096
+ this.#subs.delete(closed.id);
1097
+ try {
1098
+ if (this.#isOpen()) this.#send(["CLOSE", closed.id]);
1099
+ } catch {}
1100
+ });
1101
+ this.#subs.set(sub.id, sub);
1102
+ if (this.#isOpen()) this.#send([
1103
+ "REQ",
1104
+ sub.id,
1105
+ ...filters
1106
+ ]);
1107
+ else if (this.#enableReconnect) this.#scheduleReconnect();
1108
+ return sub;
1109
+ }
1110
+ #openLive(filters, opts) {
1111
+ const fp = filterFingerprint(filters);
1112
+ let group = this.#liveByFp.get(fp);
1113
+ if (group?.sub.eosed === true) group = void 0;
1114
+ let created = false;
1115
+ if (!group) {
1116
+ created = true;
1117
+ const slot = { group: void 0 };
1118
+ const wire = new Subscription(filters, { id: opts.id }, () => {
1119
+ const g = slot.group;
1120
+ if (g) this.#endLiveGroup(g, {
1121
+ sendClose: true,
1122
+ reason: "closed by client"
1123
+ });
1124
+ });
1125
+ group = {
1126
+ fp,
1127
+ sub: wire,
1128
+ attachments: /* @__PURE__ */ new Set()
1129
+ };
1130
+ slot.group = group;
1131
+ this.#liveByFp.set(fp, group);
1132
+ this.#liveBySubId.set(wire.id, group);
1133
+ this.#subs.set(wire.id, wire);
1134
+ }
1135
+ const slot = { handle: void 0 };
1136
+ const attached = group;
1137
+ const handle = new Subscription(filters, {
1138
+ ...opts,
1139
+ id: group.sub.id
1140
+ }, () => {
1141
+ const current = slot.handle;
1142
+ if (!current) return;
1143
+ this.#detachLive(attached, current);
695
1144
  });
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?.();
1145
+ slot.handle = handle;
1146
+ if (handle.closed) {
1147
+ if (created && group.attachments.size === 0) this.#forgetLiveGroup(group, "closed by client");
1148
+ return handle;
1149
+ }
1150
+ group.attachments.add(handle);
1151
+ if (created) {
1152
+ if (this.#isOpen()) this.#send([
1153
+ "REQ",
1154
+ group.sub.id,
1155
+ ...filters
1156
+ ]);
1157
+ else if (this.#enableReconnect) this.#scheduleReconnect();
1158
+ }
1159
+ return handle;
1160
+ }
1161
+ #forgetLiveGroup(group, reason) {
1162
+ if (this.#liveByFp.get(group.fp) === group) this.#liveByFp.delete(group.fp);
1163
+ this.#liveBySubId.delete(group.sub.id);
1164
+ this.#subs.delete(group.sub.id);
1165
+ group.sub.end(reason);
1166
+ }
1167
+ #detachLive(group, handle) {
1168
+ if (!group.attachments.delete(handle)) return;
1169
+ if (group.attachments.size > 0) return;
1170
+ this.#endLiveGroup(group, {
1171
+ sendClose: true,
1172
+ reason: "closed by client"
708
1173
  });
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
- }
1174
+ }
1175
+ #endLiveGroup(group, opts) {
1176
+ const { id } = group.sub;
1177
+ const remaining = [...group.attachments];
1178
+ group.attachments.clear();
1179
+ this.#forgetLiveGroup(group, opts.reason);
1180
+ if (opts.sendClose) try {
1181
+ if (this.#isOpen()) this.#send(["CLOSE", id]);
1182
+ } catch {}
1183
+ for (const att of remaining) invokeSafely(() => {
1184
+ att.close(opts.reason);
1185
+ });
1186
+ }
1187
+ #deliverLiveEvent(group, event) {
1188
+ const { sub } = group;
1189
+ const attachments = [...group.attachments];
1190
+ for (const att of attachments) att.notifyReceived(event.id);
1191
+ if (sub.isAtWatermark(event.id)) return;
1192
+ const recipients = [];
1193
+ for (const att of attachments) {
1194
+ if (att.closed) continue;
1195
+ if (!att.alreadyHas(event.id)) recipients.push(att);
1196
+ }
1197
+ if (recipients.length === 0) return;
1198
+ if (!this.#acceptEvent(event)) return;
1199
+ sub.noteVerified(event);
1200
+ for (const att of recipients) att.deliver(event);
1201
+ }
1202
+ #deliverLiveEose(group) {
1203
+ const attachments = [...group.attachments];
1204
+ for (const att of attachments) att.markEose();
1205
+ }
1206
+ };
721
1207
  //#endregion
722
1208
  //#region src/relay/relay.ts
723
1209
  /**
@@ -762,12 +1248,11 @@ var Relay = class Relay {
762
1248
  #connectFinish;
763
1249
  #connectTimer;
764
1250
  #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();
1251
+ #subscriptions;
1252
+ #ok = new OkTracker();
1253
+ #countRequests = new CountTracker();
1254
+ #authState;
1255
+ #ensureAuthed;
771
1256
  #neg = /* @__PURE__ */ new Map();
772
1257
  #WS;
773
1258
  #verify;
@@ -777,13 +1262,6 @@ var Relay = class Relay {
777
1262
  #enableReconnect;
778
1263
  #backoff;
779
1264
  #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
1265
  #authSigner;
788
1266
  /** `close()`/`disconnect()` was called; cleared by the next `connect()`. */
789
1267
  #manualStop = false;
@@ -791,13 +1269,7 @@ var Relay = class Relay {
791
1269
  #reconnectTimer;
792
1270
  #enablePing;
793
1271
  #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
- };
1272
+ #events = new Emitter();
801
1273
  constructor(url, opts = {}) {
802
1274
  this.url = normalizeURL(url);
803
1275
  this.#WS = opts.websocketImplementation ?? getWebSocketImplementation();
@@ -821,21 +1293,29 @@ var Relay = class Relay {
821
1293
  pingIntervalMs: opts.pingIntervalMs ?? 29e3,
822
1294
  pingTimeoutMs: opts.pingTimeoutMs ?? 2e4
823
1295
  });
824
- this.#live = {
825
- liveByFp: this.#liveByFp,
826
- liveBySubId: this.#liveBySubId,
827
- subs: this.#subs,
828
- connected: () => this.#isOpen(),
829
- enableReconnect: () => this.#enableReconnect,
1296
+ this.#subscriptions = new SubscriptionRegistry({
1297
+ isOpen: () => this.#isOpen(),
1298
+ enableReconnect: this.#enableReconnect,
830
1299
  send: (message) => this.#send(message),
831
1300
  scheduleReconnect: () => this.#scheduleReconnect(),
832
- acceptEvent: (event) => this.#acceptEvent(event),
833
- armEoseTimeout
834
- };
1301
+ acceptEvent: (event) => this.#acceptEvent(event)
1302
+ });
1303
+ this.#authState = new RelayAuth({
1304
+ url: this.url,
1305
+ isOpen: () => this.#isOpen(),
1306
+ sendAuth: (event, timeoutMs) => this.#ok.track(event.id, {
1307
+ timeoutMs,
1308
+ timeoutError: () => new RelayTimeoutError("auth timed out", this.url),
1309
+ sendError: () => new RelayPublishError("auth send failed", this.url),
1310
+ send: () => this.#send(["AUTH", event])
1311
+ }),
1312
+ defaultTimeoutMs: this.#publishTimeoutMs
1313
+ });
1314
+ this.#ensureAuthed = this.#authState.ensureAuthed.bind(this.#authState, this.#authSigner);
835
1315
  }
836
1316
  /** Latest NIP-42 challenge, if any. */
837
1317
  get challenge() {
838
- return this.#challenge;
1318
+ return this.#authState.challenge;
839
1319
  }
840
1320
  get connected() {
841
1321
  return this.#isOpen();
@@ -857,11 +1337,11 @@ var Relay = class Relay {
857
1337
  }
858
1338
  /** User subscriptions only; dummy ping REQs are not counted. */
859
1339
  get subscriptionCount() {
860
- return this.#subs.size;
1340
+ return this.#subscriptions.size;
861
1341
  }
862
1342
  /** One-shot requests still awaiting a relay reply (EVENT ACK, COUNT, NEG). */
863
1343
  get inFlightCount() {
864
- return this.#publishes.size + this.#counts.size + this.#neg.size;
1344
+ return this.#ok.size + this.#countRequests.size + this.#neg.size;
865
1345
  }
866
1346
  /**
867
1347
  * Listen for a relay event. Multiple listeners per type are allowed and fire in registration
@@ -869,14 +1349,7 @@ var Relay = class Relay {
869
1349
  * function.
870
1350
  */
871
1351
  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));
1352
+ return this.#events.on(type, listener);
880
1353
  }
881
1354
  static async connect(url, opts) {
882
1355
  const relay = new Relay(url, opts);
@@ -912,6 +1385,15 @@ var Relay = class Relay {
912
1385
  } catch {}
913
1386
  if (this.#ws === ws) this.#ws = void 0;
914
1387
  };
1388
+ const guarded = (fn) => {
1389
+ return () => {
1390
+ if (gen !== this.#gen) {
1391
+ release();
1392
+ return;
1393
+ }
1394
+ fn();
1395
+ };
1396
+ };
915
1397
  const finish = (err) => {
916
1398
  if (settled) return;
917
1399
  settled = true;
@@ -924,18 +1406,14 @@ var Relay = class Relay {
924
1406
  };
925
1407
  this.#connecting = connecting;
926
1408
  this.#connectFinish = finish;
927
- const timer = setTimeout(() => {
928
- if (gen !== this.#gen) {
929
- release();
930
- return;
931
- }
1409
+ const timer = setTimeout(guarded(() => {
932
1410
  release();
933
1411
  finish(new RelayTimeoutError("connection timed out", this.url));
934
1412
  if (gen === this.#gen) this.#handleSocketDeath("connection timed out", {
935
1413
  fromConnectAttempt: true,
936
1414
  gen
937
1415
  });
938
- }, timeoutMs);
1416
+ }), timeoutMs);
939
1417
  this.#connectTimer = timer;
940
1418
  try {
941
1419
  ws = new this.#WS(this.url);
@@ -949,36 +1427,24 @@ var Relay = class Relay {
949
1427
  return;
950
1428
  }
951
1429
  this.#ws = ws;
952
- const onOpen = () => {
953
- if (gen !== this.#gen) {
954
- release();
955
- return;
956
- }
1430
+ const onOpen = guarded(() => {
957
1431
  this.#setStatus(RelayStatus.Connected);
958
1432
  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)) {
1433
+ this.#authState.resetConnection();
1434
+ if (!this.#subscriptions.replayAll()) {
965
1435
  this.#setStatus(RelayStatus.Disconnected);
966
1436
  release();
967
1437
  finish(new RelayConnectionError("connection failed", this.url));
968
- if (this.#enableReconnect && !this.#manualStop && this.#subs.size > 0) this.#scheduleReconnect();
1438
+ if (this.#enableReconnect && !this.#manualStop && this.#subscriptions.size > 0) this.#scheduleReconnect();
969
1439
  return;
970
1440
  }
971
1441
  this.#reconnectAttempts = 0;
972
1442
  if (this.#enablePing) this.#ping.start();
973
1443
  else this.#ping.stop();
974
- if (wasReconnect) this.#emit("reconnect", void 0);
1444
+ if (wasReconnect) this.#events.emit("reconnect", void 0);
975
1445
  finish();
976
- };
977
- const onError = () => {
978
- if (gen !== this.#gen) {
979
- release();
980
- return;
981
- }
1446
+ });
1447
+ const onError = guarded(() => {
982
1448
  const fromConnectAttempt = !settled;
983
1449
  release();
984
1450
  finish(new RelayConnectionError("connection failed", this.url));
@@ -986,12 +1452,8 @@ var Relay = class Relay {
986
1452
  fromConnectAttempt,
987
1453
  gen
988
1454
  });
989
- };
990
- const onClose = () => {
991
- if (gen !== this.#gen) {
992
- release();
993
- return;
994
- }
1455
+ });
1456
+ const onClose = guarded(() => {
995
1457
  const fromConnectAttempt = !settled;
996
1458
  release();
997
1459
  if (fromConnectAttempt) finish(new RelayConnectionError("websocket closed", this.url));
@@ -999,7 +1461,7 @@ var Relay = class Relay {
999
1461
  fromConnectAttempt,
1000
1462
  gen
1001
1463
  });
1002
- };
1464
+ });
1003
1465
  const onMessage = (ev) => {
1004
1466
  if (gen !== this.#gen) return;
1005
1467
  const data = typeof ev === "object" && ev !== null && "data" in ev ? ev.data : ev;
@@ -1046,12 +1508,12 @@ var Relay = class Relay {
1046
1508
  this.#ping.stop();
1047
1509
  this.#connectFinish?.(new RelayClosedError("relay closed", this.url));
1048
1510
  try {
1049
- closeAllSubscriptions(this.#live, "relay closed");
1511
+ this.#subscriptions.closeAll("relay closed");
1050
1512
  } finally {
1051
1513
  this.#failPending(new RelayClosedError("relay closed", this.url));
1052
1514
  this.#detachSocketHandlers();
1053
1515
  this.#teardownSocket();
1054
- this.#emit("close", void 0);
1516
+ this.#events.emit("close", void 0);
1055
1517
  }
1056
1518
  }
1057
1519
  /**
@@ -1086,28 +1548,10 @@ var Relay = class Relay {
1086
1548
  this.#reconnectTimer = void 0;
1087
1549
  }
1088
1550
  }
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
1551
  #failPending(err) {
1108
- this.#rejectPublishes(err);
1109
- this.#rejectCounts(err);
1110
- this.#rejectNeg(err);
1552
+ this.#ok.rejectAll(err);
1553
+ this.#countRequests.rejectAll(err);
1554
+ failAllNegSessions(this.#neg, err);
1111
1555
  }
1112
1556
  /** Unexpected socket death. Keep subscriptions if reconnecting. */
1113
1557
  #handleSocketDeath(reason, opts) {
@@ -1117,15 +1561,15 @@ var Relay = class Relay {
1117
1561
  this.#detachSocketHandlers();
1118
1562
  this.#ws = void 0;
1119
1563
  this.#failPending(new RelayClosedError(reason, this.url));
1120
- if (this.#enableReconnect && !this.#manualStop && this.#subs.size > 0) {
1564
+ if (this.#enableReconnect && !this.#manualStop && this.#subscriptions.size > 0) {
1121
1565
  this.#setStatus(RelayStatus.Disconnected);
1122
1566
  this.#scheduleReconnect();
1123
1567
  return;
1124
1568
  }
1125
1569
  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);
1570
+ if (opts.fromConnectAttempt !== true || this.#subscriptions.size > 0) {
1571
+ this.#subscriptions.closeAll(reason);
1572
+ if (!this.#manualStop) this.#events.emit("close", void 0);
1129
1573
  }
1130
1574
  this.#setStatus(RelayStatus.Closed);
1131
1575
  }
@@ -1141,7 +1585,7 @@ var Relay = class Relay {
1141
1585
  try {
1142
1586
  await this.connect();
1143
1587
  } catch {
1144
- if (this.#enableReconnect && !this.#manualStop && this.#subs.size > 0 && !this.#isOpen()) this.#scheduleReconnect();
1588
+ if (this.#enableReconnect && !this.#manualStop && this.#subscriptions.size > 0 && !this.#isOpen()) this.#scheduleReconnect();
1145
1589
  }
1146
1590
  })();
1147
1591
  }, delay);
@@ -1162,91 +1606,78 @@ var Relay = class Relay {
1162
1606
  case "EVENT": {
1163
1607
  const [, subId, event] = msg;
1164
1608
  if (this.#ping.hasWaiter(subId)) return;
1165
- onSubEvent(this.#live, subId, event);
1609
+ this.#subscriptions.handleEvent(subId, event);
1166
1610
  break;
1167
1611
  }
1168
1612
  case "EOSE": {
1169
1613
  const [, subId] = msg;
1170
1614
  if (this.#ping.finishDummyPing(subId)) return;
1171
- onSubEose(this.#live, subId);
1615
+ this.#subscriptions.handleEose(subId);
1172
1616
  break;
1173
1617
  }
1174
1618
  case "CLOSED": {
1175
1619
  const [, subId, reason] = msg;
1176
1620
  if (this.#ping.finishDummyPing(subId)) return;
1177
- const countWaiter = this.#counts.get(subId);
1621
+ const countWaiter = this.#countRequests.get(subId);
1178
1622
  if (countWaiter) {
1179
1623
  if (isAuthRequired(reason) && this.#authSigner && !countWaiter.authRetried) {
1180
1624
  countWaiter.authRetried = true;
1181
- this.#authThenRecount(subId, countWaiter, reason);
1625
+ this.#countRequests.authRetry(countWaiter, {
1626
+ ensureAuthed: this.#ensureAuthed,
1627
+ isOpen: () => this.#isOpen(),
1628
+ failError: () => new RelayClosedError(reason || "COUNT closed", this.url),
1629
+ send: () => this.#send([
1630
+ "COUNT",
1631
+ subId,
1632
+ ...countWaiter.filters
1633
+ ])
1634
+ });
1182
1635
  return;
1183
1636
  }
1184
1637
  countWaiter.reject(new RelayClosedError(reason || "COUNT closed", this.url));
1185
1638
  return;
1186
1639
  }
1187
- const sub = this.#subs.get(subId);
1640
+ const sub = this.#subscriptions.get(subId);
1188
1641
  if (!sub) return;
1189
- if (isAuthRequired(reason) && this.#authSigner && !sub.authRetried) {
1190
- sub.authRetried = true;
1191
- this.#authThenResubscribe(sub, reason);
1642
+ if (isAuthRequired(reason) && this.#authSigner && sub.beginAuthRetry()) {
1643
+ this.#subscriptions.authRetry(sub, reason, this.#ensureAuthed);
1192
1644
  return;
1193
1645
  }
1194
- dropSubscription(this.#live, sub, reason);
1646
+ this.#subscriptions.drop(sub, reason);
1195
1647
  break;
1196
1648
  }
1197
1649
  case "OK": {
1198
1650
  const [, eventId, ok, message] = msg;
1199
- const waiter = this.#publishes.get(eventId);
1651
+ const waiter = this.#ok.handleOk(eventId, ok, message, (w) => !ok && isAuthRequired(message) && w.event !== void 0 && !w.authRetried && this.#authSigner !== void 0);
1200
1652
  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
1653
+ waiter.authRetried = true;
1654
+ this.#ok.authRetry(waiter, message, {
1655
+ ensureAuthed: this.#ensureAuthed,
1656
+ isOpen: () => this.#isOpen(),
1657
+ send: (event) => this.#send(["EVENT", event])
1211
1658
  });
1212
- break;
1659
+ return;
1213
1660
  }
1214
1661
  case "COUNT": {
1215
1662
  const [, countId, payload] = msg;
1216
- const waiter = this.#counts.get(countId);
1663
+ const waiter = this.#countRequests.get(countId);
1217
1664
  if (!waiter) return;
1218
1665
  waiter.resolve(payload);
1219
1666
  break;
1220
1667
  }
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);
1668
+ case "NEG-MSG":
1669
+ case "NEG-ERR":
1670
+ dispatchNegMessage(this.#neg, msg);
1233
1671
  break;
1234
- }
1235
1672
  case "NOTICE": {
1236
1673
  const [, notice] = msg;
1237
- this.#emit("notice", notice);
1674
+ this.#events.emit("notice", notice);
1238
1675
  break;
1239
1676
  }
1240
1677
  case "AUTH": {
1241
1678
  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);
1679
+ this.#authState.handleChallenge(authChallenge);
1680
+ this.#events.emit("auth", authChallenge);
1250
1681
  break;
1251
1682
  }
1252
1683
  }
@@ -1256,19 +1687,18 @@ var Relay = class Relay {
1256
1687
  if (filters.length === 0) throw new MessageError("REQ requires at least one filter");
1257
1688
  if (!this.#isOpen() && !this.#enableReconnect) throw new RelayClosedError("not connected", this.url);
1258
1689
  const canonical = canonicalizeFilters(filters);
1259
- if (opts.closeOnEose === true) return openExclusive(this.#live, canonical, opts);
1260
- return subscribeLive(this.#live, canonical, opts);
1690
+ return this.#subscriptions.open(canonical, opts);
1261
1691
  }
1262
1692
  #acceptEvent(event) {
1263
1693
  if (this.#verifyDead) return false;
1264
1694
  try {
1265
1695
  const ok = this.#verify(event);
1266
- if (!ok) this.#emit("invalidevent", void 0);
1696
+ if (!ok) this.#events.emit("invalidevent", void 0);
1267
1697
  return ok;
1268
1698
  } catch (error) {
1269
1699
  if (error instanceof WasmPoisonedError) {
1270
1700
  this.#verifyDead = true;
1271
- this.#emit("notice", "wasm-poisoned: instance aborted");
1701
+ this.#events.emit("notice", "wasm-poisoned: instance aborted");
1272
1702
  return false;
1273
1703
  }
1274
1704
  throw error;
@@ -1299,35 +1729,16 @@ var Relay = class Relay {
1299
1729
  */
1300
1730
  async publish(event, opts) {
1301
1731
  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;
1732
+ const existing = this.#ok.inFlight(event.id);
1733
+ if (existing !== void 0) return existing;
1304
1734
  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
- }
1735
+ return this.#ok.track(event.id, {
1736
+ timeoutMs,
1737
+ timeoutError: () => new RelayTimeoutError("publish timed out", this.url),
1738
+ sendError: () => new RelayPublishError("publish failed", this.url),
1739
+ event,
1740
+ send: () => this.#send(["EVENT", event])
1329
1741
  });
1330
- return promise;
1331
1742
  }
1332
1743
  /**
1333
1744
  * NIP-45 COUNT: ask the relay how many events match `filters`. Resolves with the COUNT payload
@@ -1340,105 +1751,22 @@ var Relay = class Relay {
1340
1751
  const canonical = canonicalizeFilters(filters);
1341
1752
  const id = opts?.id === void 0 ? this.nextSubId("count") : createSubscriptionId(opts.id);
1342
1753
  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
- }
1754
+ return this.#countRequests.track(id, {
1755
+ timeoutMs,
1756
+ timeoutError: () => new RelayTimeoutError("count timed out", this.url),
1757
+ sendError: () => new RelayPublishError("count failed", this.url),
1758
+ filters: [...canonical],
1759
+ signal: opts?.signal,
1760
+ send: () => this.#send([
1761
+ "COUNT",
1762
+ id,
1763
+ ...canonical
1764
+ ])
1380
1765
  });
1381
1766
  }
1382
1767
  /** NIP-42 AUTH: sign the current challenge and wait for OK. */
1383
1768
  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
- }
1769
+ return this.#authState.auth(sign, opts);
1442
1770
  }
1443
1771
  /**
1444
1772
  * Clear a cached AUTH rejection for the current challenge so a later `auth()` signs again. A
@@ -1446,133 +1774,8 @@ var Relay = class Relay {
1446
1774
  * `auth` event is re-fired so the pool's automatic auth can run once more.
1447
1775
  */
1448
1776
  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
- }
1777
+ const challenge = this.#authState.resetRejection();
1778
+ if (challenge !== void 0) this.#events.emit("auth", challenge);
1576
1779
  }
1577
1780
  /**
1578
1781
  * NIP-77: run Negentropy reconciliation against this relay. Returns the local-only (`have`) and
@@ -1584,32 +1787,17 @@ var Relay = class Relay {
1584
1787
  async negReconcile(filter, storage, opts) {
1585
1788
  if (!this.#isOpen()) throw new RelayClosedError("not connected", this.url);
1586
1789
  throwIfAborted(opts?.signal);
1587
- const canonical = canonicalizeFilter(filter);
1588
1790
  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
- }
1791
+ return runTrackedNegSession({
1792
+ sessions: this.#neg,
1793
+ id,
1794
+ storage,
1795
+ filter: canonicalizeFilter(filter),
1796
+ timeoutMs: opts?.timeoutMs ?? this.#publishTimeoutMs,
1797
+ signal: opts?.signal,
1798
+ send: (message) => this.#send(message),
1799
+ url: this.url
1800
+ });
1613
1801
  }
1614
1802
  /** Generate a unique subscription id for this relay instance. */
1615
1803
  nextSubId(prefix = "sub") {
@@ -1620,4 +1808,4 @@ var Relay = class Relay {
1620
1808
  //#endregion
1621
1809
  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
1810
 
1623
- //# sourceMappingURL=relay-CvxPiQ_m.mjs.map
1811
+ //# sourceMappingURL=relay-quCs7MHD.mjs.map