@qntx/nostr 0.6.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 (183) hide show
  1. package/dist/{abort-Ba8tDrOI.mjs → abort-DBq2STPj.mjs} +13 -6
  2. package/dist/abort-DBq2STPj.mjs.map +1 -0
  3. package/dist/{builder-sSflu6nB.d.mts → builder-CTwkB2zM.d.mts} +14 -15
  4. package/dist/{builder-9tjI6WQ3.mjs → builder-aNU2gGFP.mjs} +11 -17
  5. package/dist/builder-aNU2gGFP.mjs.map +1 -0
  6. package/dist/{client-CadO5fAX.mjs → client-Cj2lUaVh.mjs} +141 -353
  7. package/dist/client-Cj2lUaVh.mjs.map +1 -0
  8. package/dist/client.d.mts +2 -2
  9. package/dist/client.mjs +2 -2
  10. package/dist/collections-DtwMc1NU.mjs +33 -0
  11. package/dist/collections-DtwMc1NU.mjs.map +1 -0
  12. package/dist/core.d.mts +9 -8
  13. package/dist/core.mjs +7 -7
  14. package/dist/emitter-DkCY6NkU.mjs +32 -0
  15. package/dist/emitter-DkCY6NkU.mjs.map +1 -0
  16. package/dist/{error-BW7re1UN.d.mts → error-BD_0QFKT.d.mts} +2 -2
  17. package/dist/{error-DrEIM7FO.mjs → error-BILMe3Gr.mjs} +2 -2
  18. package/dist/{error-DrEIM7FO.mjs.map → error-BILMe3Gr.mjs.map} +1 -1
  19. package/dist/{error-CR8Urc84.d.mts → error-BquXDmIN.d.mts} +4 -4
  20. package/dist/{error-B__OMBRY.mjs → error-DlhGW4mJ.mjs} +2 -2
  21. package/dist/{error-B__OMBRY.mjs.map → error-DlhGW4mJ.mjs.map} +1 -1
  22. package/dist/{event-D8Nfoh4K.mjs → event-CHf47dPM.mjs} +16 -10
  23. package/dist/event-CHf47dPM.mjs.map +1 -0
  24. package/dist/{event-fU3Y01PR.d.mts → event-DJeb_DIL.d.mts} +4 -2
  25. package/dist/{filter-C0IMByE3.d.mts → filter-jI_9htF7.d.mts} +2 -2
  26. package/dist/{gossip-oloDMWEj.mjs → gossip-DFxIBlsr.mjs} +23 -33
  27. package/dist/gossip-DFxIBlsr.mjs.map +1 -0
  28. package/dist/{gossip-DpIvkBuj.d.mts → gossip-OOeeFzG5.d.mts} +37 -28
  29. package/dist/gossip.d.mts +1 -1
  30. package/dist/gossip.mjs +1 -1
  31. package/dist/{index-D1bqFHKB.d.mts → index-BxCBX-8R.d.mts} +18 -18
  32. package/dist/{index-lvXeER5t.d.mts → index-CPVEyzNX.d.mts} +32 -50
  33. package/dist/{index-MxC28S4P.d.mts → index-C_w2WfVz.d.mts} +27 -29
  34. package/dist/{index-CNl2q67H.d.mts → index-Cl_1XdYP.d.mts} +111 -180
  35. package/dist/{index-CvkvqB16.d.mts → index-DU9tRFTZ.d.mts} +7 -20
  36. package/dist/{index-q4MDce_r.d.mts → index-DyKwQnbt.d.mts} +6 -8
  37. package/dist/index.d.mts +23 -21
  38. package/dist/index.mjs +21 -19
  39. package/dist/{instance-DQE8BoWt.mjs → instance-C62w3cEt.mjs} +26 -27
  40. package/dist/instance-C62w3cEt.mjs.map +1 -0
  41. package/dist/{instance-CIca-ynn.d.mts → instance-DjsL8h84.d.mts} +5 -5
  42. package/dist/{key-Cv4_pN3x.d.mts → key-D_5OgGT7.d.mts} +11 -7
  43. package/dist/{key-Bps-LqX7.mjs → key-DnVpchl6.mjs} +13 -9
  44. package/dist/key-DnVpchl6.mjs.map +1 -0
  45. package/dist/{loaders-o6YXB_Ri.mjs → loaders-BmPkQfzI.mjs} +73 -82
  46. package/dist/loaders-BmPkQfzI.mjs.map +1 -0
  47. package/dist/loaders.d.mts +3 -3
  48. package/dist/loaders.mjs +2 -2
  49. package/dist/{memory-C2EVx0_O.mjs → memory-CdJ07kub.mjs} +2 -2
  50. package/dist/memory-CdJ07kub.mjs.map +1 -0
  51. package/dist/{memory-index-Do1mz0WF.mjs → memory-index-Cbc83vOZ.mjs} +30 -247
  52. package/dist/memory-index-Cbc83vOZ.mjs.map +1 -0
  53. package/dist/message-oNr7t0SJ.mjs +194 -0
  54. package/dist/message-oNr7t0SJ.mjs.map +1 -0
  55. package/dist/{message-iGPIqnn0.d.mts → message-tclYCVNc.d.mts} +6 -6
  56. package/dist/{nip17-CKIufV98.d.mts → nip17-B5WgW4C4.d.mts} +6 -7
  57. package/dist/{nip19-BevCJZsp.d.mts → nip19-DpPN17bY.d.mts} +25 -31
  58. package/dist/{nip46-DDV4dC56.d.mts → nip46-BHaxC9TZ.d.mts} +19 -21
  59. package/dist/nip51-CR0Q8eym.d.mts +87 -0
  60. package/dist/{nip59-zQgdLcEr.d.mts → nip59-C-RWFQIv.d.mts} +11 -5
  61. package/dist/nip65-D1fbmhOg.d.mts +23 -0
  62. package/dist/{nip77-DH4RhS5r.d.mts → nip77-DzUwUxHq.d.mts} +8 -9
  63. package/dist/nips/blossom.d.mts +17 -13
  64. package/dist/nips/blossom.mjs +30 -9
  65. package/dist/nips/blossom.mjs.map +1 -1
  66. package/dist/nips/nip04.mjs +17 -7
  67. package/dist/nips/nip04.mjs.map +1 -1
  68. package/dist/nips/nip05.d.mts +9 -10
  69. package/dist/nips/nip05.mjs +1 -4
  70. package/dist/nips/nip05.mjs.map +1 -1
  71. package/dist/nips/nip10.d.mts +8 -8
  72. package/dist/nips/nip10.mjs +34 -32
  73. package/dist/nips/nip10.mjs.map +1 -1
  74. package/dist/nips/nip11.d.mts +28 -28
  75. package/dist/nips/nip11.mjs +4 -4
  76. package/dist/nips/nip11.mjs.map +1 -1
  77. package/dist/nips/nip13.d.mts +7 -7
  78. package/dist/nips/nip13.mjs +32 -10
  79. package/dist/nips/nip13.mjs.map +1 -1
  80. package/dist/nips/nip17.d.mts +1 -1
  81. package/dist/nips/nip17.mjs +16 -34
  82. package/dist/nips/nip17.mjs.map +1 -1
  83. package/dist/nips/nip19.d.mts +2 -2
  84. package/dist/nips/nip19.mjs +16 -28
  85. package/dist/nips/nip19.mjs.map +1 -1
  86. package/dist/nips/nip21.d.mts +9 -4
  87. package/dist/nips/nip21.mjs +15 -5
  88. package/dist/nips/nip21.mjs.map +1 -1
  89. package/dist/nips/nip27.d.mts +22 -22
  90. package/dist/nips/nip27.mjs +2 -2
  91. package/dist/nips/nip27.mjs.map +1 -1
  92. package/dist/nips/nip42.d.mts +1 -1
  93. package/dist/nips/nip42.mjs +2 -1
  94. package/dist/nips/nip42.mjs.map +1 -1
  95. package/dist/nips/nip44.mjs +2 -2
  96. package/dist/nips/nip44.mjs.map +1 -1
  97. package/dist/nips/nip46.d.mts +2 -2
  98. package/dist/nips/nip46.mjs +15 -19
  99. package/dist/nips/nip46.mjs.map +1 -1
  100. package/dist/nips/nip49.d.mts +9 -9
  101. package/dist/nips/nip49.mjs +4 -4
  102. package/dist/nips/nip49.mjs.map +1 -1
  103. package/dist/nips/nip51.d.mts +2 -86
  104. package/dist/nips/nip51.mjs +7 -23
  105. package/dist/nips/nip51.mjs.map +1 -1
  106. package/dist/nips/nip57.d.mts +25 -23
  107. package/dist/nips/nip57.mjs +12 -17
  108. package/dist/nips/nip57.mjs.map +1 -1
  109. package/dist/nips/nip59.d.mts +1 -1
  110. package/dist/nips/nip59.mjs +27 -19
  111. package/dist/nips/nip59.mjs.map +1 -1
  112. package/dist/nips/nip65.d.mts +2 -2
  113. package/dist/nips/nip65.mjs +14 -37
  114. package/dist/nips/nip65.mjs.map +1 -1
  115. package/dist/nips/nip77.d.mts +1 -1
  116. package/dist/nips/nip77.mjs +33 -32
  117. package/dist/nips/nip77.mjs.map +1 -1
  118. package/dist/nips/nip98.d.mts +5 -5
  119. package/dist/nips/nip98.mjs +8 -8
  120. package/dist/nips/nip98.mjs.map +1 -1
  121. package/dist/{pool-DBIYMtwy.d.mts → pool-CqQvUnsq.d.mts} +132 -108
  122. package/dist/pool-vlw74C4t.mjs +606 -0
  123. package/dist/pool-vlw74C4t.mjs.map +1 -0
  124. package/dist/put-w13ci3sC.mjs +187 -0
  125. package/dist/put-w13ci3sC.mjs.map +1 -0
  126. package/dist/{reactive-BfhPp9a3.mjs → reactive-Tz0otDIP.mjs} +125 -88
  127. package/dist/reactive-Tz0otDIP.mjs.map +1 -0
  128. package/dist/{reactive-7aqeip4A.d.mts → reactive-gGqUrzW8.d.mts} +20 -26
  129. package/dist/relay-quCs7MHD.mjs +1811 -0
  130. package/dist/relay-quCs7MHD.mjs.map +1 -0
  131. package/dist/relay.d.mts +4 -4
  132. package/dist/relay.mjs +3 -2
  133. package/dist/serial-Bzpw-xz3.mjs +21 -0
  134. package/dist/serial-Bzpw-xz3.mjs.map +1 -0
  135. package/dist/{signer-CYQutDzD.mjs → signer-poTGe6Er.mjs} +12 -14
  136. package/dist/signer-poTGe6Er.mjs.map +1 -0
  137. package/dist/signer.d.mts +2 -2
  138. package/dist/signer.mjs +2 -2
  139. package/dist/{storage-Dky3wuoH.mjs → storage-B5IXTVuS.mjs} +28 -51
  140. package/dist/storage-B5IXTVuS.mjs.map +1 -0
  141. package/dist/storage.d.mts +3 -3
  142. package/dist/storage.mjs +4 -4
  143. package/dist/store.d.mts +2 -2
  144. package/dist/store.mjs +1 -1
  145. package/dist/testing.d.mts +19 -19
  146. package/dist/testing.mjs +9 -9
  147. package/dist/testing.mjs.map +1 -1
  148. package/dist/{types-2pCUprRU.d.mts → types-BacwnDtU.d.mts} +10 -10
  149. package/dist/{types-CRQEk0Oi.d.mts → types-DNmiscfd.d.mts} +2 -2
  150. package/dist/{util-LfQirgDk.mjs → util-DMF9AIO0.mjs} +33 -7
  151. package/dist/util-DMF9AIO0.mjs.map +1 -0
  152. package/dist/{verifier-t5WoWbtY.mjs → verifier-B51Wkt5H.mjs} +3 -3
  153. package/dist/verifier-B51Wkt5H.mjs.map +1 -0
  154. package/dist/verifier-CgTFpgsC.d.mts +22 -0
  155. package/dist/wasm.browser.d.mts +3 -3
  156. package/dist/wasm.browser.mjs +3 -3
  157. package/dist/wasm.d.mts +3 -3
  158. package/dist/wasm.mjs +3 -3
  159. package/dist/wasm.mjs.map +1 -1
  160. package/package.json +5 -9
  161. package/dist/abort-Ba8tDrOI.mjs.map +0 -1
  162. package/dist/builder-9tjI6WQ3.mjs.map +0 -1
  163. package/dist/client-CadO5fAX.mjs.map +0 -1
  164. package/dist/event-D8Nfoh4K.mjs.map +0 -1
  165. package/dist/gossip-oloDMWEj.mjs.map +0 -1
  166. package/dist/instance-DQE8BoWt.mjs.map +0 -1
  167. package/dist/key-Bps-LqX7.mjs.map +0 -1
  168. package/dist/loaders-o6YXB_Ri.mjs.map +0 -1
  169. package/dist/memory-C2EVx0_O.mjs.map +0 -1
  170. package/dist/memory-index-Do1mz0WF.mjs.map +0 -1
  171. package/dist/message-DcY6Fi9o.mjs +0 -193
  172. package/dist/message-DcY6Fi9o.mjs.map +0 -1
  173. package/dist/nip65-CjgmdpPB.d.mts +0 -26
  174. package/dist/nips/nip96.d.mts +0 -40
  175. package/dist/nips/nip96.mjs +0 -118
  176. package/dist/nips/nip96.mjs.map +0 -1
  177. package/dist/pool-As20OGxB.mjs +0 -2166
  178. package/dist/pool-As20OGxB.mjs.map +0 -1
  179. package/dist/reactive-BfhPp9a3.mjs.map +0 -1
  180. package/dist/signer-CYQutDzD.mjs.map +0 -1
  181. package/dist/storage-Dky3wuoH.mjs.map +0 -1
  182. package/dist/util-LfQirgDk.mjs.map +0 -1
  183. package/dist/verifier-t5WoWbtY.mjs.map +0 -1
