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