@@ -0,0 +1,1811 @@
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
+ 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-oNr7t0SJ.mjs";
5
+ import { t as NoSignerError } from "./error-DlhGW4mJ.mjs";
6
+ import { t as invokeSafely } from "./report-CHld4uWj.mjs";
7
+ 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";
10
+ import { Nip77Error, runNegSession } from "./nips/nip77.mjs";
11
+ //#region src/relay/error.ts
12
+ /** Base class for relay errors; carries the relay URL when known. */
13
+ var RelayError = class extends NostrError {
14
+ name = "RelayError";
15
+ url;
16
+ constructor(message, url, options) {
17
+ super(url !== void 0 && url !== "" ? `${message} (${url})` : message, options);
18
+ this.url = url;
19
+ }
20
+ };
21
+ /** A relay connection attempt or socket failed. */
22
+ var RelayConnectionError = class extends RelayError {
23
+ name = "RelayConnectionError";
24
+ };
25
+ /** A relay answered an EVENT with `ok: false`. */
26
+ var RelayPublishError = class extends RelayError {
27
+ name = "RelayPublishError";
28
+ };
29
+ /** A subscription was closed by the relay (`CLOSED`) or the socket dropped. */
30
+ var RelayClosedError = class extends RelayError {
31
+ name = "RelayClosedError";
32
+ };
33
+ /** A relay operation exceeded its deadline. */
34
+ var RelayTimeoutError = class extends RelayError {
35
+ name = "RelayTimeoutError";
36
+ };
37
+ /**
38
+ * A relay was suspended for delivering more events that fail id/signature verification than its
39
+ * {@link PoolOptions.invalidEventPolicy} allows. Carries `until`, the epoch-ms time when the relay
40
+ * may connect again.
41
+ */
42
+ var RelaySuspendedError = class extends RelayError {
43
+ name = "RelaySuspendedError";
44
+ /** Epoch milliseconds when the suspension lifts. */
45
+ until;
46
+ constructor(url, until) {
47
+ super("relay suspended for delivering invalid events", url);
48
+ this.until = until;
49
+ }
50
+ };
51
+ //#endregion
52
+ //#region src/relay/websocket.ts
53
+ let impl;
54
+ try {
55
+ if (globalThis.WebSocket !== void 0) impl = globalThis.WebSocket;
56
+ } catch {}
57
+ /** Inject a WebSocket implementation (required on Node unless undici/global is present). */
58
+ function useWebSocketImplementation(websocketImplementation) {
59
+ impl = websocketImplementation;
60
+ }
61
+ function getWebSocketImplementation() {
62
+ if (!impl) throw new RelayConnectionError("No WebSocket implementation available; call useWebSocketImplementation() or run in a browser");
63
+ return impl;
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
573
+ //#region src/relay/neg-session.ts
574
+ function createNegSession() {
575
+ return {
576
+ queue: [],
577
+ waiter: void 0,
578
+ error: void 0
579
+ };
580
+ }
581
+ function pushNegMsg(session, hex) {
582
+ if (session.waiter) {
583
+ const { waiter } = session;
584
+ session.waiter = void 0;
585
+ waiter.resolve(hex);
586
+ } else session.queue.push(hex);
587
+ }
588
+ function failNegSession(session, err) {
589
+ session.error = err;
590
+ if (session.waiter) {
591
+ const { waiter } = session;
592
+ session.waiter = void 0;
593
+ waiter.reject(err);
594
+ }
595
+ }
596
+ function failNegErr(session, reason) {
597
+ failNegSession(session, new Nip77Error(reason));
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
+ }
614
+ /** Wire queue/timeout/abort around `runNegSession`. Not a second session class. */
615
+ async function runWiredNegSession(opts) {
616
+ const { session, storage, filter, id, timeoutMs, signal, send, url } = opts;
617
+ const deadline = Date.now() + timeoutMs;
618
+ const timedOut = () => new RelayTimeoutError("negentropy timed out", url);
619
+ const remainingMs = () => deadline - Date.now();
620
+ const next = async () => {
621
+ if (session.error !== void 0) throw session.error;
622
+ if (remainingMs() <= 0) throw timedOut();
623
+ const queued = session.queue.shift();
624
+ if (queued !== void 0) return queued;
625
+ return new Promise((resolve, reject) => {
626
+ const timer = setTimeout(() => {
627
+ session.waiter = void 0;
628
+ disposeAbort();
629
+ reject(timedOut());
630
+ }, remainingMs());
631
+ const fail = (err) => {
632
+ clearTimeout(timer);
633
+ session.waiter = void 0;
634
+ disposeAbort();
635
+ reject(err);
636
+ };
637
+ session.waiter = {
638
+ resolve: (hex) => {
639
+ clearTimeout(timer);
640
+ disposeAbort();
641
+ resolve(hex);
642
+ },
643
+ reject: (err) => fail(err)
644
+ };
645
+ const disposeAbort = onAbort(signal, () => {
646
+ if (signal) fail(abortReason(signal));
647
+ });
648
+ });
649
+ };
650
+ return runNegSession({
651
+ storage,
652
+ openingSend: (hex) => {
653
+ send([
654
+ "NEG-OPEN",
655
+ id,
656
+ filter,
657
+ hex
658
+ ]);
659
+ },
660
+ msgSend: (hex) => {
661
+ send([
662
+ "NEG-MSG",
663
+ id,
664
+ hex
665
+ ]);
666
+ },
667
+ next
668
+ });
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
+ };
839
+ const PING_DUMMY_FILTER = {
840
+ ids: ["a".repeat(64)],
841
+ limit: 0
842
+ };
843
+ /** Browser WebSocket and `ws` both use 1 for OPEN. */
844
+ const WS_OPEN = 1;
845
+ /** Node `ws` delivers `pong` on the EventEmitter, not via addEventListener. */
846
+ function canNativePing(ws) {
847
+ return typeof ws.ping === "function" && (typeof ws.once === "function" || typeof ws.on === "function");
848
+ }
849
+ /** REQ-dummy or native ping/pong keepalive. Does not import class Relay. */
850
+ var PingLoop = class {
851
+ #opts;
852
+ #timer;
853
+ #waiters = /* @__PURE__ */ new Map();
854
+ #gen = 0;
855
+ #nativePing;
856
+ constructor(opts) {
857
+ this.#opts = opts;
858
+ }
859
+ start() {
860
+ this.stop();
861
+ this.#timer = setInterval(() => {
862
+ this.#pingpong();
863
+ }, this.#opts.pingIntervalMs);
864
+ }
865
+ stop() {
866
+ if (this.#timer !== void 0) {
867
+ clearInterval(this.#timer);
868
+ this.#timer = void 0;
869
+ }
870
+ this.#abortCurrentPing();
871
+ }
872
+ hasWaiter(id) {
873
+ return this.#waiters.has(id);
874
+ }
875
+ finishDummyPing(id) {
876
+ const waiter = this.#waiters.get(id);
877
+ if (!waiter) return false;
878
+ this.#waiters.delete(id);
879
+ waiter.resolve(true);
880
+ try {
881
+ this.#opts.send(["CLOSE", id]);
882
+ } catch {}
883
+ return true;
884
+ }
885
+ #abortCurrentPing() {
886
+ this.#gen += 1;
887
+ const native = this.#nativePing;
888
+ this.#nativePing = void 0;
889
+ native?.abort();
890
+ for (const waiter of this.#waiters.values()) waiter.resolve(false);
891
+ this.#waiters.clear();
892
+ }
893
+ async #pingpong() {
894
+ const ws = this.#opts.getWs();
895
+ if (!ws || ws.readyState !== WS_OPEN) return;
896
+ if (this.#nativePing !== void 0 || this.#waiters.size > 0) return;
897
+ const gen = this.#gen;
898
+ const ping = canNativePing(ws) ? this.#waitForNativePing(ws) : this.#waitForDummyPing();
899
+ const ok = await new Promise((resolve) => {
900
+ let settled = false;
901
+ const done = (alive) => {
902
+ if (settled) return;
903
+ settled = true;
904
+ clearTimeout(timer);
905
+ resolve(alive);
906
+ };
907
+ const timer = setTimeout(() => {
908
+ if (this.#nativePing !== void 0 || this.#waiters.size > 0) done(false);
909
+ }, this.#opts.pingTimeoutMs);
910
+ (async () => {
911
+ done(await ping);
912
+ })();
913
+ });
914
+ if (gen !== this.#gen) return;
915
+ if (!ok) {
916
+ this.#abortCurrentPing();
917
+ this.#opts.closeWs();
918
+ }
919
+ }
920
+ async #waitForNativePing(ws) {
921
+ return new Promise((resolve) => {
922
+ let settled = false;
923
+ const finish = (alive) => {
924
+ if (settled) return;
925
+ settled = true;
926
+ this.#nativePing = void 0;
927
+ ws.off?.("pong", onPong);
928
+ resolve(alive);
929
+ };
930
+ const onPong = () => finish(true);
931
+ this.#nativePing = { abort: () => finish(false) };
932
+ if (typeof ws.on === "function" && typeof ws.off === "function") ws.on("pong", onPong);
933
+ else if (typeof ws.once === "function") ws.once("pong", onPong);
934
+ else ws.on?.("pong", onPong);
935
+ try {
936
+ if (typeof ws.ping === "function") ws.ping();
937
+ else finish(false);
938
+ } catch {
939
+ finish(false);
940
+ }
941
+ });
942
+ }
943
+ async #waitForDummyPing() {
944
+ return new Promise((resolve) => {
945
+ const id = this.#opts.nextSubId("__ping__");
946
+ this.#waiters.set(id, { resolve });
947
+ try {
948
+ this.#opts.send([
949
+ "REQ",
950
+ id,
951
+ PING_DUMMY_FILTER
952
+ ]);
953
+ } catch {
954
+ this.#waiters.delete(id);
955
+ resolve(false);
956
+ }
957
+ });
958
+ }
959
+ };
960
+ //#endregion
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;
981
+ }
982
+ /** User subscriptions only; dummy ping REQs are not counted. */
983
+ get size() {
984
+ return this.#subs.size;
985
+ }
986
+ get(subId) {
987
+ return this.#subs.get(subId);
988
+ }
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);
1000
+ return;
1001
+ }
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;
1016
+ }
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;
1028
+ }
1029
+ this.#subs.delete(sub.id);
1030
+ sub.end(reason);
1031
+ }
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) {
1037
+ try {
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
+ }
1046
+ }
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([
1053
+ "REQ",
1054
+ sub.id,
1055
+ ...sub.replayFilters()
1056
+ ]);
1057
+ }
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
+ }
1078
+ }
1079
+ return true;
1080
+ }
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
+ });
1088
+ });
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);
1144
+ });
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"
1173
+ });
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
+ };
1207
+ //#endregion
1208
+ //#region src/relay/relay.ts
1209
+ /**
1210
+ * Relay lifecycle states:
1211
+ *
1212
+ * - `initialized` — constructed; `connect()` not yet attempted.
1213
+ * - `connecting` — a `connect()` attempt is in flight (socket opening / resubscribing).
1214
+ * - `connected` — socket open; REQ/EVENT/COUNT flow.
1215
+ * - `disconnected` — socket down: a reconnect is scheduled when `enableReconnect` has live
1216
+ * subscriptions to restore, or `disconnect()` severed manually (REQ state kept).
1217
+ * - `closed` — terminal until the next `connect()`: `close()` or a socket death without a scheduled
1218
+ * reconnect.
1219
+ */
1220
+ const RelayStatus = {
1221
+ Initialized: "initialized",
1222
+ Connecting: "connecting",
1223
+ Connected: "connected",
1224
+ Disconnected: "disconnected",
1225
+ Closed: "closed"
1226
+ };
1227
+ const DEFAULT_REQUEST_TIMEOUT_MS = 4400;
1228
+ const DEFAULT_BACKOFF = [
1229
+ 1e3,
1230
+ 2e3,
1231
+ 5e3,
1232
+ 1e4,
1233
+ 2e4,
1234
+ 3e4,
1235
+ 6e4
1236
+ ];
1237
+ const noop = () => {};
1238
+ /**
1239
+ * Single-relay NIP-01 client. Connection lifecycle, REQ/CLOSE, EVENT publish ACK, AUTH, COUNT,
1240
+ * NIP-77, optional reconnect.
1241
+ */
1242
+ var Relay = class Relay {
1243
+ url;
1244
+ #ws;
1245
+ #gen = 0;
1246
+ #status = RelayStatus.Initialized;
1247
+ #connecting;
1248
+ #connectFinish;
1249
+ #connectTimer;
1250
+ #socketHandlers;
1251
+ #subscriptions;
1252
+ #ok = new OkTracker();
1253
+ #countRequests = new CountTracker();
1254
+ #authState;
1255
+ #ensureAuthed;
1256
+ #neg = /* @__PURE__ */ new Map();
1257
+ #WS;
1258
+ #verify;
1259
+ #verifyDead = false;
1260
+ #publishTimeoutMs;
1261
+ #connectTimeoutMs;
1262
+ #enableReconnect;
1263
+ #backoff;
1264
+ #serial = 0;
1265
+ #authSigner;
1266
+ /** `close()`/`disconnect()` was called; cleared by the next `connect()`. */
1267
+ #manualStop = false;
1268
+ #reconnectAttempts = 0;
1269
+ #reconnectTimer;
1270
+ #enablePing;
1271
+ #ping;
1272
+ #events = new Emitter();
1273
+ constructor(url, opts = {}) {
1274
+ this.url = normalizeURL(url);
1275
+ this.#WS = opts.websocketImplementation ?? getWebSocketImplementation();
1276
+ this.#verify = opts.verifyEvent ?? verifyEvent;
1277
+ this.#publishTimeoutMs = opts.publishTimeoutMs ?? 4400;
1278
+ this.#connectTimeoutMs = opts.connectTimeoutMs ?? 5e3;
1279
+ this.#enableReconnect = opts.enableReconnect ?? false;
1280
+ this.#backoff = opts.reconnectBackoffMs ?? DEFAULT_BACKOFF;
1281
+ this.#enablePing = opts.enablePing ?? false;
1282
+ this.#authSigner = opts.authSigner;
1283
+ this.#ping = new PingLoop({
1284
+ send: (message) => this.#send(message),
1285
+ nextSubId: (prefix) => this.nextSubId(prefix),
1286
+ getWs: () => this.#ws,
1287
+ closeWs: () => {
1288
+ const ws = this.#ws;
1289
+ if (ws && ws.readyState === this.#WS.OPEN) try {
1290
+ ws.close();
1291
+ } catch {}
1292
+ },
1293
+ pingIntervalMs: opts.pingIntervalMs ?? 29e3,
1294
+ pingTimeoutMs: opts.pingTimeoutMs ?? 2e4
1295
+ });
1296
+ this.#subscriptions = new SubscriptionRegistry({
1297
+ isOpen: () => this.#isOpen(),
1298
+ enableReconnect: this.#enableReconnect,
1299
+ send: (message) => this.#send(message),
1300
+ scheduleReconnect: () => this.#scheduleReconnect(),
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);
1315
+ }
1316
+ /** Latest NIP-42 challenge, if any. */
1317
+ get challenge() {
1318
+ return this.#authState.challenge;
1319
+ }
1320
+ get connected() {
1321
+ return this.#isOpen();
1322
+ }
1323
+ get status() {
1324
+ return this.#status;
1325
+ }
1326
+ #isOpen() {
1327
+ return this.#status === RelayStatus.Connected;
1328
+ }
1329
+ #setStatus(next) {
1330
+ this.#status = next;
1331
+ }
1332
+ get generation() {
1333
+ return this.#gen;
1334
+ }
1335
+ get reconnectEnabled() {
1336
+ return this.#enableReconnect;
1337
+ }
1338
+ /** User subscriptions only; dummy ping REQs are not counted. */
1339
+ get subscriptionCount() {
1340
+ return this.#subscriptions.size;
1341
+ }
1342
+ /** One-shot requests still awaiting a relay reply (EVENT ACK, COUNT, NEG). */
1343
+ get inFlightCount() {
1344
+ return this.#ok.size + this.#countRequests.size + this.#neg.size;
1345
+ }
1346
+ /**
1347
+ * Listen for a relay event. Multiple listeners per type are allowed and fire in registration
1348
+ * order; a throwing listener is reported and does not break the dispatch. Returns an unsubscribe
1349
+ * function.
1350
+ */
1351
+ on(type, listener) {
1352
+ return this.#events.on(type, listener);
1353
+ }
1354
+ static async connect(url, opts) {
1355
+ const relay = new Relay(url, opts);
1356
+ await relay.connect({ signal: opts?.signal });
1357
+ return relay;
1358
+ }
1359
+ async connect(opts) {
1360
+ if (this.#isOpen()) return;
1361
+ if (this.#connecting) {
1362
+ await raceSignal(this.#connecting, opts?.signal);
1363
+ return;
1364
+ }
1365
+ throwIfAborted(opts?.signal);
1366
+ const gen = ++this.#gen;
1367
+ this.#manualStop = false;
1368
+ this.#clearReconnectTimer();
1369
+ this.#setStatus(RelayStatus.Connecting);
1370
+ const timeoutMs = opts?.timeoutMs ?? this.#connectTimeoutMs;
1371
+ let resolveConnect = noop;
1372
+ let rejectConnect = noop;
1373
+ let settled = false;
1374
+ let ws;
1375
+ const connecting = new Promise((resolve, reject) => {
1376
+ resolveConnect = resolve;
1377
+ rejectConnect = reject;
1378
+ });
1379
+ const release = () => {
1380
+ if (ws === void 0) return;
1381
+ this.#removeSocketListeners(handlers);
1382
+ if (this.#socketHandlers === handlers) this.#socketHandlers = void 0;
1383
+ try {
1384
+ if (ws.readyState !== this.#WS.CLOSED && ws.readyState !== this.#WS.CLOSING) ws.close();
1385
+ } catch {}
1386
+ if (this.#ws === ws) this.#ws = void 0;
1387
+ };
1388
+ const guarded = (fn) => {
1389
+ return () => {
1390
+ if (gen !== this.#gen) {
1391
+ release();
1392
+ return;
1393
+ }
1394
+ fn();
1395
+ };
1396
+ };
1397
+ const finish = (err) => {
1398
+ if (settled) return;
1399
+ settled = true;
1400
+ clearTimeout(timer);
1401
+ if (this.#connectTimer === timer) this.#connectTimer = void 0;
1402
+ if (this.#connectFinish === finish) this.#connectFinish = void 0;
1403
+ if (this.#connecting === connecting) this.#connecting = void 0;
1404
+ if (err === void 0) resolveConnect();
1405
+ else rejectConnect(err);
1406
+ };
1407
+ this.#connecting = connecting;
1408
+ this.#connectFinish = finish;
1409
+ const timer = setTimeout(guarded(() => {
1410
+ release();
1411
+ finish(new RelayTimeoutError("connection timed out", this.url));
1412
+ if (gen === this.#gen) this.#handleSocketDeath("connection timed out", {
1413
+ fromConnectAttempt: true,
1414
+ gen
1415
+ });
1416
+ }), timeoutMs);
1417
+ this.#connectTimer = timer;
1418
+ try {
1419
+ ws = new this.#WS(this.url);
1420
+ } catch (error) {
1421
+ finish(error);
1422
+ if (gen === this.#gen) this.#handleSocketDeath("connection failed", {
1423
+ fromConnectAttempt: true,
1424
+ gen
1425
+ });
1426
+ await raceSignal(connecting, opts?.signal);
1427
+ return;
1428
+ }
1429
+ this.#ws = ws;
1430
+ const onOpen = guarded(() => {
1431
+ this.#setStatus(RelayStatus.Connected);
1432
+ const wasReconnect = this.#reconnectAttempts > 0;
1433
+ this.#authState.resetConnection();
1434
+ if (!this.#subscriptions.replayAll()) {
1435
+ this.#setStatus(RelayStatus.Disconnected);
1436
+ release();
1437
+ finish(new RelayConnectionError("connection failed", this.url));
1438
+ if (this.#enableReconnect && !this.#manualStop && this.#subscriptions.size > 0) this.#scheduleReconnect();
1439
+ return;
1440
+ }
1441
+ this.#reconnectAttempts = 0;
1442
+ if (this.#enablePing) this.#ping.start();
1443
+ else this.#ping.stop();
1444
+ if (wasReconnect) this.#events.emit("reconnect", void 0);
1445
+ finish();
1446
+ });
1447
+ const onError = guarded(() => {
1448
+ const fromConnectAttempt = !settled;
1449
+ release();
1450
+ finish(new RelayConnectionError("connection failed", this.url));
1451
+ if (gen === this.#gen) this.#handleSocketDeath("connection failed", {
1452
+ fromConnectAttempt,
1453
+ gen
1454
+ });
1455
+ });
1456
+ const onClose = guarded(() => {
1457
+ const fromConnectAttempt = !settled;
1458
+ release();
1459
+ if (fromConnectAttempt) finish(new RelayConnectionError("websocket closed", this.url));
1460
+ if (gen === this.#gen) this.#handleSocketDeath("websocket closed", {
1461
+ fromConnectAttempt,
1462
+ gen
1463
+ });
1464
+ });
1465
+ const onMessage = (ev) => {
1466
+ if (gen !== this.#gen) return;
1467
+ const data = typeof ev === "object" && ev !== null && "data" in ev ? ev.data : ev;
1468
+ this.#onMessage(String(data));
1469
+ };
1470
+ const handlers = {
1471
+ onOpen,
1472
+ onError,
1473
+ onClose,
1474
+ onMessage,
1475
+ ws
1476
+ };
1477
+ this.#socketHandlers = handlers;
1478
+ ws.addEventListener("open", onOpen);
1479
+ ws.addEventListener("error", onError);
1480
+ ws.addEventListener("close", onClose);
1481
+ ws.addEventListener("message", onMessage);
1482
+ await raceSignal(connecting, opts?.signal);
1483
+ }
1484
+ #removeSocketListeners(h) {
1485
+ try {
1486
+ h.ws.removeEventListener("open", h.onOpen);
1487
+ h.ws.removeEventListener("error", h.onError);
1488
+ h.ws.removeEventListener("close", h.onClose);
1489
+ h.ws.removeEventListener("message", h.onMessage);
1490
+ } catch {}
1491
+ }
1492
+ #detachSocketHandlers() {
1493
+ const h = this.#socketHandlers;
1494
+ if (!h) return;
1495
+ this.#removeSocketListeners(h);
1496
+ this.#socketHandlers = void 0;
1497
+ }
1498
+ /** Graceful shutdown: disables reconnect and closes all subscriptions. */
1499
+ close() {
1500
+ this.#gen += 1;
1501
+ this.#setStatus(RelayStatus.Closed);
1502
+ this.#manualStop = true;
1503
+ this.#clearReconnectTimer();
1504
+ if (this.#connectTimer !== void 0) {
1505
+ clearTimeout(this.#connectTimer);
1506
+ this.#connectTimer = void 0;
1507
+ }
1508
+ this.#ping.stop();
1509
+ this.#connectFinish?.(new RelayClosedError("relay closed", this.url));
1510
+ try {
1511
+ this.#subscriptions.closeAll("relay closed");
1512
+ } finally {
1513
+ this.#failPending(new RelayClosedError("relay closed", this.url));
1514
+ this.#detachSocketHandlers();
1515
+ this.#teardownSocket();
1516
+ this.#events.emit("close", void 0);
1517
+ }
1518
+ }
1519
+ /**
1520
+ * Sever the socket while keeping every subscription: unlike {@link close}, open REQ state is
1521
+ * preserved and reconnect stays suppressed — the next {@link connect} re-subscribes all of them.
1522
+ * {@link Pool} uses this to suspend a relay without tearing down its live subscriptions.
1523
+ */
1524
+ disconnect() {
1525
+ this.#gen += 1;
1526
+ this.#setStatus(RelayStatus.Disconnected);
1527
+ this.#manualStop = true;
1528
+ this.#clearReconnectTimer();
1529
+ if (this.#connectTimer !== void 0) {
1530
+ clearTimeout(this.#connectTimer);
1531
+ this.#connectTimer = void 0;
1532
+ }
1533
+ this.#ping.stop();
1534
+ this.#connectFinish?.(new RelayClosedError("relay disconnected", this.url));
1535
+ this.#failPending(new RelayClosedError("relay disconnected", this.url));
1536
+ this.#detachSocketHandlers();
1537
+ this.#teardownSocket();
1538
+ }
1539
+ #teardownSocket() {
1540
+ try {
1541
+ this.#ws?.close();
1542
+ } catch {}
1543
+ this.#ws = void 0;
1544
+ }
1545
+ #clearReconnectTimer() {
1546
+ if (this.#reconnectTimer !== void 0) {
1547
+ clearTimeout(this.#reconnectTimer);
1548
+ this.#reconnectTimer = void 0;
1549
+ }
1550
+ }
1551
+ #failPending(err) {
1552
+ this.#ok.rejectAll(err);
1553
+ this.#countRequests.rejectAll(err);
1554
+ failAllNegSessions(this.#neg, err);
1555
+ }
1556
+ /** Unexpected socket death. Keep subscriptions if reconnecting. */
1557
+ #handleSocketDeath(reason, opts) {
1558
+ if (opts.gen !== this.#gen) return;
1559
+ this.#gen += 1;
1560
+ this.#ping.stop();
1561
+ this.#detachSocketHandlers();
1562
+ this.#ws = void 0;
1563
+ this.#failPending(new RelayClosedError(reason, this.url));
1564
+ if (this.#enableReconnect && !this.#manualStop && this.#subscriptions.size > 0) {
1565
+ this.#setStatus(RelayStatus.Disconnected);
1566
+ this.#scheduleReconnect();
1567
+ return;
1568
+ }
1569
+ this.#setStatus(RelayStatus.Disconnected);
1570
+ if (opts.fromConnectAttempt !== true || this.#subscriptions.size > 0) {
1571
+ this.#subscriptions.closeAll(reason);
1572
+ if (!this.#manualStop) this.#events.emit("close", void 0);
1573
+ }
1574
+ this.#setStatus(RelayStatus.Closed);
1575
+ }
1576
+ #scheduleReconnect() {
1577
+ if (this.#reconnectTimer !== void 0) return;
1578
+ if (this.#isOpen()) return;
1579
+ const delay = this.#backoff[Math.min(this.#reconnectAttempts, this.#backoff.length - 1)] ?? 6e4;
1580
+ this.#reconnectAttempts += 1;
1581
+ this.#reconnectTimer = setTimeout(() => {
1582
+ this.#reconnectTimer = void 0;
1583
+ if (this.#manualStop || this.#isOpen()) return;
1584
+ (async () => {
1585
+ try {
1586
+ await this.connect();
1587
+ } catch {
1588
+ if (this.#enableReconnect && !this.#manualStop && this.#subscriptions.size > 0 && !this.#isOpen()) this.#scheduleReconnect();
1589
+ }
1590
+ })();
1591
+ }, delay);
1592
+ }
1593
+ #send(message) {
1594
+ if (!this.#ws || !this.#isOpen()) throw new RelayClosedError("not connected", this.url);
1595
+ const raw = typeof message === "string" ? message : encodeClientMessage(message);
1596
+ this.#ws.send(raw);
1597
+ }
1598
+ #onMessage(raw) {
1599
+ let msg;
1600
+ try {
1601
+ msg = parseRelayMessage(raw);
1602
+ } catch {
1603
+ return;
1604
+ }
1605
+ switch (msg[0]) {
1606
+ case "EVENT": {
1607
+ const [, subId, event] = msg;
1608
+ if (this.#ping.hasWaiter(subId)) return;
1609
+ this.#subscriptions.handleEvent(subId, event);
1610
+ break;
1611
+ }
1612
+ case "EOSE": {
1613
+ const [, subId] = msg;
1614
+ if (this.#ping.finishDummyPing(subId)) return;
1615
+ this.#subscriptions.handleEose(subId);
1616
+ break;
1617
+ }
1618
+ case "CLOSED": {
1619
+ const [, subId, reason] = msg;
1620
+ if (this.#ping.finishDummyPing(subId)) return;
1621
+ const countWaiter = this.#countRequests.get(subId);
1622
+ if (countWaiter) {
1623
+ if (isAuthRequired(reason) && this.#authSigner && !countWaiter.authRetried) {
1624
+ countWaiter.authRetried = true;
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
+ });
1635
+ return;
1636
+ }
1637
+ countWaiter.reject(new RelayClosedError(reason || "COUNT closed", this.url));
1638
+ return;
1639
+ }
1640
+ const sub = this.#subscriptions.get(subId);
1641
+ if (!sub) return;
1642
+ if (isAuthRequired(reason) && this.#authSigner && sub.beginAuthRetry()) {
1643
+ this.#subscriptions.authRetry(sub, reason, this.#ensureAuthed);
1644
+ return;
1645
+ }
1646
+ this.#subscriptions.drop(sub, reason);
1647
+ break;
1648
+ }
1649
+ case "OK": {
1650
+ const [, eventId, ok, message] = msg;
1651
+ const waiter = this.#ok.handleOk(eventId, ok, message, (w) => !ok && isAuthRequired(message) && w.event !== void 0 && !w.authRetried && this.#authSigner !== void 0);
1652
+ if (!waiter) return;
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])
1658
+ });
1659
+ return;
1660
+ }
1661
+ case "COUNT": {
1662
+ const [, countId, payload] = msg;
1663
+ const waiter = this.#countRequests.get(countId);
1664
+ if (!waiter) return;
1665
+ waiter.resolve(payload);
1666
+ break;
1667
+ }
1668
+ case "NEG-MSG":
1669
+ case "NEG-ERR":
1670
+ dispatchNegMessage(this.#neg, msg);
1671
+ break;
1672
+ case "NOTICE": {
1673
+ const [, notice] = msg;
1674
+ this.#events.emit("notice", notice);
1675
+ break;
1676
+ }
1677
+ case "AUTH": {
1678
+ const [, authChallenge] = msg;
1679
+ this.#authState.handleChallenge(authChallenge);
1680
+ this.#events.emit("auth", authChallenge);
1681
+ break;
1682
+ }
1683
+ }
1684
+ }
1685
+ /** Low-level REQ with callbacks. Survives reconnect when enableReconnect is on. */
1686
+ subscribe(filters, opts = {}) {
1687
+ if (filters.length === 0) throw new MessageError("REQ requires at least one filter");
1688
+ if (!this.#isOpen() && !this.#enableReconnect) throw new RelayClosedError("not connected", this.url);
1689
+ const canonical = canonicalizeFilters(filters);
1690
+ return this.#subscriptions.open(canonical, opts);
1691
+ }
1692
+ #acceptEvent(event) {
1693
+ if (this.#verifyDead) return false;
1694
+ try {
1695
+ const ok = this.#verify(event);
1696
+ if (!ok) this.#events.emit("invalidevent", void 0);
1697
+ return ok;
1698
+ } catch (error) {
1699
+ if (error instanceof WasmPoisonedError) {
1700
+ this.#verifyDead = true;
1701
+ this.#events.emit("notice", "wasm-poisoned: instance aborted");
1702
+ return false;
1703
+ }
1704
+ throw error;
1705
+ }
1706
+ }
1707
+ /** AsyncIterable of events for filters until the subscription is closed. */
1708
+ stream(filters, opts) {
1709
+ return streamFilters((f, o) => this.subscribe(f, o), filters, opts);
1710
+ }
1711
+ /**
1712
+ * One-shot query: collect events until the REQ ends, then close. `end` reports how it ended —
1713
+ * relay `EOSE`, relay `CLOSED` (with its reason), or the local timeout; events received before a
1714
+ * CLOSED or the deadline are still returned. Abort rejects; a failed (re)connect rejects.
1715
+ */
1716
+ async fetch(filters, opts) {
1717
+ if (filters.length === 0) throw new MessageError("REQ requires at least one filter");
1718
+ const canonical = canonicalizeFilters(filters);
1719
+ if (!this.#isOpen()) await this.connect({ signal: opts?.signal });
1720
+ return fetchFilters((f, o) => this.subscribe(f, o), canonical, {
1721
+ timeoutMs: opts?.timeoutMs ?? 4400,
1722
+ signal: opts?.signal,
1723
+ id: opts?.id
1724
+ });
1725
+ }
1726
+ /**
1727
+ * Publish an event and wait for OK. A concurrent publish of the same event id joins the in-flight
1728
+ * publish instead of racing it — the joiner's `timeoutMs` does not apply.
1729
+ */
1730
+ async publish(event, opts) {
1731
+ if (!this.#isOpen()) throw new RelayClosedError("not connected", this.url);
1732
+ const existing = this.#ok.inFlight(event.id);
1733
+ if (existing !== void 0) return existing;
1734
+ const timeoutMs = opts?.timeoutMs ?? this.#publishTimeoutMs;
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])
1741
+ });
1742
+ }
1743
+ /**
1744
+ * NIP-45 COUNT: ask the relay how many events match `filters`. Resolves with the COUNT payload
1745
+ * (count / optional approximate / optional hll).
1746
+ */
1747
+ async count(filters, opts) {
1748
+ if (!this.#isOpen()) throw new RelayClosedError("not connected", this.url);
1749
+ if (filters.length === 0) throw new RelayError("COUNT requires at least one filter", this.url);
1750
+ throwIfAborted(opts?.signal);
1751
+ const canonical = canonicalizeFilters(filters);
1752
+ const id = opts?.id === void 0 ? this.nextSubId("count") : createSubscriptionId(opts.id);
1753
+ const timeoutMs = opts?.timeoutMs ?? this.#publishTimeoutMs;
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
+ ])
1765
+ });
1766
+ }
1767
+ /** NIP-42 AUTH: sign the current challenge and wait for OK. */
1768
+ async auth(sign, opts) {
1769
+ return this.#authState.auth(sign, opts);
1770
+ }
1771
+ /**
1772
+ * Clear a cached AUTH rejection for the current challenge so a later `auth()` signs again. A
1773
+ * successful auth and in-flight answers are kept. When a challenge is pending and unanswered, an
1774
+ * `auth` event is re-fired so the pool's automatic auth can run once more.
1775
+ */
1776
+ resetAuth() {
1777
+ const challenge = this.#authState.resetRejection();
1778
+ if (challenge !== void 0) this.#events.emit("auth", challenge);
1779
+ }
1780
+ /**
1781
+ * NIP-77: run Negentropy reconciliation against this relay. Returns the local-only (`have`) and
1782
+ * remote-only (`need`) event ids. Does not upload or download events.
1783
+ *
1784
+ * `timeoutMs` is a single wall-clock deadline for the whole session (`NEG-OPEN` through the last
1785
+ * `NEG-MSG`), not a per-message budget. Default: {@link RelayOptions.publishTimeoutMs}.
1786
+ */
1787
+ async negReconcile(filter, storage, opts) {
1788
+ if (!this.#isOpen()) throw new RelayClosedError("not connected", this.url);
1789
+ throwIfAborted(opts?.signal);
1790
+ const id = opts?.id === void 0 ? this.nextSubId("neg") : assertSubscriptionId(opts.id);
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
+ });
1801
+ }
1802
+ /** Generate a unique subscription id for this relay instance. */
1803
+ nextSubId(prefix = "sub") {
1804
+ this.#serial += 1;
1805
+ return `${prefix}:${this.#serial}`;
1806
+ }
1807
+ };
1808
+ //#endregion
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 };
1810
+
1811
+ //# sourceMappingURL=relay-quCs7MHD.mjs.map